Add SocketConnectionFactory and http2cat (#14582)
This commit is contained in:
parent
53a54d9f91
commit
ff8363a638
|
|
@ -14,3 +14,4 @@ using System.Runtime.CompilerServices;
|
|||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||
[assembly: InternalsVisibleTo("http2cat, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Hostin
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebUtilities", "..\..\Http\WebUtilities\src\Microsoft.AspNetCore.WebUtilities.csproj", "{EE45763C-753D-4228-8E5D-A71F8BDB3D89}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "http2cat", "samples\http2cat\http2cat.csproj", "{3D6821F5-F242-4828-8DDE-89488E85512D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -452,6 +454,18 @@ Global
|
|||
{EE45763C-753D-4228-8E5D-A71F8BDB3D89}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EE45763C-753D-4228-8E5D-A71F8BDB3D89}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EE45763C-753D-4228-8E5D-A71F8BDB3D89}.Release|x86.Build.0 = Release|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -488,6 +502,7 @@ Global
|
|||
{D9872E91-EF1D-4181-82C9-584224ADE368} = {F0A1281A-B512-49D2-8362-21EE32B3674F}
|
||||
{E0AD50A3-2518-4060-8BB9-5649B04B3A6D} = {F0A1281A-B512-49D2-8362-21EE32B3674F}
|
||||
{EE45763C-753D-4228-8E5D-A71F8BDB3D89} = {F0A1281A-B512-49D2-8362-21EE32B3674F}
|
||||
{3D6821F5-F242-4828-8DDE-89488E85512D} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {48207B50-7D05-4B10-B585-890FE0F4FCE1}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Client
|
||||
{
|
||||
internal class SocketConnectionFactory : IConnectionFactory, IAsyncDisposable
|
||||
{
|
||||
private readonly SocketTransportOptions _options;
|
||||
private readonly MemoryPool<byte> _memoryPool;
|
||||
private readonly SocketsTrace _trace;
|
||||
|
||||
public SocketConnectionFactory(IOptions<SocketTransportOptions> options, ILoggerFactory loggerFactory)
|
||||
{
|
||||
if (options == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
_options = options.Value;
|
||||
_memoryPool = options.Value.MemoryPoolFactory();
|
||||
var logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Client");
|
||||
_trace = new SocketsTrace(logger);
|
||||
}
|
||||
|
||||
public async ValueTask<ConnectionContext> ConnectAsync(EndPoint endpoint, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ipEndPoint = endpoint as IPEndPoint;
|
||||
|
||||
if (ipEndPoint is null)
|
||||
{
|
||||
throw new NotSupportedException("The SocketConnectionFactory only supports IPEndPoints for now.");
|
||||
}
|
||||
|
||||
var socket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
NoDelay = _options.NoDelay
|
||||
};
|
||||
|
||||
await socket.ConnectAsync(ipEndPoint);
|
||||
|
||||
var socketConnection = new SocketConnection(
|
||||
socket,
|
||||
_memoryPool,
|
||||
PipeScheduler.ThreadPool,
|
||||
_trace,
|
||||
_options.MaxReadBufferSize,
|
||||
_options.MaxWriteBufferSize);
|
||||
|
||||
socketConnection.Start();
|
||||
return socketConnection;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_memoryPool.Dispose();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,10 @@
|
|||
<Reference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="http2cat" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="SocketsStrings.resx">
|
||||
<Generator></Generator>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,920 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace http2cat
|
||||
{
|
||||
public class Http2Utilities : IHttpHeadersHandler
|
||||
{
|
||||
public static readonly int MaxRequestHeaderFieldSize = 16 * 1024;
|
||||
public static readonly string _4kHeaderValue = new string('a', 4096);
|
||||
|
||||
public static readonly IEnumerable<KeyValuePair<string, string>> _browserRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
||||
new KeyValuePair<string, string>("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"),
|
||||
new KeyValuePair<string, string>("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
|
||||
new KeyValuePair<string, string>("accept-language", "en-US,en;q=0.5"),
|
||||
new KeyValuePair<string, string>("accept-encoding", "gzip, deflate, br"),
|
||||
new KeyValuePair<string, string>("upgrade-insecure-requests", "1"),
|
||||
};
|
||||
|
||||
public static readonly IEnumerable<KeyValuePair<string, string>> _postRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
||||
};
|
||||
|
||||
public static readonly IEnumerable<KeyValuePair<string, string>> _expectContinueRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Authority, "127.0.0.1"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||
new KeyValuePair<string, string>("expect", "100-continue"),
|
||||
};
|
||||
|
||||
public static readonly IEnumerable<KeyValuePair<string, string>> _requestTrailers = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("trailer-one", "1"),
|
||||
new KeyValuePair<string, string>("trailer-two", "2"),
|
||||
};
|
||||
|
||||
public static readonly IEnumerable<KeyValuePair<string, string>> _oneContinuationRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
||||
new KeyValuePair<string, string>("a", _4kHeaderValue),
|
||||
new KeyValuePair<string, string>("b", _4kHeaderValue),
|
||||
new KeyValuePair<string, string>("c", _4kHeaderValue),
|
||||
new KeyValuePair<string, string>("d", _4kHeaderValue)
|
||||
};
|
||||
|
||||
public static readonly IEnumerable<KeyValuePair<string, string>> _twoContinuationsRequestHeaders = new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
||||
new KeyValuePair<string, string>("a", _4kHeaderValue),
|
||||
new KeyValuePair<string, string>("b", _4kHeaderValue),
|
||||
new KeyValuePair<string, string>("c", _4kHeaderValue),
|
||||
new KeyValuePair<string, string>("d", _4kHeaderValue),
|
||||
new KeyValuePair<string, string>("e", _4kHeaderValue),
|
||||
new KeyValuePair<string, string>("f", _4kHeaderValue),
|
||||
new KeyValuePair<string, string>("g", _4kHeaderValue),
|
||||
};
|
||||
|
||||
public static IEnumerable<KeyValuePair<string, string>> ReadRateRequestHeaders(int expectedBytes) => new[]
|
||||
{
|
||||
new KeyValuePair<string, string>(HeaderNames.Method, "POST"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Path, "/" + expectedBytes),
|
||||
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
|
||||
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
|
||||
};
|
||||
|
||||
public static readonly byte[] _helloBytes = Encoding.ASCII.GetBytes("hello");
|
||||
public static readonly byte[] _worldBytes = Encoding.ASCII.GetBytes("world");
|
||||
public static readonly byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("hello, world");
|
||||
public static readonly byte[] _noData = new byte[0];
|
||||
public static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', Http2PeerSettings.MinAllowedMaxFrameSize));
|
||||
|
||||
internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings();
|
||||
internal readonly HPackEncoder _hpackEncoder = new HPackEncoder();
|
||||
internal readonly HPackDecoder _hpackDecoder;
|
||||
private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize];
|
||||
|
||||
public readonly Dictionary<string, string> _decodedHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
internal DuplexPipe.DuplexPipePair _pair;
|
||||
public long _bytesReceived;
|
||||
|
||||
public Http2Utilities(ConnectionContext clientConnectionContext)
|
||||
{
|
||||
_hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize);
|
||||
_pair = new DuplexPipe.DuplexPipePair(transport: null, application: clientConnectionContext.Transport);
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeader(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
_decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
|
||||
}
|
||||
|
||||
void IHttpHeadersHandler.OnHeadersComplete() { }
|
||||
|
||||
public async Task InitializeConnectionAsync(int expectedSettingsCount = 3)
|
||||
{
|
||||
await SendPreambleAsync().ConfigureAwait(false);
|
||||
await SendSettingsAsync();
|
||||
|
||||
await ExpectAsync(Http2FrameType.SETTINGS,
|
||||
withLength: expectedSettingsCount * Http2FrameReader.SettingSize,
|
||||
withFlags: 0,
|
||||
withStreamId: 0);
|
||||
|
||||
await ExpectAsync(Http2FrameType.WINDOW_UPDATE,
|
||||
withLength: 4,
|
||||
withFlags: 0,
|
||||
withStreamId: 0);
|
||||
|
||||
await ExpectAsync(Http2FrameType.SETTINGS,
|
||||
withLength: 0,
|
||||
withFlags: (byte)Http2SettingsFrameFlags.ACK,
|
||||
withStreamId: 0);
|
||||
}
|
||||
|
||||
public Task StartStreamAsync(int streamId, IEnumerable<KeyValuePair<string, string>> headers, bool endStream)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId);
|
||||
|
||||
var buffer = _headerEncodingBuffer.AsSpan();
|
||||
var done = _hpackEncoder.BeginEncode(headers, buffer, out var length);
|
||||
frame.PayloadLength = length;
|
||||
|
||||
if (done)
|
||||
{
|
||||
frame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS;
|
||||
}
|
||||
|
||||
if (endStream)
|
||||
{
|
||||
frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM;
|
||||
}
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
writableBuffer.Write(buffer.Slice(0, length));
|
||||
|
||||
while (!done)
|
||||
{
|
||||
frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId);
|
||||
|
||||
done = _hpackEncoder.Encode(buffer, out length);
|
||||
frame.PayloadLength = length;
|
||||
|
||||
if (done)
|
||||
{
|
||||
frame.ContinuationFlags = Http2ContinuationFrameFlags.END_HEADERS;
|
||||
}
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
writableBuffer.Write(buffer.Slice(0, length));
|
||||
}
|
||||
|
||||
return FlushAsync(writableBuffer);
|
||||
}
|
||||
|
||||
internal Dictionary<string, string> DecodeHeaders(Http2FrameWithPayload frame, bool endHeaders = false)
|
||||
{
|
||||
Assert.Equal(Http2FrameType.HEADERS, frame.Type);
|
||||
_hpackDecoder.Decode(frame.PayloadSequence, endHeaders, handler: this);
|
||||
return _decodedHeaders;
|
||||
}
|
||||
|
||||
/* https://tools.ietf.org/html/rfc7540#section-6.2
|
||||
+---------------+
|
||||
|Pad Length? (8)|
|
||||
+-+-------------+-----------------------------------------------+
|
||||
| Header Block Fragment (*) ...
|
||||
+---------------------------------------------------------------+
|
||||
| Padding (*) ...
|
||||
+---------------------------------------------------------------+
|
||||
*/
|
||||
public Task SendHeadersWithPaddingAsync(int streamId, IEnumerable<KeyValuePair<string, string>> headers, byte padLength, bool endStream)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED, streamId);
|
||||
frame.HeadersPadLength = padLength;
|
||||
|
||||
var extendedHeaderLength = 1; // Padding length field
|
||||
var buffer = _headerEncodingBuffer.AsSpan();
|
||||
var extendedHeader = buffer.Slice(0, extendedHeaderLength);
|
||||
extendedHeader[0] = padLength;
|
||||
var payload = buffer.Slice(extendedHeaderLength, buffer.Length - padLength - extendedHeaderLength);
|
||||
|
||||
_hpackEncoder.BeginEncode(headers, payload, out var length);
|
||||
var padding = buffer.Slice(extendedHeaderLength + length, padLength);
|
||||
padding.Fill(0);
|
||||
|
||||
frame.PayloadLength = extendedHeaderLength + length + padLength;
|
||||
|
||||
if (endStream)
|
||||
{
|
||||
frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM;
|
||||
}
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
writableBuffer.Write(buffer.Slice(0, frame.PayloadLength));
|
||||
return FlushAsync(writableBuffer);
|
||||
}
|
||||
|
||||
/* https://tools.ietf.org/html/rfc7540#section-6.2
|
||||
+-+-------------+-----------------------------------------------+
|
||||
|E| Stream Dependency? (31) |
|
||||
+-+-------------+-----------------------------------------------+
|
||||
| Weight? (8) |
|
||||
+-+-------------+-----------------------------------------------+
|
||||
| Header Block Fragment (*) ...
|
||||
+---------------------------------------------------------------+
|
||||
*/
|
||||
public Task SendHeadersWithPriorityAsync(int streamId, IEnumerable<KeyValuePair<string, string>> headers, byte priority, int streamDependency, bool endStream)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PRIORITY, streamId);
|
||||
frame.HeadersPriorityWeight = priority;
|
||||
frame.HeadersStreamDependency = streamDependency;
|
||||
|
||||
var extendedHeaderLength = 5; // stream dependency + weight
|
||||
var buffer = _headerEncodingBuffer.AsSpan();
|
||||
var extendedHeader = buffer.Slice(0, extendedHeaderLength);
|
||||
Bitshifter.WriteUInt31BigEndian(extendedHeader, (uint)streamDependency);
|
||||
extendedHeader[4] = priority;
|
||||
var payload = buffer.Slice(extendedHeaderLength);
|
||||
|
||||
_hpackEncoder.BeginEncode(headers, payload, out var length);
|
||||
|
||||
frame.PayloadLength = extendedHeaderLength + length;
|
||||
|
||||
if (endStream)
|
||||
{
|
||||
frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM;
|
||||
}
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
writableBuffer.Write(buffer.Slice(0, frame.PayloadLength));
|
||||
return FlushAsync(writableBuffer);
|
||||
}
|
||||
|
||||
/* https://tools.ietf.org/html/rfc7540#section-6.2
|
||||
+---------------+
|
||||
|Pad Length? (8)|
|
||||
+-+-------------+-----------------------------------------------+
|
||||
|E| Stream Dependency? (31) |
|
||||
+-+-------------+-----------------------------------------------+
|
||||
| Weight? (8) |
|
||||
+-+-------------+-----------------------------------------------+
|
||||
| Header Block Fragment (*) ...
|
||||
+---------------------------------------------------------------+
|
||||
| Padding (*) ...
|
||||
+---------------------------------------------------------------+
|
||||
*/
|
||||
public Task SendHeadersWithPaddingAndPriorityAsync(int streamId, IEnumerable<KeyValuePair<string, string>> headers, byte padLength, byte priority, int streamDependency, bool endStream)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.PADDED | Http2HeadersFrameFlags.PRIORITY, streamId);
|
||||
frame.HeadersPadLength = padLength;
|
||||
frame.HeadersPriorityWeight = priority;
|
||||
frame.HeadersStreamDependency = streamDependency;
|
||||
|
||||
var extendedHeaderLength = 6; // pad length + stream dependency + weight
|
||||
var buffer = _headerEncodingBuffer.AsSpan();
|
||||
var extendedHeader = buffer.Slice(0, extendedHeaderLength);
|
||||
extendedHeader[0] = padLength;
|
||||
Bitshifter.WriteUInt31BigEndian(extendedHeader.Slice(1), (uint)streamDependency);
|
||||
extendedHeader[5] = priority;
|
||||
var payload = buffer.Slice(extendedHeaderLength, buffer.Length - padLength - extendedHeaderLength);
|
||||
|
||||
_hpackEncoder.BeginEncode(headers, payload, out var length);
|
||||
var padding = buffer.Slice(extendedHeaderLength + length, padLength);
|
||||
padding.Fill(0);
|
||||
|
||||
frame.PayloadLength = extendedHeaderLength + length + padLength;
|
||||
|
||||
if (endStream)
|
||||
{
|
||||
frame.HeadersFlags |= Http2HeadersFrameFlags.END_STREAM;
|
||||
}
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
writableBuffer.Write(buffer.Slice(0, frame.PayloadLength));
|
||||
return FlushAsync(writableBuffer);
|
||||
}
|
||||
|
||||
public Task SendAsync(ReadOnlySpan<byte> span)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
writableBuffer.Write(span);
|
||||
return FlushAsync(writableBuffer);
|
||||
}
|
||||
|
||||
public static async Task FlushAsync(PipeWriter writableBuffer)
|
||||
{
|
||||
await writableBuffer.FlushAsync().AsTask().DefaultTimeout();
|
||||
}
|
||||
|
||||
public Task SendPreambleAsync() => SendAsync(new ArraySegment<byte>(Http2Connection.ClientPreface));
|
||||
|
||||
public async Task SendSettingsAsync()
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareSettings(Http2SettingsFrameFlags.NONE);
|
||||
var settings = _clientSettings.GetNonProtocolDefaults();
|
||||
var payload = new byte[settings.Count * Http2FrameReader.SettingSize];
|
||||
frame.PayloadLength = payload.Length;
|
||||
Http2FrameWriter.WriteSettings(settings, payload);
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
await SendAsync(payload);
|
||||
}
|
||||
|
||||
public async Task SendSettingsAckWithInvalidLengthAsync(int length)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareSettings(Http2SettingsFrameFlags.ACK);
|
||||
frame.PayloadLength = length;
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
await SendAsync(new byte[length]);
|
||||
}
|
||||
|
||||
public async Task SendSettingsWithInvalidStreamIdAsync(int streamId)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareSettings(Http2SettingsFrameFlags.NONE);
|
||||
frame.StreamId = streamId;
|
||||
var settings = _clientSettings.GetNonProtocolDefaults();
|
||||
var payload = new byte[settings.Count * Http2FrameReader.SettingSize];
|
||||
frame.PayloadLength = payload.Length;
|
||||
Http2FrameWriter.WriteSettings(settings, payload);
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
await SendAsync(payload);
|
||||
}
|
||||
|
||||
public async Task SendSettingsWithInvalidLengthAsync(int length)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareSettings(Http2SettingsFrameFlags.NONE);
|
||||
|
||||
frame.PayloadLength = length;
|
||||
var payload = new byte[length];
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
await SendAsync(payload);
|
||||
}
|
||||
|
||||
internal async Task SendSettingsWithInvalidParameterValueAsync(Http2SettingsParameter parameter, uint value)
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareSettings(Http2SettingsFrameFlags.NONE);
|
||||
frame.PayloadLength = 6;
|
||||
var payload = new byte[Http2FrameReader.SettingSize];
|
||||
payload[0] = (byte)((ushort)parameter >> 8);
|
||||
payload[1] = (byte)(ushort)parameter;
|
||||
payload[2] = (byte)(value >> 24);
|
||||
payload[3] = (byte)(value >> 16);
|
||||
payload[4] = (byte)(value >> 8);
|
||||
payload[5] = (byte)value;
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
await SendAsync(payload);
|
||||
}
|
||||
|
||||
public Task SendPushPromiseFrameAsync()
|
||||
{
|
||||
var writableBuffer = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PayloadLength = 0;
|
||||
frame.Type = Http2FrameType.PUSH_PROMISE;
|
||||
frame.StreamId = 1;
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, writableBuffer);
|
||||
return FlushAsync(writableBuffer);
|
||||
}
|
||||
|
||||
internal async Task<bool> SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, IEnumerable<KeyValuePair<string, string>> headers)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareHeaders(flags, streamId);
|
||||
var buffer = _headerEncodingBuffer.AsMemory();
|
||||
var done = _hpackEncoder.BeginEncode(headers, buffer.Span, out var length);
|
||||
frame.PayloadLength = length;
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
await SendAsync(buffer.Span.Slice(0, length));
|
||||
|
||||
return done;
|
||||
}
|
||||
|
||||
internal async Task SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, byte[] headerBlock)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareHeaders(flags, streamId);
|
||||
frame.PayloadLength = headerBlock.Length;
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
await SendAsync(headerBlock);
|
||||
}
|
||||
|
||||
public async Task SendInvalidHeadersFrameAsync(int streamId, int payloadLength, byte padLength)
|
||||
{
|
||||
Assert.True(padLength >= payloadLength, $"{nameof(padLength)} must be greater than or equal to {nameof(payloadLength)} to create an invalid frame.");
|
||||
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareHeaders(Http2HeadersFrameFlags.PADDED, streamId);
|
||||
frame.PayloadLength = payloadLength;
|
||||
var payload = new byte[payloadLength];
|
||||
if (payloadLength > 0)
|
||||
{
|
||||
payload[0] = padLength;
|
||||
}
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
await SendAsync(payload);
|
||||
}
|
||||
|
||||
public async Task SendIncompleteHeadersFrameAsync(int streamId)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareHeaders(Http2HeadersFrameFlags.END_HEADERS, streamId);
|
||||
frame.PayloadLength = 3;
|
||||
var payload = new byte[3];
|
||||
// Set up an incomplete Literal Header Field w/ Incremental Indexing frame,
|
||||
// with an incomplete new name
|
||||
payload[0] = 0;
|
||||
payload[1] = 2;
|
||||
payload[2] = (byte)'a';
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
await SendAsync(payload);
|
||||
}
|
||||
|
||||
internal async Task<bool> SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareContinuation(flags, streamId);
|
||||
var buffer = _headerEncodingBuffer.AsMemory();
|
||||
var done = _hpackEncoder.Encode(buffer.Span, out var length);
|
||||
frame.PayloadLength = length;
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
await SendAsync(buffer.Span.Slice(0, length));
|
||||
|
||||
return done;
|
||||
}
|
||||
|
||||
internal async Task SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags, byte[] payload)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareContinuation(flags, streamId);
|
||||
frame.PayloadLength = payload.Length;
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
await SendAsync(payload);
|
||||
}
|
||||
|
||||
internal async Task<bool> SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags, IEnumerable<KeyValuePair<string, string>> headers)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareContinuation(flags, streamId);
|
||||
var buffer = _headerEncodingBuffer.AsMemory();
|
||||
var done = _hpackEncoder.BeginEncode(headers, buffer.Span, out var length);
|
||||
frame.PayloadLength = length;
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
await SendAsync(buffer.Span.Slice(0, length));
|
||||
|
||||
return done;
|
||||
}
|
||||
|
||||
internal Task SendEmptyContinuationFrameAsync(int streamId, Http2ContinuationFrameFlags flags)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareContinuation(flags, streamId);
|
||||
frame.PayloadLength = 0;
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
return FlushAsync(outputWriter);
|
||||
}
|
||||
|
||||
public async Task SendIncompleteContinuationFrameAsync(int streamId)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareContinuation(Http2ContinuationFrameFlags.END_HEADERS, streamId);
|
||||
frame.PayloadLength = 3;
|
||||
var payload = new byte[3];
|
||||
// Set up an incomplete Literal Header Field w/ Incremental Indexing frame,
|
||||
// with an incomplete new name
|
||||
payload[0] = 0;
|
||||
payload[1] = 2;
|
||||
payload[2] = (byte)'a';
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
await SendAsync(payload);
|
||||
}
|
||||
|
||||
public Task SendDataAsync(int streamId, Memory<byte> data, bool endStream)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareData(streamId);
|
||||
frame.PayloadLength = data.Length;
|
||||
frame.DataFlags = endStream ? Http2DataFrameFlags.END_STREAM : Http2DataFrameFlags.NONE;
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
return SendAsync(data.Span);
|
||||
}
|
||||
|
||||
public async Task SendDataWithPaddingAsync(int streamId, Memory<byte> data, byte padLength, bool endStream)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareData(streamId, padLength);
|
||||
frame.PayloadLength = data.Length + 1 + padLength;
|
||||
|
||||
if (endStream)
|
||||
{
|
||||
frame.DataFlags |= Http2DataFrameFlags.END_STREAM;
|
||||
}
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
outputWriter.GetSpan(1)[0] = padLength;
|
||||
outputWriter.Advance(1);
|
||||
await SendAsync(data.Span);
|
||||
await SendAsync(new byte[padLength]);
|
||||
}
|
||||
|
||||
public Task SendInvalidDataFrameAsync(int streamId, int frameLength, byte padLength)
|
||||
{
|
||||
Assert.True(padLength >= frameLength, $"{nameof(padLength)} must be greater than or equal to {nameof(frameLength)} to create an invalid frame.");
|
||||
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
|
||||
frame.PrepareData(streamId);
|
||||
frame.DataFlags = Http2DataFrameFlags.PADDED;
|
||||
frame.PayloadLength = frameLength;
|
||||
var payload = new byte[frameLength];
|
||||
if (frameLength > 0)
|
||||
{
|
||||
payload[0] = padLength;
|
||||
}
|
||||
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
return SendAsync(payload);
|
||||
}
|
||||
|
||||
internal Task SendPingAsync(Http2PingFrameFlags flags)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var pingFrame = new Http2Frame();
|
||||
pingFrame.PreparePing(flags);
|
||||
Http2FrameWriter.WriteHeader(pingFrame, outputWriter);
|
||||
return SendAsync(new byte[8]); // Empty payload
|
||||
}
|
||||
|
||||
public Task SendPingWithInvalidLengthAsync(int length)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var pingFrame = new Http2Frame();
|
||||
pingFrame.PreparePing(Http2PingFrameFlags.NONE);
|
||||
pingFrame.PayloadLength = length;
|
||||
Http2FrameWriter.WriteHeader(pingFrame, outputWriter);
|
||||
return SendAsync(new byte[length]);
|
||||
}
|
||||
|
||||
public Task SendPingWithInvalidStreamIdAsync(int streamId)
|
||||
{
|
||||
Assert.NotEqual(0, streamId);
|
||||
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var pingFrame = new Http2Frame();
|
||||
pingFrame.PreparePing(Http2PingFrameFlags.NONE);
|
||||
pingFrame.StreamId = streamId;
|
||||
Http2FrameWriter.WriteHeader(pingFrame, outputWriter);
|
||||
return SendAsync(new byte[pingFrame.PayloadLength]);
|
||||
}
|
||||
|
||||
/* https://tools.ietf.org/html/rfc7540#section-6.3
|
||||
+-+-------------------------------------------------------------+
|
||||
|E| Stream Dependency (31) |
|
||||
+-+-------------+-----------------------------------------------+
|
||||
| Weight (8) |
|
||||
+-+-------------+
|
||||
*/
|
||||
public Task SendPriorityAsync(int streamId, int streamDependency = 0)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var priorityFrame = new Http2Frame();
|
||||
priorityFrame.PreparePriority(streamId, streamDependency: streamDependency, exclusive: false, weight: 0);
|
||||
|
||||
var payload = new byte[priorityFrame.PayloadLength].AsSpan();
|
||||
Bitshifter.WriteUInt31BigEndian(payload, (uint)streamDependency);
|
||||
payload[4] = 0; // Weight
|
||||
|
||||
Http2FrameWriter.WriteHeader(priorityFrame, outputWriter);
|
||||
return SendAsync(payload);
|
||||
}
|
||||
|
||||
public Task SendInvalidPriorityFrameAsync(int streamId, int length)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var priorityFrame = new Http2Frame();
|
||||
priorityFrame.PreparePriority(streamId, streamDependency: 0, exclusive: false, weight: 0);
|
||||
priorityFrame.PayloadLength = length;
|
||||
|
||||
Http2FrameWriter.WriteHeader(priorityFrame, outputWriter);
|
||||
return SendAsync(new byte[length]);
|
||||
}
|
||||
|
||||
/* https://tools.ietf.org/html/rfc7540#section-6.4
|
||||
+---------------------------------------------------------------+
|
||||
| Error Code (32) |
|
||||
+---------------------------------------------------------------+
|
||||
*/
|
||||
public Task SendRstStreamAsync(int streamId)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var rstStreamFrame = new Http2Frame();
|
||||
rstStreamFrame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL);
|
||||
var payload = new byte[rstStreamFrame.PayloadLength];
|
||||
BinaryPrimitives.WriteUInt32BigEndian(payload, (uint)Http2ErrorCode.CANCEL);
|
||||
|
||||
Http2FrameWriter.WriteHeader(rstStreamFrame, outputWriter);
|
||||
return SendAsync(payload);
|
||||
}
|
||||
|
||||
public Task SendInvalidRstStreamFrameAsync(int streamId, int length)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareRstStream(streamId, Http2ErrorCode.CANCEL);
|
||||
frame.PayloadLength = length;
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
return SendAsync(new byte[length]);
|
||||
}
|
||||
|
||||
public Task SendGoAwayAsync()
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR);
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
return SendAsync(new byte[frame.PayloadLength]);
|
||||
}
|
||||
|
||||
public Task SendInvalidGoAwayFrameAsync()
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareGoAway(0, Http2ErrorCode.NO_ERROR);
|
||||
frame.StreamId = 1;
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
return SendAsync(new byte[frame.PayloadLength]);
|
||||
}
|
||||
|
||||
public Task SendWindowUpdateAsync(int streamId, int sizeIncrement)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareWindowUpdate(streamId, sizeIncrement);
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
var buffer = outputWriter.GetSpan(4);
|
||||
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)sizeIncrement);
|
||||
outputWriter.Advance(4);
|
||||
return FlushAsync(outputWriter);
|
||||
}
|
||||
|
||||
public Task SendInvalidWindowUpdateAsync(int streamId, int sizeIncrement, int length)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.PrepareWindowUpdate(streamId, sizeIncrement);
|
||||
frame.PayloadLength = length;
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
return SendAsync(new byte[length]);
|
||||
}
|
||||
|
||||
public Task SendUnknownFrameTypeAsync(int streamId, int frameType)
|
||||
{
|
||||
var outputWriter = _pair.Application.Output;
|
||||
var frame = new Http2Frame();
|
||||
frame.StreamId = streamId;
|
||||
frame.Type = (Http2FrameType)frameType;
|
||||
frame.PayloadLength = 0;
|
||||
Http2FrameWriter.WriteHeader(frame, outputWriter);
|
||||
return FlushAsync(outputWriter);
|
||||
}
|
||||
|
||||
internal async Task<Http2FrameWithPayload> ReceiveFrameAsync(uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize)
|
||||
{
|
||||
var frame = new Http2FrameWithPayload();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout();
|
||||
var buffer = result.Buffer;
|
||||
var consumed = buffer.Start;
|
||||
var examined = buffer.Start;
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(buffer.Length > 0);
|
||||
|
||||
if (Http2FrameReader.TryReadFrame(ref buffer, frame, maxFrameSize, out var framePayload))
|
||||
{
|
||||
consumed = examined = framePayload.End;
|
||||
frame.Payload = framePayload.ToArray();
|
||||
return frame;
|
||||
}
|
||||
else
|
||||
{
|
||||
examined = buffer.End;
|
||||
}
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
throw new IOException("The reader completed without returning a frame.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_bytesReceived += buffer.Slice(buffer.Start, consumed).Length;
|
||||
_pair.Application.Input.AdvanceTo(consumed, examined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<Http2FrameWithPayload> ExpectAsync(Http2FrameType type, int withLength, byte withFlags, int withStreamId)
|
||||
{
|
||||
var frame = await ReceiveFrameAsync((uint)withLength);
|
||||
|
||||
Assert.Equal(type, frame.Type);
|
||||
Assert.Equal(withLength, frame.PayloadLength);
|
||||
Assert.Equal(withFlags, frame.Flags);
|
||||
Assert.Equal(withStreamId, frame.StreamId);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public async Task StopConnectionAsync(int expectedLastStreamId, bool ignoreNonGoAwayFrames)
|
||||
{
|
||||
await SendGoAwayAsync();
|
||||
await WaitForConnectionStopAsync(expectedLastStreamId, ignoreNonGoAwayFrames);
|
||||
|
||||
_pair.Application.Output.Complete();
|
||||
}
|
||||
|
||||
public Task WaitForConnectionStopAsync(int expectedLastStreamId, bool ignoreNonGoAwayFrames)
|
||||
{
|
||||
return WaitForConnectionErrorAsync<Exception>(ignoreNonGoAwayFrames, expectedLastStreamId, Http2ErrorCode.NO_ERROR);
|
||||
}
|
||||
|
||||
internal void VerifyGoAway(Http2Frame frame, int expectedLastStreamId, Http2ErrorCode expectedErrorCode)
|
||||
{
|
||||
Assert.Equal(Http2FrameType.GOAWAY, frame.Type);
|
||||
Assert.Equal(8, frame.PayloadLength);
|
||||
Assert.Equal(0, frame.Flags);
|
||||
Assert.Equal(0, frame.StreamId);
|
||||
Assert.Equal(expectedLastStreamId, frame.GoAwayLastStreamId);
|
||||
Assert.Equal(expectedErrorCode, frame.GoAwayErrorCode);
|
||||
}
|
||||
|
||||
internal async Task WaitForConnectionErrorAsync<TException>(bool ignoreNonGoAwayFrames, int expectedLastStreamId, Http2ErrorCode expectedErrorCode)
|
||||
where TException : Exception
|
||||
{
|
||||
await WaitForConnectionErrorAsyncDoNotCloseTransport<TException>(ignoreNonGoAwayFrames, expectedLastStreamId, expectedErrorCode);
|
||||
_pair.Application.Output.Complete();
|
||||
}
|
||||
|
||||
internal async Task WaitForConnectionErrorAsyncDoNotCloseTransport<TException>(bool ignoreNonGoAwayFrames, int expectedLastStreamId, Http2ErrorCode expectedErrorCode)
|
||||
where TException : Exception
|
||||
{
|
||||
var frame = await ReceiveFrameAsync();
|
||||
|
||||
if (ignoreNonGoAwayFrames)
|
||||
{
|
||||
while (frame.Type != Http2FrameType.GOAWAY)
|
||||
{
|
||||
frame = await ReceiveFrameAsync();
|
||||
}
|
||||
}
|
||||
|
||||
VerifyGoAway(frame, expectedLastStreamId, expectedErrorCode);
|
||||
}
|
||||
|
||||
internal async Task WaitForStreamErrorAsync(int expectedStreamId, Http2ErrorCode expectedErrorCode)
|
||||
{
|
||||
var frame = await ReceiveFrameAsync();
|
||||
|
||||
Assert.Equal(Http2FrameType.RST_STREAM, frame.Type);
|
||||
Assert.Equal(4, frame.PayloadLength);
|
||||
Assert.Equal(0, frame.Flags);
|
||||
Assert.Equal(expectedStreamId, frame.StreamId);
|
||||
Assert.Equal(expectedErrorCode, frame.RstStreamErrorCode);
|
||||
}
|
||||
|
||||
internal class Http2FrameWithPayload : Http2Frame
|
||||
{
|
||||
public Http2FrameWithPayload() : base()
|
||||
{
|
||||
}
|
||||
|
||||
// This does not contain extended headers
|
||||
public Memory<byte> Payload { get; set; }
|
||||
|
||||
public ReadOnlySequence<byte> PayloadSequence => new ReadOnlySequence<byte>(Payload);
|
||||
}
|
||||
|
||||
private static class Assert
|
||||
{
|
||||
public static void True(bool condition, string message = "")
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
throw new Exception($"Assert.True failed: '{message}'");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Equal<T>(T expected, T actual)
|
||||
{
|
||||
if (!expected.Equals(actual))
|
||||
{
|
||||
throw new Exception($"Assert.Equal('{expected}', '{actual}') failed");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Equal(string expected, string actual, bool ignoreCase = false)
|
||||
{
|
||||
if (!expected.Equals(actual, ignoreCase ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture))
|
||||
{
|
||||
throw new Exception($"Assert.Equal('{expected}', '{actual}') failed");
|
||||
}
|
||||
}
|
||||
|
||||
public static void NotEqual<T>(T value1, T value2)
|
||||
{
|
||||
if (value1.Equals(value2))
|
||||
{
|
||||
throw new Exception($"Assert.NotEqual('{value1}', '{value2}') failed");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Contains<T>(IEnumerable<T> collection, T value)
|
||||
{
|
||||
if (!collection.Contains(value))
|
||||
{
|
||||
throw new Exception($"Assert.Contains(collection, '{value}') failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Connections.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Client;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace http2cat
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
var host = new HostBuilder()
|
||||
.ConfigureLogging(loggingBuilder =>
|
||||
{
|
||||
loggingBuilder.AddConsole();
|
||||
})
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<IConnectionFactory, SocketConnectionFactory>();
|
||||
services.AddSingleton<Http2CatHostedService>();
|
||||
})
|
||||
.Build();
|
||||
|
||||
await host.Services.GetService<Http2CatHostedService>().RunAsync();
|
||||
}
|
||||
|
||||
private class Http2CatHostedService
|
||||
{
|
||||
private readonly IConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<Http2CatHostedService> _logger;
|
||||
|
||||
public Http2CatHostedService(IConnectionFactory connectionFactory, ILogger<Http2CatHostedService> logger)
|
||||
{
|
||||
_connectionFactory = connectionFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
var endpoint = new IPEndPoint(IPAddress.Loopback, 5001);
|
||||
|
||||
_logger.LogInformation($"Connecting to '{endpoint}'.");
|
||||
|
||||
await using var context = await _connectionFactory.ConnectAsync(endpoint);
|
||||
|
||||
_logger.LogInformation($"Connected to '{endpoint}'. Starting TLS handshake.");
|
||||
|
||||
var memoryPool = context.Features.Get<IMemoryPoolFeature>()?.MemoryPool;
|
||||
var inputPipeOptions = new StreamPipeReaderOptions(memoryPool, memoryPool.GetMinimumSegmentSize(), memoryPool.GetMinimumAllocSize(), leaveOpen: true);
|
||||
var outputPipeOptions = new StreamPipeWriterOptions(pool: memoryPool, leaveOpen: true);
|
||||
|
||||
await using var sslDuplexPipe = new SslDuplexPipe(context.Transport, inputPipeOptions, outputPipeOptions);
|
||||
await using var sslStream = sslDuplexPipe.Stream;
|
||||
|
||||
var originalTransport = context.Transport;
|
||||
context.Transport = sslDuplexPipe;
|
||||
|
||||
try
|
||||
{
|
||||
await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
|
||||
{
|
||||
TargetHost = "localhost",
|
||||
RemoteCertificateValidationCallback = (_, __, ___, ____) => true,
|
||||
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http2 },
|
||||
EnabledSslProtocols = SslProtocols.Tls12,
|
||||
}, CancellationToken.None);
|
||||
|
||||
_logger.LogInformation($"TLS handshake completed successfully.");
|
||||
|
||||
var http2Utilities = new Http2Utilities(context);
|
||||
|
||||
await http2Utilities.InitializeConnectionAsync();
|
||||
|
||||
_logger.LogInformation("Initialized http2 connection. Starting stream 1.");
|
||||
|
||||
await http2Utilities.StartStreamAsync(1, Http2Utilities._browserRequestHeaders, endStream: true);
|
||||
|
||||
var headersFrame = await http2Utilities.ReceiveFrameAsync();
|
||||
|
||||
Trace.Assert(headersFrame.Type == Http2FrameType.HEADERS);
|
||||
Trace.Assert((headersFrame.Flags & (byte)Http2HeadersFrameFlags.END_HEADERS) != 0);
|
||||
Trace.Assert((headersFrame.Flags & (byte)Http2HeadersFrameFlags.END_STREAM) == 0);
|
||||
|
||||
_logger.LogInformation("Received headers in a single frame.");
|
||||
|
||||
var decodedHeaders = http2Utilities.DecodeHeaders(headersFrame);
|
||||
|
||||
foreach (var header in decodedHeaders)
|
||||
{
|
||||
_logger.LogInformation($"{header.Key}: {header.Value}");
|
||||
}
|
||||
|
||||
var dataFrame = await http2Utilities.ReceiveFrameAsync();
|
||||
|
||||
Trace.Assert(dataFrame.Type == Http2FrameType.DATA);
|
||||
Trace.Assert((dataFrame.Flags & (byte)Http2DataFrameFlags.END_STREAM) == 0);
|
||||
|
||||
_logger.LogInformation("Received data in a single frame.");
|
||||
|
||||
_logger.LogInformation(Encoding.UTF8.GetString(dataFrame.Payload.ToArray()));
|
||||
|
||||
var trailersFrame = await http2Utilities.ReceiveFrameAsync();
|
||||
|
||||
Trace.Assert(trailersFrame.Type == Http2FrameType.HEADERS);
|
||||
Trace.Assert((trailersFrame.Flags & (byte)Http2DataFrameFlags.END_STREAM) == 1);
|
||||
|
||||
_logger.LogInformation("Received trailers in a single frame.");
|
||||
|
||||
http2Utilities._decodedHeaders.Clear();
|
||||
|
||||
var decodedTrailers = http2Utilities.DecodeHeaders(trailersFrame);
|
||||
|
||||
foreach (var header in decodedHeaders)
|
||||
{
|
||||
_logger.LogInformation($"{header.Key}: {header.Value}");
|
||||
}
|
||||
|
||||
await http2Utilities.StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
|
||||
|
||||
_logger.LogInformation("Connection stopped.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Transport = originalTransport;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SslDuplexPipe : DuplexPipeStreamAdapter<SslStream>
|
||||
{
|
||||
public SslDuplexPipe(IDuplexPipe transport, StreamPipeReaderOptions readerOptions, StreamPipeWriterOptions writerOptions)
|
||||
: this(transport, readerOptions, writerOptions, s => new SslStream(s))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public SslDuplexPipe(IDuplexPipe transport, StreamPipeReaderOptions readerOptions, StreamPipeWriterOptions writerOptions, Func<Stream, SslStream> factory) :
|
||||
base(transport, readerOptions, writerOptions, factory)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace System.Threading.Tasks
|
||||
{
|
||||
public static class TaskTimeoutExtensions
|
||||
{
|
||||
public static TimeSpan DefaultTimeoutTimeSpan { get; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
public static Task<T> DefaultTimeout<T>(this ValueTask<T> task)
|
||||
{
|
||||
return task.AsTask().TimeoutAfter(DefaultTimeoutTimeSpan);
|
||||
}
|
||||
|
||||
public static Task DefaultTimeout(this ValueTask task)
|
||||
{
|
||||
return task.AsTask().TimeoutAfter(DefaultTimeoutTimeSpan);
|
||||
}
|
||||
|
||||
public static Task<T> DefaultTimeout<T>(this Task<T> task)
|
||||
{
|
||||
return task.TimeoutAfter(DefaultTimeoutTimeSpan);
|
||||
}
|
||||
|
||||
public static Task DefaultTimeout(this Task task)
|
||||
{
|
||||
return task.TimeoutAfter(DefaultTimeoutTimeSpan);
|
||||
}
|
||||
|
||||
public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout,
|
||||
[CallerFilePath] string filePath = null,
|
||||
[CallerLineNumber] int lineNumber = default)
|
||||
{
|
||||
// Don't create a timer if the task is already completed
|
||||
// or the debugger is attached
|
||||
if (task.IsCompleted || Debugger.IsAttached)
|
||||
{
|
||||
return await task;
|
||||
}
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)))
|
||||
{
|
||||
cts.Cancel();
|
||||
return await task;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber));
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task TimeoutAfter(this Task task, TimeSpan timeout,
|
||||
[CallerFilePath] string filePath = null,
|
||||
[CallerLineNumber] int lineNumber = default)
|
||||
{
|
||||
// Don't create a timer if the task is already completed
|
||||
// or the debugger is attached
|
||||
if (task.IsCompleted || Debugger.IsAttached)
|
||||
{
|
||||
await task;
|
||||
return;
|
||||
}
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)))
|
||||
{
|
||||
cts.Cancel();
|
||||
await task;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TimeoutException(CreateMessage(timeout, filePath, lineNumber));
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateMessage(TimeSpan timeout, string filePath, int lineNumber)
|
||||
=> string.IsNullOrEmpty(filePath)
|
||||
? $"The operation timed out after reaching the limit of {timeout.TotalMilliseconds}ms."
|
||||
: $"The operation at {filePath}:{lineNumber} timed out after reaching the limit of {timeout.TotalMilliseconds}ms.";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Core" />
|
||||
<Reference Include="Microsoft.Extensions.Hosting" />
|
||||
<Reference Include="Microsoft.Extensions.Logging.Console" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Reference in New Issue