From ff8363a638401c5602910553356bd7f98b8d2f98 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Tue, 8 Oct 2019 18:14:24 -0700 Subject: [PATCH] Add SocketConnectionFactory and http2cat (#14582) --- .../Core/src/Properties/AssemblyInfo.cs | 1 + src/Servers/Kestrel/Kestrel.sln | 15 + .../src/Client/SocketConnectionFactory.cs | 76 ++ ...re.Server.Kestrel.Transport.Sockets.csproj | 4 + .../samples/http2cat/Http2Utilities.cs | 920 ++++++++++++++++++ .../Kestrel/samples/http2cat/Program.cs | 161 +++ .../samples/http2cat/TaskTimeoutExtensions.cs | 85 ++ .../Kestrel/samples/http2cat/http2cat.csproj | 15 + 8 files changed, 1277 insertions(+) create mode 100644 src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs create mode 100644 src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs create mode 100644 src/Servers/Kestrel/samples/http2cat/Program.cs create mode 100644 src/Servers/Kestrel/samples/http2cat/TaskTimeoutExtensions.cs create mode 100644 src/Servers/Kestrel/samples/http2cat/http2cat.csproj diff --git a/src/Servers/Kestrel/Core/src/Properties/AssemblyInfo.cs b/src/Servers/Kestrel/Core/src/Properties/AssemblyInfo.cs index 706d5176fb..fa7073fd5f 100644 --- a/src/Servers/Kestrel/Core/src/Properties/AssemblyInfo.cs +++ b/src/Servers/Kestrel/Core/src/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Servers/Kestrel/Kestrel.sln b/src/Servers/Kestrel/Kestrel.sln index 5f7b931878..8ff2ba3287 100644 --- a/src/Servers/Kestrel/Kestrel.sln +++ b/src/Servers/Kestrel/Kestrel.sln @@ -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} diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs new file mode 100644 index 0000000000..b52b697260 --- /dev/null +++ b/src/Servers/Kestrel/Transport.Sockets/src/Client/SocketConnectionFactory.cs @@ -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 _memoryPool; + private readonly SocketsTrace _trace; + + public SocketConnectionFactory(IOptions 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 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; + } + } +} diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj b/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj index 037dcfb6fc..b9b5bdc589 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj +++ b/src/Servers/Kestrel/Transport.Sockets/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj @@ -26,6 +26,10 @@ + + + + diff --git a/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs b/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs new file mode 100644 index 0000000000..6ca7d17f3c --- /dev/null +++ b/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs @@ -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> _browserRequestHeaders = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + new KeyValuePair("user-agent", "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"), + new KeyValuePair("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"), + new KeyValuePair("accept-language", "en-US,en;q=0.5"), + new KeyValuePair("accept-encoding", "gzip, deflate, br"), + new KeyValuePair("upgrade-insecure-requests", "1"), + }; + + public static readonly IEnumerable> _postRequestHeaders = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + public static readonly IEnumerable> _expectContinueRequestHeaders = new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Authority, "127.0.0.1"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair("expect", "100-continue"), + }; + + public static readonly IEnumerable> _requestTrailers = new[] + { + new KeyValuePair("trailer-one", "1"), + new KeyValuePair("trailer-two", "2"), + }; + + public static readonly IEnumerable> _oneContinuationRequestHeaders = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + new KeyValuePair("a", _4kHeaderValue), + new KeyValuePair("b", _4kHeaderValue), + new KeyValuePair("c", _4kHeaderValue), + new KeyValuePair("d", _4kHeaderValue) + }; + + public static readonly IEnumerable> _twoContinuationsRequestHeaders = new[] + { + new KeyValuePair(HeaderNames.Method, "GET"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + new KeyValuePair("a", _4kHeaderValue), + new KeyValuePair("b", _4kHeaderValue), + new KeyValuePair("c", _4kHeaderValue), + new KeyValuePair("d", _4kHeaderValue), + new KeyValuePair("e", _4kHeaderValue), + new KeyValuePair("f", _4kHeaderValue), + new KeyValuePair("g", _4kHeaderValue), + }; + + public static IEnumerable> ReadRateRequestHeaders(int expectedBytes) => new[] + { + new KeyValuePair(HeaderNames.Method, "POST"), + new KeyValuePair(HeaderNames.Path, "/" + expectedBytes), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(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 _decodedHeaders = new Dictionary(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 name, Span 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> headers, bool endStream) + { + var writableBuffer = _pair.Application.Output; + var tcs = new TaskCompletionSource(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 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> headers, byte padLength, bool endStream) + { + var writableBuffer = _pair.Application.Output; + var tcs = new TaskCompletionSource(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> headers, byte priority, int streamDependency, bool endStream) + { + var writableBuffer = _pair.Application.Output; + var tcs = new TaskCompletionSource(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> headers, byte padLength, byte priority, int streamDependency, bool endStream) + { + var writableBuffer = _pair.Application.Output; + var tcs = new TaskCompletionSource(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 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(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 SendHeadersAsync(int streamId, Http2HeadersFrameFlags flags, IEnumerable> 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 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 SendContinuationAsync(int streamId, Http2ContinuationFrameFlags flags, IEnumerable> 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 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 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 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 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(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(bool ignoreNonGoAwayFrames, int expectedLastStreamId, Http2ErrorCode expectedErrorCode) + where TException : Exception + { + await WaitForConnectionErrorAsyncDoNotCloseTransport(ignoreNonGoAwayFrames, expectedLastStreamId, expectedErrorCode); + _pair.Application.Output.Complete(); + } + + internal async Task WaitForConnectionErrorAsyncDoNotCloseTransport(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 Payload { get; set; } + + public ReadOnlySequence PayloadSequence => new ReadOnlySequence(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 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 value1, T value2) + { + if (value1.Equals(value2)) + { + throw new Exception($"Assert.NotEqual('{value1}', '{value2}') failed"); + } + } + + public static void Contains(IEnumerable collection, T value) + { + if (!collection.Contains(value)) + { + throw new Exception($"Assert.Contains(collection, '{value}') failed"); + } + } + } + } +} diff --git a/src/Servers/Kestrel/samples/http2cat/Program.cs b/src/Servers/Kestrel/samples/http2cat/Program.cs new file mode 100644 index 0000000000..7b9f5d7033 --- /dev/null +++ b/src/Servers/Kestrel/samples/http2cat/Program.cs @@ -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(); + services.AddSingleton(); + }) + .Build(); + + await host.Services.GetService().RunAsync(); + } + + private class Http2CatHostedService + { + private readonly IConnectionFactory _connectionFactory; + private readonly ILogger _logger; + + public Http2CatHostedService(IConnectionFactory connectionFactory, ILogger 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()?.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.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 + { + 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 factory) : + base(transport, readerOptions, writerOptions, factory) + { + } + } + } +} diff --git a/src/Servers/Kestrel/samples/http2cat/TaskTimeoutExtensions.cs b/src/Servers/Kestrel/samples/http2cat/TaskTimeoutExtensions.cs new file mode 100644 index 0000000000..943673291e --- /dev/null +++ b/src/Servers/Kestrel/samples/http2cat/TaskTimeoutExtensions.cs @@ -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 DefaultTimeout(this ValueTask task) + { + return task.AsTask().TimeoutAfter(DefaultTimeoutTimeSpan); + } + + public static Task DefaultTimeout(this ValueTask task) + { + return task.AsTask().TimeoutAfter(DefaultTimeoutTimeSpan); + } + + public static Task DefaultTimeout(this Task task) + { + return task.TimeoutAfter(DefaultTimeoutTimeSpan); + } + + public static Task DefaultTimeout(this Task task) + { + return task.TimeoutAfter(DefaultTimeoutTimeSpan); + } + + 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) + { + 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."; + } +} diff --git a/src/Servers/Kestrel/samples/http2cat/http2cat.csproj b/src/Servers/Kestrel/samples/http2cat/http2cat.csproj new file mode 100644 index 0000000000..50e4f6ea2b --- /dev/null +++ b/src/Servers/Kestrel/samples/http2cat/http2cat.csproj @@ -0,0 +1,15 @@ + + + + Exe + netcoreapp5.0 + + + + + + + + + +