From 0f580f1082b27fd3a802ad1f14c3d712649d7d69 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Fri, 15 Nov 2019 14:50:54 -0800 Subject: [PATCH] Initial HTTP/3 Implementation in Kestrel (#16914) --- .../Microsoft.Net.Http.Headers.netcoreapp.cs | 1 + src/Http/Headers/src/HeaderNames.cs | 1 + ...ore.Connections.Abstractions.netcoreapp.cs | 11 +- ...Connections.Abstractions.netstandard2.0.cs | 11 +- ...Connections.Abstractions.netstandard2.1.cs | 11 +- ...StreamFeature.cs => IQuicStreamFeature.cs} | 4 +- .../IMultiplexedConnectionListenerFactory.cs | 9 + ...pNetCore.Server.Kestrel.Core.netcoreapp.cs | 13 +- src/Servers/Kestrel/Core/src/CoreStrings.resx | 66 +- src/Servers/Kestrel/Core/src/Http3Limits.cs | 53 ++ src/Servers/Kestrel/Core/src/HttpProtocols.cs | 2 + .../Internal/Http/HttpHeaders.Generated.cs | 789 ++++++++++-------- .../Core/src/Internal/Http/HttpProtocol.cs | 16 +- .../Core/src/Internal/Http/HttpVersion.cs | 5 +- .../Internal/Http3/Frames/Http3ErrorCode.cs | 101 +++ .../Internal/Http3/Frames/Http3Frame.Data.cs | 14 + .../Http3/Frames/Http3Frame.GoAway.cs | 14 + .../Http3/Frames/Http3Frame.Headers.cs | 14 + .../Http3/Frames/Http3Frame.Settings.cs | 14 + .../src/Internal/Http3/Frames/Http3Frame.cs | 17 + .../Internal/Http3/Frames/Http3FrameType.cs | 17 + .../Helpers/VariableLengthIntegerHelper.cs | 115 +++ .../src/Internal/Http3/Http3Connection.cs | 206 +++++ .../Http3/Http3ConnectionException.cs | 28 + .../src/Internal/Http3/Http3ControlStream.cs | 388 +++++++++ .../Internal/Http3/Http3ControlStreamOfT.cs | 26 + .../src/Internal/Http3/Http3FrameReader.cs | 59 ++ .../src/Internal/Http3/Http3FrameWriter.cs | 326 ++++++++ .../src/Internal/Http3/Http3MessageBody.cs | 126 +++ .../src/Internal/Http3/Http3OutputProducer.cs | 406 +++++++++ .../src/Internal/Http3/Http3PeerSettings.cs | 12 + .../src/Internal/Http3/Http3SettingType.cs | 15 + .../Core/src/Internal/Http3/Http3Stream.cs | 471 +++++++++++ .../Http3/Http3StreamErrorException.cs | 18 + .../Core/src/Internal/Http3/Http3StreamOfT.cs | 26 + .../Http3/QPack/DecoderStreamReader.cs | 130 +++ .../src/Internal/Http3/QPack/DynamicTable.cs | 46 + .../Http3/QPack/EncoderStreamReader.cs | 332 ++++++++ .../src/Internal/Http3/QPack/HeaderField.cs | 27 + .../src/Internal/Http3/QPack/QPackDecoder.cs | 513 ++++++++++++ .../Http3/QPack/QPackDecodingException.cs | 28 + .../src/Internal/Http3/QPack/QPackEncoder.cs | 504 +++++++++++ .../Http3/QPack/QPackEncodingException.cs | 19 + .../src/Internal/Http3/QPack/StaticTable.cs | 162 ++++ .../Core/src/Internal/HttpConnection.cs | 16 +- .../Internal/Infrastructure/HttpUtilities.cs | 1 + src/Servers/Kestrel/Core/src/KestrelServer.cs | 39 +- .../Kestrel/Core/src/KestrelServerLimits.cs | 7 +- .../Kestrel/Core/src/KestrelServerOptions.cs | 5 + .../Middleware/HttpsConnectionMiddleware.cs | 7 + .../Kestrel/Core/test/KestrelServerTests.cs | 17 +- .../Core/test/VariableIntHelperTests.cs | 46 + src/Servers/Kestrel/Kestrel.sln | 17 +- ...ver.Kestrel.Transport.MsQuic.netcoreapp.cs | 2 +- .../src/Internal/MsQuicApi.cs | 14 + .../src/Internal/MsQuicConnection.cs | 2 + .../src/Internal/MsQuicConnectionListener.cs | 2 + .../src/Internal/MsQuicNativeMethods.cs | 65 +- .../src/Internal/MsQuicStream.cs | 62 +- .../src/MsQuicTransportFactory.cs | 2 +- .../Http3SampleApp/Http3SampleApp.csproj | 14 + .../Kestrel/samples/Http3SampleApp/Program.cs | 52 ++ .../Kestrel/samples/Http3SampleApp/Startup.cs | 30 + .../appsettings.Development.json | 9 + .../samples/Http3SampleApp/appsettings.json | 10 + .../Kestrel/samples/QuicSampleApp/Program.cs | 2 + .../appsettings.Development.json | 9 + .../samples/QuicSampleApp/appsettings.json | 10 + .../samples/QuicSampleClient/Program.cs | 2 + src/Servers/Kestrel/shared/KnownHeaders.cs | 1 + .../test/TransportTestHelpers/TestServer.cs | 3 +- .../Http3/Http3StreamTests.cs | 34 + .../Http3/Http3TestBase.cs | 386 +++++++++ .../InMemory.FunctionalTests.csproj | 2 +- .../TestTransport/TestServer.cs | 4 +- 75 files changed, 5595 insertions(+), 443 deletions(-) rename src/Servers/Connections.Abstractions/src/Features/{IUnidirectionalStreamFeature.cs => IQuicStreamFeature.cs} (68%) create mode 100644 src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs create mode 100644 src/Servers/Kestrel/Core/src/Http3Limits.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3ErrorCode.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Data.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.GoAway.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Headers.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Settings.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3FrameType.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Helpers/VariableLengthIntegerHelper.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionException.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3MessageBody.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DecoderStreamReader.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DynamicTable.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/EncoderStreamReader.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/HeaderField.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecoder.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecodingException.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncoder.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncodingException.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/StaticTable.cs create mode 100644 src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs create mode 100644 src/Servers/Kestrel/samples/Http3SampleApp/Http3SampleApp.csproj create mode 100644 src/Servers/Kestrel/samples/Http3SampleApp/Program.cs create mode 100644 src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs create mode 100644 src/Servers/Kestrel/samples/Http3SampleApp/appsettings.Development.json create mode 100644 src/Servers/Kestrel/samples/Http3SampleApp/appsettings.json create mode 100644 src/Servers/Kestrel/samples/QuicSampleApp/appsettings.Development.json create mode 100644 src/Servers/Kestrel/samples/QuicSampleApp/appsettings.json create mode 100644 src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs create mode 100644 src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs diff --git a/src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs b/src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs index 2e059efe1a..41d44ad26d 100644 --- a/src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs +++ b/src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs @@ -133,6 +133,7 @@ namespace Microsoft.Net.Http.Headers public static readonly string AccessControlRequestMethod; public static readonly string Age; public static readonly string Allow; + public static readonly string AltSvc; public static readonly string Authority; public static readonly string Authorization; public static readonly string CacheControl; diff --git a/src/Http/Headers/src/HeaderNames.cs b/src/Http/Headers/src/HeaderNames.cs index 602916254a..368cd8be46 100644 --- a/src/Http/Headers/src/HeaderNames.cs +++ b/src/Http/Headers/src/HeaderNames.cs @@ -21,6 +21,7 @@ namespace Microsoft.Net.Http.Headers public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method"; public static readonly string Age = "Age"; public static readonly string Allow = "Allow"; + public static readonly string AltSvc = "Alt-Svc"; public static readonly string Authority = ":authority"; public static readonly string Authorization = "Authorization"; public static readonly string CacheControl = "Cache-Control"; diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs index 0e39f6a158..1cf1fe4d3a 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netcoreapp.cs @@ -123,6 +123,9 @@ namespace Microsoft.AspNetCore.Connections { System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); } + public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory + { + } [System.FlagsAttribute] public enum TransferFormat { @@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features { System.Buffers.MemoryPool MemoryPool { get; } } + public partial interface IQuicStreamFeature + { + bool IsUnidirectional { get; } + long StreamId { get; } + } public partial interface IQuicStreamListenerFeature { System.Threading.Tasks.ValueTask AcceptAsync(); @@ -212,7 +220,4 @@ namespace Microsoft.AspNetCore.Connections.Features Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; } Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; } } - public partial interface IUnidirectionalStreamFeature - { - } } diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs index 0e39f6a158..1cf1fe4d3a 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.0.cs @@ -123,6 +123,9 @@ namespace Microsoft.AspNetCore.Connections { System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); } + public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory + { + } [System.FlagsAttribute] public enum TransferFormat { @@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features { System.Buffers.MemoryPool MemoryPool { get; } } + public partial interface IQuicStreamFeature + { + bool IsUnidirectional { get; } + long StreamId { get; } + } public partial interface IQuicStreamListenerFeature { System.Threading.Tasks.ValueTask AcceptAsync(); @@ -212,7 +220,4 @@ namespace Microsoft.AspNetCore.Connections.Features Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; } Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; } } - public partial interface IUnidirectionalStreamFeature - { - } } diff --git a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs index 0e39f6a158..1cf1fe4d3a 100644 --- a/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs +++ b/src/Servers/Connections.Abstractions/ref/Microsoft.AspNetCore.Connections.Abstractions.netstandard2.1.cs @@ -123,6 +123,9 @@ namespace Microsoft.AspNetCore.Connections { System.Threading.Tasks.ValueTask BindAsync(System.Net.EndPoint endpoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); } + public partial interface IMultiplexedConnectionListenerFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory + { + } [System.FlagsAttribute] public enum TransferFormat { @@ -193,6 +196,11 @@ namespace Microsoft.AspNetCore.Connections.Features { System.Buffers.MemoryPool MemoryPool { get; } } + public partial interface IQuicStreamFeature + { + bool IsUnidirectional { get; } + long StreamId { get; } + } public partial interface IQuicStreamListenerFeature { System.Threading.Tasks.ValueTask AcceptAsync(); @@ -212,7 +220,4 @@ namespace Microsoft.AspNetCore.Connections.Features Microsoft.AspNetCore.Connections.TransferFormat ActiveFormat { get; set; } Microsoft.AspNetCore.Connections.TransferFormat SupportedFormats { get; } } - public partial interface IUnidirectionalStreamFeature - { - } } diff --git a/src/Servers/Connections.Abstractions/src/Features/IUnidirectionalStreamFeature.cs b/src/Servers/Connections.Abstractions/src/Features/IQuicStreamFeature.cs similarity index 68% rename from src/Servers/Connections.Abstractions/src/Features/IUnidirectionalStreamFeature.cs rename to src/Servers/Connections.Abstractions/src/Features/IQuicStreamFeature.cs index 1c6550d008..98c71b7a08 100644 --- a/src/Servers/Connections.Abstractions/src/Features/IUnidirectionalStreamFeature.cs +++ b/src/Servers/Connections.Abstractions/src/Features/IQuicStreamFeature.cs @@ -3,7 +3,9 @@ namespace Microsoft.AspNetCore.Connections.Features { - public interface IUnidirectionalStreamFeature + public interface IQuicStreamFeature { + bool IsUnidirectional { get; } + long StreamId { get; } } } diff --git a/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs new file mode 100644 index 0000000000..65727b7ad8 --- /dev/null +++ b/src/Servers/Connections.Abstractions/src/IMultiplexedConnectionListenerFactory.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.AspNetCore.Connections +{ + public interface IMultiplexedConnectionListenerFactory : IConnectionListenerFactory + { + } +} diff --git a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs index 7e77770b7a..6600b52b48 100644 --- a/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs +++ b/src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs @@ -77,6 +77,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core public int MaxRequestHeaderFieldSize { get { throw null; } set { } } public int MaxStreamsPerConnection { get { throw null; } set { } } } + public partial class Http3Limits + { + public Http3Limits() { } + public int HeaderTableSize { get { throw null; } set { } } + public int MaxRequestHeaderFieldSize { get { throw null; } set { } } + } [System.FlagsAttribute] public enum HttpProtocols { @@ -84,10 +90,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core Http1 = 1, Http2 = 2, Http1AndHttp2 = 3, + Http3 = 4, + Http1AndHttp2AndHttp3 = 7, } public partial class KestrelServer : Microsoft.AspNetCore.Hosting.Server.IServer, System.IDisposable { - public KestrelServer(Microsoft.Extensions.Options.IOptions options, Microsoft.AspNetCore.Connections.IConnectionListenerFactory transportFactory, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } + public KestrelServer(Microsoft.Extensions.Options.IOptions options, System.Collections.Generic.IEnumerable transportFactories, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { } public Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions Options { get { throw null; } } public void Dispose() { } @@ -100,6 +108,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core { public KestrelServerLimits() { } public Microsoft.AspNetCore.Server.Kestrel.Core.Http2Limits Http2 { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } + public Microsoft.AspNetCore.Server.Kestrel.Core.Http3Limits Http3 { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public System.TimeSpan KeepAliveTimeout { get { throw null; } set { } } public long? MaxConcurrentConnections { get { throw null; } set { } } public long? MaxConcurrentUpgradedConnections { get { throw null; } set { } } @@ -121,6 +130,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public bool EnableAltSvc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; } @@ -216,6 +226,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http Http10 = 0, Http11 = 1, Http2 = 2, + Http3 = 3, } public partial interface IHttpRequestLineHandler { diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index a5b2b5ff5a..3e88281f1a 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -1,17 +1,17 @@ - @@ -562,4 +562,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l The ASP.NET Core developer certificate is in an invalid state. To fix this issue, run the following commands 'dotnet dev-certs https --clean' and 'dotnet dev-certs https' to remove all existing ASP.NET Core development certificates and create a new untrusted developer certificate. On macOS or Windows, use 'dotnet dev-certs https --trust' to trust the new certificate. + + Index {index} is outside the bounds of the header field table. + + + The decoded integer exceeds the maximum value of Int32.MaxValue. + + + Huffman decoding error. + + + Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets. + diff --git a/src/Servers/Kestrel/Core/src/Http3Limits.cs b/src/Servers/Kestrel/Core/src/Http3Limits.cs new file mode 100644 index 0000000000..fa82ede8c3 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Http3Limits.cs @@ -0,0 +1,53 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core +{ + public class Http3Limits + { + private int _headerTableSize = 4096; + private int _maxRequestHeaderFieldSize = 8192; + + /// + /// Limits the size of the header compression table, in octets, the HPACK decoder on the server can use. + /// + /// Value must be greater than 0, defaults to 4096 + /// + /// + public int HeaderTableSize + { + get => _headerTableSize; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired); + } + + _headerTableSize = value; + } + } + + /// + /// Indicates the size of the maximum allowed size of a request header field sequence. This limit applies to both name and value sequences in their compressed and uncompressed representations. + /// + /// Value must be greater than 0, defaults to 8192 + /// + /// + public int MaxRequestHeaderFieldSize + { + get => _maxRequestHeaderFieldSize; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired); + } + + _maxRequestHeaderFieldSize = value; + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/HttpProtocols.cs b/src/Servers/Kestrel/Core/src/HttpProtocols.cs index 09524bf156..294ae6ae69 100644 --- a/src/Servers/Kestrel/Core/src/HttpProtocols.cs +++ b/src/Servers/Kestrel/Core/src/HttpProtocols.cs @@ -12,5 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core Http1 = 0x1, Http2 = 0x2, Http1AndHttp2 = Http1 | Http2, + Http3 = 0x4, + Http1AndHttp2AndHttp3 = Http1 | Http2 | Http3 } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs index b21c4914a8..402809d818 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs @@ -6659,14 +6659,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { private static ReadOnlySpan HeaderBytes => new byte[] { - 13,10,67,97,99,104,101,45,67,111,110,116,114,111,108,58,32,13,10,67,111,110,110,101,99,116,105,111,110,58,32,13,10,68,97,116,101,58,32,13,10,75,101,101,112,45,65,108,105,118,101,58,32,13,10,80,114,97,103,109,97,58,32,13,10,84,114,97,105,108,101,114,58,32,13,10,84,114,97,110,115,102,101,114,45,69,110,99,111,100,105,110,103,58,32,13,10,85,112,103,114,97,100,101,58,32,13,10,86,105,97,58,32,13,10,87,97,114,110,105,110,103,58,32,13,10,65,108,108,111,119,58,32,13,10,67,111,110,116,101,110,116,45,84,121,112,101,58,32,13,10,67,111,110,116,101,110,116,45,69,110,99,111,100,105,110,103,58,32,13,10,67,111,110,116,101,110,116,45,76,97,110,103,117,97,103,101,58,32,13,10,67,111,110,116,101,110,116,45,76,111,99,97,116,105,111,110,58,32,13,10,67,111,110,116,101,110,116,45,77,68,53,58,32,13,10,67,111,110,116,101,110,116,45,82,97,110,103,101,58,32,13,10,69,120,112,105,114,101,115,58,32,13,10,76,97,115,116,45,77,111,100,105,102,105,101,100,58,32,13,10,65,99,99,101,112,116,45,82,97,110,103,101,115,58,32,13,10,65,103,101,58,32,13,10,69,84,97,103,58,32,13,10,76,111,99,97,116,105,111,110,58,32,13,10,80,114,111,120,121,45,65,117,116,104,101,110,116,105,99,97,116,101,58,32,13,10,82,101,116,114,121,45,65,102,116,101,114,58,32,13,10,83,101,114,118,101,114,58,32,13,10,83,101,116,45,67,111,111,107,105,101,58,32,13,10,86,97,114,121,58,32,13,10,87,87,87,45,65,117,116,104,101,110,116,105,99,97,116,101,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,65,108,108,111,119,45,67,114,101,100,101,110,116,105,97,108,115,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,65,108,108,111,119,45,72,101,97,100,101,114,115,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,65,108,108,111,119,45,77,101,116,104,111,100,115,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,65,108,108,111,119,45,79,114,105,103,105,110,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,69,120,112,111,115,101,45,72,101,97,100,101,114,115,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,77,97,120,45,65,103,101,58,32,13,10,67,111,110,116,101,110,116,45,76,101,110,103,116,104,58,32, + 13,10,67,97,99,104,101,45,67,111,110,116,114,111,108,58,32,13,10,67,111,110,110,101,99,116,105,111,110,58,32,13,10,68,97,116,101,58,32,13,10,75,101,101,112,45,65,108,105,118,101,58,32,13,10,80,114,97,103,109,97,58,32,13,10,84,114,97,105,108,101,114,58,32,13,10,84,114,97,110,115,102,101,114,45,69,110,99,111,100,105,110,103,58,32,13,10,85,112,103,114,97,100,101,58,32,13,10,86,105,97,58,32,13,10,87,97,114,110,105,110,103,58,32,13,10,65,108,108,111,119,58,32,13,10,67,111,110,116,101,110,116,45,84,121,112,101,58,32,13,10,67,111,110,116,101,110,116,45,69,110,99,111,100,105,110,103,58,32,13,10,67,111,110,116,101,110,116,45,76,97,110,103,117,97,103,101,58,32,13,10,67,111,110,116,101,110,116,45,76,111,99,97,116,105,111,110,58,32,13,10,67,111,110,116,101,110,116,45,77,68,53,58,32,13,10,67,111,110,116,101,110,116,45,82,97,110,103,101,58,32,13,10,69,120,112,105,114,101,115,58,32,13,10,76,97,115,116,45,77,111,100,105,102,105,101,100,58,32,13,10,65,99,99,101,112,116,45,82,97,110,103,101,115,58,32,13,10,65,103,101,58,32,13,10,65,108,116,45,83,118,99,58,32,13,10,69,84,97,103,58,32,13,10,76,111,99,97,116,105,111,110,58,32,13,10,80,114,111,120,121,45,65,117,116,104,101,110,116,105,99,97,116,101,58,32,13,10,82,101,116,114,121,45,65,102,116,101,114,58,32,13,10,83,101,114,118,101,114,58,32,13,10,83,101,116,45,67,111,111,107,105,101,58,32,13,10,86,97,114,121,58,32,13,10,87,87,87,45,65,117,116,104,101,110,116,105,99,97,116,101,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,65,108,108,111,119,45,67,114,101,100,101,110,116,105,97,108,115,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,65,108,108,111,119,45,72,101,97,100,101,114,115,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,65,108,108,111,119,45,77,101,116,104,111,100,115,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,65,108,108,111,119,45,79,114,105,103,105,110,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,69,120,112,111,115,101,45,72,101,97,100,101,114,115,58,32,13,10,65,99,99,101,115,115,45,67,111,110,116,114,111,108,45,77,97,120,45,65,103,101,58,32,13,10,67,111,110,116,101,110,116,45,76,101,110,103,116,104,58,32, }; private HeaderReferences _headers; public bool HasConnection => (_bits & 0x2L) != 0; public bool HasDate => (_bits & 0x4L) != 0; public bool HasTransferEncoding => (_bits & 0x40L) != 0; - public bool HasServer => (_bits & 0x2000000L) != 0; + public bool HasServer => (_bits & 0x4000000L) != 0; public StringValues HeaderCacheControl @@ -7029,12 +7029,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _headers._Age = value; } } - public StringValues HeaderETag + public StringValues HeaderAltSvc { get { StringValues value = default; if ((_bits & 0x200000L) != 0) + { + value = _headers._AltSvc; + } + return value; + } + set + { + _bits |= 0x200000L; + _headers._AltSvc = value; + } + } + public StringValues HeaderETag + { + get + { + StringValues value = default; + if ((_bits & 0x400000L) != 0) { value = _headers._ETag; } @@ -7042,7 +7059,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x200000L; + _bits |= 0x400000L; _headers._ETag = value; } } @@ -7051,7 +7068,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x400000L) != 0) + if ((_bits & 0x800000L) != 0) { value = _headers._Location; } @@ -7059,7 +7076,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x400000L; + _bits |= 0x800000L; _headers._Location = value; } } @@ -7068,7 +7085,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x800000L) != 0) + if ((_bits & 0x1000000L) != 0) { value = _headers._ProxyAuthenticate; } @@ -7076,7 +7093,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x800000L; + _bits |= 0x1000000L; _headers._ProxyAuthenticate = value; } } @@ -7085,7 +7102,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x1000000L) != 0) + if ((_bits & 0x2000000L) != 0) { value = _headers._RetryAfter; } @@ -7093,7 +7110,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x1000000L; + _bits |= 0x2000000L; _headers._RetryAfter = value; } } @@ -7102,7 +7119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x2000000L) != 0) + if ((_bits & 0x4000000L) != 0) { value = _headers._Server; } @@ -7110,7 +7127,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x2000000L; + _bits |= 0x4000000L; _headers._Server = value; _headers._rawServer = null; } @@ -7120,7 +7137,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x4000000L) != 0) + if ((_bits & 0x8000000L) != 0) { value = _headers._SetCookie; } @@ -7128,7 +7145,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x4000000L; + _bits |= 0x8000000L; _headers._SetCookie = value; } } @@ -7137,7 +7154,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x8000000L) != 0) + if ((_bits & 0x10000000L) != 0) { value = _headers._Vary; } @@ -7145,7 +7162,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x8000000L; + _bits |= 0x10000000L; _headers._Vary = value; } } @@ -7154,7 +7171,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x10000000L) != 0) + if ((_bits & 0x20000000L) != 0) { value = _headers._WWWAuthenticate; } @@ -7162,7 +7179,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x10000000L; + _bits |= 0x20000000L; _headers._WWWAuthenticate = value; } } @@ -7171,7 +7188,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x20000000L) != 0) + if ((_bits & 0x40000000L) != 0) { value = _headers._AccessControlAllowCredentials; } @@ -7179,7 +7196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x20000000L; + _bits |= 0x40000000L; _headers._AccessControlAllowCredentials = value; } } @@ -7188,7 +7205,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x40000000L) != 0) + if ((_bits & 0x80000000L) != 0) { value = _headers._AccessControlAllowHeaders; } @@ -7196,7 +7213,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x40000000L; + _bits |= 0x80000000L; _headers._AccessControlAllowHeaders = value; } } @@ -7205,7 +7222,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x80000000L) != 0) + if ((_bits & 0x100000000L) != 0) { value = _headers._AccessControlAllowMethods; } @@ -7213,7 +7230,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x80000000L; + _bits |= 0x100000000L; _headers._AccessControlAllowMethods = value; } } @@ -7222,7 +7239,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x100000000L) != 0) + if ((_bits & 0x200000000L) != 0) { value = _headers._AccessControlAllowOrigin; } @@ -7230,7 +7247,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x100000000L; + _bits |= 0x200000000L; _headers._AccessControlAllowOrigin = value; } } @@ -7239,7 +7256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x200000000L) != 0) + if ((_bits & 0x400000000L) != 0) { value = _headers._AccessControlExposeHeaders; } @@ -7247,7 +7264,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x200000000L; + _bits |= 0x400000000L; _headers._AccessControlExposeHeaders = value; } } @@ -7256,7 +7273,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http get { StringValues value = default; - if ((_bits & 0x400000000L) != 0) + if ((_bits & 0x800000000L) != 0) { value = _headers._AccessControlMaxAge; } @@ -7264,7 +7281,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } set { - _bits |= 0x400000000L; + _bits |= 0x800000000L; _headers._AccessControlMaxAge = value; } } @@ -7305,7 +7322,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } public void SetRawServer(StringValues value, byte[] raw) { - _bits |= 0x2000000L; + _bits |= 0x4000000L; _headers._Server = value; _headers._rawServer = raw; } @@ -7373,7 +7390,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.ETag, key)) { - if ((_bits & 0x200000L) != 0) + if ((_bits & 0x400000L) != 0) { value = _headers._ETag; return true; @@ -7382,7 +7399,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.Vary, key)) { - if ((_bits & 0x8000000L) != 0) + if ((_bits & 0x10000000L) != 0) { value = _headers._Vary; return true; @@ -7401,7 +7418,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.ETag.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x200000L) != 0) + if ((_bits & 0x400000L) != 0) { value = _headers._ETag; return true; @@ -7410,7 +7427,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.Vary.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x8000000L) != 0) + if ((_bits & 0x10000000L) != 0) { value = _headers._Vary; return true; @@ -7446,7 +7463,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.Server, key)) { - if ((_bits & 0x2000000L) != 0) + if ((_bits & 0x4000000L) != 0) { value = _headers._Server; return true; @@ -7465,7 +7482,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.Server.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x2000000L) != 0) + if ((_bits & 0x4000000L) != 0) { value = _headers._Server; return true; @@ -7521,6 +7538,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } return false; } + if (ReferenceEquals(HeaderNames.AltSvc, key)) + { + if ((_bits & 0x200000L) != 0) + { + value = _headers._AltSvc; + return true; + } + return false; + } if (HeaderNames.Trailer.Equals(key, StringComparison.OrdinalIgnoreCase)) { @@ -7558,13 +7584,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } return false; } + if (HeaderNames.AltSvc.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + if ((_bits & 0x200000L) != 0) + { + value = _headers._AltSvc; + return true; + } + return false; + } break; } case 8: { if (ReferenceEquals(HeaderNames.Location, key)) { - if ((_bits & 0x400000L) != 0) + if ((_bits & 0x800000L) != 0) { value = _headers._Location; return true; @@ -7574,7 +7609,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.Location.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x400000L) != 0) + if ((_bits & 0x800000L) != 0) { value = _headers._Location; return true; @@ -7605,7 +7640,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.SetCookie, key)) { - if ((_bits & 0x4000000L) != 0) + if ((_bits & 0x8000000L) != 0) { value = _headers._SetCookie; return true; @@ -7633,7 +7668,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.SetCookie.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x4000000L) != 0) + if ((_bits & 0x8000000L) != 0) { value = _headers._SetCookie; return true; @@ -7655,7 +7690,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.RetryAfter, key)) { - if ((_bits & 0x1000000L) != 0) + if ((_bits & 0x2000000L) != 0) { value = _headers._RetryAfter; return true; @@ -7674,7 +7709,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.RetryAfter.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x1000000L) != 0) + if ((_bits & 0x2000000L) != 0) { value = _headers._RetryAfter; return true; @@ -7837,7 +7872,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.WWWAuthenticate, key)) { - if ((_bits & 0x10000000L) != 0) + if ((_bits & 0x20000000L) != 0) { value = _headers._WWWAuthenticate; return true; @@ -7874,7 +7909,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.WWWAuthenticate.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x10000000L) != 0) + if ((_bits & 0x20000000L) != 0) { value = _headers._WWWAuthenticate; return true; @@ -7910,7 +7945,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.ProxyAuthenticate, key)) { - if ((_bits & 0x800000L) != 0) + if ((_bits & 0x1000000L) != 0) { value = _headers._ProxyAuthenticate; return true; @@ -7920,7 +7955,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.ProxyAuthenticate.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x800000L) != 0) + if ((_bits & 0x1000000L) != 0) { value = _headers._ProxyAuthenticate; return true; @@ -7933,7 +7968,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlMaxAge, key)) { - if ((_bits & 0x400000000L) != 0) + if ((_bits & 0x800000000L) != 0) { value = _headers._AccessControlMaxAge; return true; @@ -7943,7 +7978,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlMaxAge.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x400000000L) != 0) + if ((_bits & 0x800000000L) != 0) { value = _headers._AccessControlMaxAge; return true; @@ -7956,7 +7991,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowOrigin, key)) { - if ((_bits & 0x100000000L) != 0) + if ((_bits & 0x200000000L) != 0) { value = _headers._AccessControlAllowOrigin; return true; @@ -7966,7 +8001,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlAllowOrigin.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x100000000L) != 0) + if ((_bits & 0x200000000L) != 0) { value = _headers._AccessControlAllowOrigin; return true; @@ -7979,7 +8014,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowHeaders, key)) { - if ((_bits & 0x40000000L) != 0) + if ((_bits & 0x80000000L) != 0) { value = _headers._AccessControlAllowHeaders; return true; @@ -7988,7 +8023,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.AccessControlAllowMethods, key)) { - if ((_bits & 0x80000000L) != 0) + if ((_bits & 0x100000000L) != 0) { value = _headers._AccessControlAllowMethods; return true; @@ -7998,7 +8033,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlAllowHeaders.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x40000000L) != 0) + if ((_bits & 0x80000000L) != 0) { value = _headers._AccessControlAllowHeaders; return true; @@ -8007,7 +8042,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.AccessControlAllowMethods.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x80000000L) != 0) + if ((_bits & 0x100000000L) != 0) { value = _headers._AccessControlAllowMethods; return true; @@ -8020,7 +8055,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlExposeHeaders, key)) { - if ((_bits & 0x200000000L) != 0) + if ((_bits & 0x400000000L) != 0) { value = _headers._AccessControlExposeHeaders; return true; @@ -8030,7 +8065,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlExposeHeaders.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x200000000L) != 0) + if ((_bits & 0x400000000L) != 0) { value = _headers._AccessControlExposeHeaders; return true; @@ -8043,7 +8078,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowCredentials, key)) { - if ((_bits & 0x20000000L) != 0) + if ((_bits & 0x40000000L) != 0) { value = _headers._AccessControlAllowCredentials; return true; @@ -8053,7 +8088,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlAllowCredentials.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x20000000L) != 0) + if ((_bits & 0x40000000L) != 0) { value = _headers._AccessControlAllowCredentials; return true; @@ -8112,13 +8147,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.ETag, key)) { - _bits |= 0x200000L; + _bits |= 0x400000L; _headers._ETag = value; return; } if (ReferenceEquals(HeaderNames.Vary, key)) { - _bits |= 0x8000000L; + _bits |= 0x10000000L; _headers._Vary = value; return; } @@ -8132,13 +8167,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.ETag.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x200000L; + _bits |= 0x400000L; _headers._ETag = value; return; } if (HeaderNames.Vary.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x8000000L; + _bits |= 0x10000000L; _headers._Vary = value; return; } @@ -8165,7 +8200,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.Server, key)) { - _bits |= 0x2000000L; + _bits |= 0x4000000L; _headers._Server = value; _headers._rawServer = null; return; @@ -8179,7 +8214,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.Server.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x2000000L; + _bits |= 0x4000000L; _headers._Server = value; _headers._rawServer = null; return; @@ -8218,6 +8253,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _headers._Expires = value; return; } + if (ReferenceEquals(HeaderNames.AltSvc, key)) + { + _bits |= 0x200000L; + _headers._AltSvc = value; + return; + } if (HeaderNames.Trailer.Equals(key, StringComparison.OrdinalIgnoreCase)) { @@ -8243,20 +8284,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _headers._Expires = value; return; } + if (HeaderNames.AltSvc.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + _bits |= 0x200000L; + _headers._AltSvc = value; + return; + } break; } case 8: { if (ReferenceEquals(HeaderNames.Location, key)) { - _bits |= 0x400000L; + _bits |= 0x800000L; _headers._Location = value; return; } if (HeaderNames.Location.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x400000L; + _bits |= 0x800000L; _headers._Location = value; return; } @@ -8279,7 +8326,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.SetCookie, key)) { - _bits |= 0x4000000L; + _bits |= 0x8000000L; _headers._SetCookie = value; return; } @@ -8299,7 +8346,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.SetCookie.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x4000000L; + _bits |= 0x8000000L; _headers._SetCookie = value; return; } @@ -8315,7 +8362,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.RetryAfter, key)) { - _bits |= 0x1000000L; + _bits |= 0x2000000L; _headers._RetryAfter = value; return; } @@ -8328,7 +8375,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.RetryAfter.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x1000000L; + _bits |= 0x2000000L; _headers._RetryAfter = value; return; } @@ -8441,7 +8488,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.WWWAuthenticate, key)) { - _bits |= 0x10000000L; + _bits |= 0x20000000L; _headers._WWWAuthenticate = value; return; } @@ -8466,7 +8513,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.WWWAuthenticate.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x10000000L; + _bits |= 0x20000000L; _headers._WWWAuthenticate = value; return; } @@ -8495,14 +8542,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.ProxyAuthenticate, key)) { - _bits |= 0x800000L; + _bits |= 0x1000000L; _headers._ProxyAuthenticate = value; return; } if (HeaderNames.ProxyAuthenticate.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x800000L; + _bits |= 0x1000000L; _headers._ProxyAuthenticate = value; return; } @@ -8512,14 +8559,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlMaxAge, key)) { - _bits |= 0x400000000L; + _bits |= 0x800000000L; _headers._AccessControlMaxAge = value; return; } if (HeaderNames.AccessControlMaxAge.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x400000000L; + _bits |= 0x800000000L; _headers._AccessControlMaxAge = value; return; } @@ -8529,14 +8576,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowOrigin, key)) { - _bits |= 0x100000000L; + _bits |= 0x200000000L; _headers._AccessControlAllowOrigin = value; return; } if (HeaderNames.AccessControlAllowOrigin.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x100000000L; + _bits |= 0x200000000L; _headers._AccessControlAllowOrigin = value; return; } @@ -8546,26 +8593,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowHeaders, key)) { - _bits |= 0x40000000L; + _bits |= 0x80000000L; _headers._AccessControlAllowHeaders = value; return; } if (ReferenceEquals(HeaderNames.AccessControlAllowMethods, key)) { - _bits |= 0x80000000L; + _bits |= 0x100000000L; _headers._AccessControlAllowMethods = value; return; } if (HeaderNames.AccessControlAllowHeaders.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x40000000L; + _bits |= 0x80000000L; _headers._AccessControlAllowHeaders = value; return; } if (HeaderNames.AccessControlAllowMethods.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x80000000L; + _bits |= 0x100000000L; _headers._AccessControlAllowMethods = value; return; } @@ -8575,14 +8622,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlExposeHeaders, key)) { - _bits |= 0x200000000L; + _bits |= 0x400000000L; _headers._AccessControlExposeHeaders = value; return; } if (HeaderNames.AccessControlExposeHeaders.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x200000000L; + _bits |= 0x400000000L; _headers._AccessControlExposeHeaders = value; return; } @@ -8592,14 +8639,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowCredentials, key)) { - _bits |= 0x20000000L; + _bits |= 0x40000000L; _headers._AccessControlAllowCredentials = value; return; } if (HeaderNames.AccessControlAllowCredentials.Equals(key, StringComparison.OrdinalIgnoreCase)) { - _bits |= 0x20000000L; + _bits |= 0x40000000L; _headers._AccessControlAllowCredentials = value; return; } @@ -8675,9 +8722,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.ETag, key)) { - if ((_bits & 0x200000L) == 0) + if ((_bits & 0x400000L) == 0) { - _bits |= 0x200000L; + _bits |= 0x400000L; _headers._ETag = value; return true; } @@ -8685,9 +8732,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.Vary, key)) { - if ((_bits & 0x8000000L) == 0) + if ((_bits & 0x10000000L) == 0) { - _bits |= 0x8000000L; + _bits |= 0x10000000L; _headers._Vary = value; return true; } @@ -8707,9 +8754,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.ETag.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x200000L) == 0) + if ((_bits & 0x400000L) == 0) { - _bits |= 0x200000L; + _bits |= 0x400000L; _headers._ETag = value; return true; } @@ -8717,9 +8764,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.Vary.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x8000000L) == 0) + if ((_bits & 0x10000000L) == 0) { - _bits |= 0x8000000L; + _bits |= 0x10000000L; _headers._Vary = value; return true; } @@ -8756,9 +8803,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.Server, key)) { - if ((_bits & 0x2000000L) == 0) + if ((_bits & 0x4000000L) == 0) { - _bits |= 0x2000000L; + _bits |= 0x4000000L; _headers._Server = value; _headers._rawServer = null; return true; @@ -8778,9 +8825,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.Server.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x2000000L) == 0) + if ((_bits & 0x4000000L) == 0) { - _bits |= 0x2000000L; + _bits |= 0x4000000L; _headers._Server = value; _headers._rawServer = null; return true; @@ -8841,6 +8888,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } return false; } + if (ReferenceEquals(HeaderNames.AltSvc, key)) + { + if ((_bits & 0x200000L) == 0) + { + _bits |= 0x200000L; + _headers._AltSvc = value; + return true; + } + return false; + } if (HeaderNames.Trailer.Equals(key, StringComparison.OrdinalIgnoreCase)) { @@ -8882,15 +8939,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } return false; } + if (HeaderNames.AltSvc.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + if ((_bits & 0x200000L) == 0) + { + _bits |= 0x200000L; + _headers._AltSvc = value; + return true; + } + return false; + } break; } case 8: { if (ReferenceEquals(HeaderNames.Location, key)) { - if ((_bits & 0x400000L) == 0) + if ((_bits & 0x800000L) == 0) { - _bits |= 0x400000L; + _bits |= 0x800000L; _headers._Location = value; return true; } @@ -8899,9 +8966,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.Location.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x400000L) == 0) + if ((_bits & 0x800000L) == 0) { - _bits |= 0x400000L; + _bits |= 0x800000L; _headers._Location = value; return true; } @@ -8934,9 +9001,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.SetCookie, key)) { - if ((_bits & 0x4000000L) == 0) + if ((_bits & 0x8000000L) == 0) { - _bits |= 0x4000000L; + _bits |= 0x8000000L; _headers._SetCookie = value; return true; } @@ -8966,9 +9033,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.SetCookie.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x4000000L) == 0) + if ((_bits & 0x8000000L) == 0) { - _bits |= 0x4000000L; + _bits |= 0x8000000L; _headers._SetCookie = value; return true; } @@ -8990,9 +9057,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.RetryAfter, key)) { - if ((_bits & 0x1000000L) == 0) + if ((_bits & 0x2000000L) == 0) { - _bits |= 0x1000000L; + _bits |= 0x2000000L; _headers._RetryAfter = value; return true; } @@ -9011,9 +9078,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.RetryAfter.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x1000000L) == 0) + if ((_bits & 0x2000000L) == 0) { - _bits |= 0x1000000L; + _bits |= 0x2000000L; _headers._RetryAfter = value; return true; } @@ -9188,9 +9255,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.WWWAuthenticate, key)) { - if ((_bits & 0x10000000L) == 0) + if ((_bits & 0x20000000L) == 0) { - _bits |= 0x10000000L; + _bits |= 0x20000000L; _headers._WWWAuthenticate = value; return true; } @@ -9229,9 +9296,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.WWWAuthenticate.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x10000000L) == 0) + if ((_bits & 0x20000000L) == 0) { - _bits |= 0x10000000L; + _bits |= 0x20000000L; _headers._WWWAuthenticate = value; return true; } @@ -9270,9 +9337,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.ProxyAuthenticate, key)) { - if ((_bits & 0x800000L) == 0) + if ((_bits & 0x1000000L) == 0) { - _bits |= 0x800000L; + _bits |= 0x1000000L; _headers._ProxyAuthenticate = value; return true; } @@ -9281,9 +9348,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.ProxyAuthenticate.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x800000L) == 0) + if ((_bits & 0x1000000L) == 0) { - _bits |= 0x800000L; + _bits |= 0x1000000L; _headers._ProxyAuthenticate = value; return true; } @@ -9295,9 +9362,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlMaxAge, key)) { - if ((_bits & 0x400000000L) == 0) + if ((_bits & 0x800000000L) == 0) { - _bits |= 0x400000000L; + _bits |= 0x800000000L; _headers._AccessControlMaxAge = value; return true; } @@ -9306,9 +9373,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlMaxAge.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x400000000L) == 0) + if ((_bits & 0x800000000L) == 0) { - _bits |= 0x400000000L; + _bits |= 0x800000000L; _headers._AccessControlMaxAge = value; return true; } @@ -9320,9 +9387,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowOrigin, key)) { - if ((_bits & 0x100000000L) == 0) + if ((_bits & 0x200000000L) == 0) { - _bits |= 0x100000000L; + _bits |= 0x200000000L; _headers._AccessControlAllowOrigin = value; return true; } @@ -9331,9 +9398,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlAllowOrigin.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x100000000L) == 0) + if ((_bits & 0x200000000L) == 0) { - _bits |= 0x100000000L; + _bits |= 0x200000000L; _headers._AccessControlAllowOrigin = value; return true; } @@ -9345,9 +9412,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowHeaders, key)) { - if ((_bits & 0x40000000L) == 0) + if ((_bits & 0x80000000L) == 0) { - _bits |= 0x40000000L; + _bits |= 0x80000000L; _headers._AccessControlAllowHeaders = value; return true; } @@ -9355,9 +9422,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.AccessControlAllowMethods, key)) { - if ((_bits & 0x80000000L) == 0) + if ((_bits & 0x100000000L) == 0) { - _bits |= 0x80000000L; + _bits |= 0x100000000L; _headers._AccessControlAllowMethods = value; return true; } @@ -9366,9 +9433,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlAllowHeaders.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x40000000L) == 0) + if ((_bits & 0x80000000L) == 0) { - _bits |= 0x40000000L; + _bits |= 0x80000000L; _headers._AccessControlAllowHeaders = value; return true; } @@ -9376,9 +9443,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.AccessControlAllowMethods.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x80000000L) == 0) + if ((_bits & 0x100000000L) == 0) { - _bits |= 0x80000000L; + _bits |= 0x100000000L; _headers._AccessControlAllowMethods = value; return true; } @@ -9390,9 +9457,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlExposeHeaders, key)) { - if ((_bits & 0x200000000L) == 0) + if ((_bits & 0x400000000L) == 0) { - _bits |= 0x200000000L; + _bits |= 0x400000000L; _headers._AccessControlExposeHeaders = value; return true; } @@ -9401,9 +9468,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlExposeHeaders.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x200000000L) == 0) + if ((_bits & 0x400000000L) == 0) { - _bits |= 0x200000000L; + _bits |= 0x400000000L; _headers._AccessControlExposeHeaders = value; return true; } @@ -9415,9 +9482,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowCredentials, key)) { - if ((_bits & 0x20000000L) == 0) + if ((_bits & 0x40000000L) == 0) { - _bits |= 0x20000000L; + _bits |= 0x40000000L; _headers._AccessControlAllowCredentials = value; return true; } @@ -9426,9 +9493,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlAllowCredentials.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x20000000L) == 0) + if ((_bits & 0x40000000L) == 0) { - _bits |= 0x20000000L; + _bits |= 0x40000000L; _headers._AccessControlAllowCredentials = value; return true; } @@ -9505,9 +9572,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.ETag, key)) { - if ((_bits & 0x200000L) != 0) + if ((_bits & 0x400000L) != 0) { - _bits &= ~0x200000L; + _bits &= ~0x400000L; _headers._ETag = default(StringValues); return true; } @@ -9515,9 +9582,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.Vary, key)) { - if ((_bits & 0x8000000L) != 0) + if ((_bits & 0x10000000L) != 0) { - _bits &= ~0x8000000L; + _bits &= ~0x10000000L; _headers._Vary = default(StringValues); return true; } @@ -9537,9 +9604,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.ETag.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x200000L) != 0) + if ((_bits & 0x400000L) != 0) { - _bits &= ~0x200000L; + _bits &= ~0x400000L; _headers._ETag = default(StringValues); return true; } @@ -9547,9 +9614,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.Vary.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x8000000L) != 0) + if ((_bits & 0x10000000L) != 0) { - _bits &= ~0x8000000L; + _bits &= ~0x10000000L; _headers._Vary = default(StringValues); return true; } @@ -9586,9 +9653,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.Server, key)) { - if ((_bits & 0x2000000L) != 0) + if ((_bits & 0x4000000L) != 0) { - _bits &= ~0x2000000L; + _bits &= ~0x4000000L; _headers._Server = default(StringValues); _headers._rawServer = null; return true; @@ -9608,9 +9675,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.Server.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x2000000L) != 0) + if ((_bits & 0x4000000L) != 0) { - _bits &= ~0x2000000L; + _bits &= ~0x4000000L; _headers._Server = default(StringValues); _headers._rawServer = null; return true; @@ -9671,6 +9738,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } return false; } + if (ReferenceEquals(HeaderNames.AltSvc, key)) + { + if ((_bits & 0x200000L) != 0) + { + _bits &= ~0x200000L; + _headers._AltSvc = default(StringValues); + return true; + } + return false; + } if (HeaderNames.Trailer.Equals(key, StringComparison.OrdinalIgnoreCase)) { @@ -9712,15 +9789,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } return false; } + if (HeaderNames.AltSvc.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + if ((_bits & 0x200000L) != 0) + { + _bits &= ~0x200000L; + _headers._AltSvc = default(StringValues); + return true; + } + return false; + } break; } case 8: { if (ReferenceEquals(HeaderNames.Location, key)) { - if ((_bits & 0x400000L) != 0) + if ((_bits & 0x800000L) != 0) { - _bits &= ~0x400000L; + _bits &= ~0x800000L; _headers._Location = default(StringValues); return true; } @@ -9729,9 +9816,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.Location.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x400000L) != 0) + if ((_bits & 0x800000L) != 0) { - _bits &= ~0x400000L; + _bits &= ~0x800000L; _headers._Location = default(StringValues); return true; } @@ -9764,9 +9851,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.SetCookie, key)) { - if ((_bits & 0x4000000L) != 0) + if ((_bits & 0x8000000L) != 0) { - _bits &= ~0x4000000L; + _bits &= ~0x8000000L; _headers._SetCookie = default(StringValues); return true; } @@ -9796,9 +9883,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.SetCookie.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x4000000L) != 0) + if ((_bits & 0x8000000L) != 0) { - _bits &= ~0x4000000L; + _bits &= ~0x8000000L; _headers._SetCookie = default(StringValues); return true; } @@ -9820,9 +9907,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.RetryAfter, key)) { - if ((_bits & 0x1000000L) != 0) + if ((_bits & 0x2000000L) != 0) { - _bits &= ~0x1000000L; + _bits &= ~0x2000000L; _headers._RetryAfter = default(StringValues); return true; } @@ -9841,9 +9928,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.RetryAfter.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x1000000L) != 0) + if ((_bits & 0x2000000L) != 0) { - _bits &= ~0x1000000L; + _bits &= ~0x2000000L; _headers._RetryAfter = default(StringValues); return true; } @@ -10018,9 +10105,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.WWWAuthenticate, key)) { - if ((_bits & 0x10000000L) != 0) + if ((_bits & 0x20000000L) != 0) { - _bits &= ~0x10000000L; + _bits &= ~0x20000000L; _headers._WWWAuthenticate = default(StringValues); return true; } @@ -10059,9 +10146,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.WWWAuthenticate.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x10000000L) != 0) + if ((_bits & 0x20000000L) != 0) { - _bits &= ~0x10000000L; + _bits &= ~0x20000000L; _headers._WWWAuthenticate = default(StringValues); return true; } @@ -10100,9 +10187,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.ProxyAuthenticate, key)) { - if ((_bits & 0x800000L) != 0) + if ((_bits & 0x1000000L) != 0) { - _bits &= ~0x800000L; + _bits &= ~0x1000000L; _headers._ProxyAuthenticate = default(StringValues); return true; } @@ -10111,9 +10198,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.ProxyAuthenticate.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x800000L) != 0) + if ((_bits & 0x1000000L) != 0) { - _bits &= ~0x800000L; + _bits &= ~0x1000000L; _headers._ProxyAuthenticate = default(StringValues); return true; } @@ -10125,9 +10212,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlMaxAge, key)) { - if ((_bits & 0x400000000L) != 0) + if ((_bits & 0x800000000L) != 0) { - _bits &= ~0x400000000L; + _bits &= ~0x800000000L; _headers._AccessControlMaxAge = default(StringValues); return true; } @@ -10136,9 +10223,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlMaxAge.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x400000000L) != 0) + if ((_bits & 0x800000000L) != 0) { - _bits &= ~0x400000000L; + _bits &= ~0x800000000L; _headers._AccessControlMaxAge = default(StringValues); return true; } @@ -10150,9 +10237,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowOrigin, key)) { - if ((_bits & 0x100000000L) != 0) + if ((_bits & 0x200000000L) != 0) { - _bits &= ~0x100000000L; + _bits &= ~0x200000000L; _headers._AccessControlAllowOrigin = default(StringValues); return true; } @@ -10161,9 +10248,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlAllowOrigin.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x100000000L) != 0) + if ((_bits & 0x200000000L) != 0) { - _bits &= ~0x100000000L; + _bits &= ~0x200000000L; _headers._AccessControlAllowOrigin = default(StringValues); return true; } @@ -10175,9 +10262,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowHeaders, key)) { - if ((_bits & 0x40000000L) != 0) + if ((_bits & 0x80000000L) != 0) { - _bits &= ~0x40000000L; + _bits &= ~0x80000000L; _headers._AccessControlAllowHeaders = default(StringValues); return true; } @@ -10185,9 +10272,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (ReferenceEquals(HeaderNames.AccessControlAllowMethods, key)) { - if ((_bits & 0x80000000L) != 0) + if ((_bits & 0x100000000L) != 0) { - _bits &= ~0x80000000L; + _bits &= ~0x100000000L; _headers._AccessControlAllowMethods = default(StringValues); return true; } @@ -10196,9 +10283,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlAllowHeaders.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x40000000L) != 0) + if ((_bits & 0x80000000L) != 0) { - _bits &= ~0x40000000L; + _bits &= ~0x80000000L; _headers._AccessControlAllowHeaders = default(StringValues); return true; } @@ -10206,9 +10293,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } if (HeaderNames.AccessControlAllowMethods.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x80000000L) != 0) + if ((_bits & 0x100000000L) != 0) { - _bits &= ~0x80000000L; + _bits &= ~0x100000000L; _headers._AccessControlAllowMethods = default(StringValues); return true; } @@ -10220,9 +10307,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlExposeHeaders, key)) { - if ((_bits & 0x200000000L) != 0) + if ((_bits & 0x400000000L) != 0) { - _bits &= ~0x200000000L; + _bits &= ~0x400000000L; _headers._AccessControlExposeHeaders = default(StringValues); return true; } @@ -10231,9 +10318,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlExposeHeaders.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x200000000L) != 0) + if ((_bits & 0x400000000L) != 0) { - _bits &= ~0x200000000L; + _bits &= ~0x400000000L; _headers._AccessControlExposeHeaders = default(StringValues); return true; } @@ -10245,9 +10332,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { if (ReferenceEquals(HeaderNames.AccessControlAllowCredentials, key)) { - if ((_bits & 0x20000000L) != 0) + if ((_bits & 0x40000000L) != 0) { - _bits &= ~0x20000000L; + _bits &= ~0x40000000L; _headers._AccessControlAllowCredentials = default(StringValues); return true; } @@ -10256,9 +10343,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (HeaderNames.AccessControlAllowCredentials.Equals(key, StringComparison.OrdinalIgnoreCase)) { - if ((_bits & 0x20000000L) != 0) + if ((_bits & 0x40000000L) != 0) { - _bits &= ~0x20000000L; + _bits &= ~0x40000000L; _headers._AccessControlAllowCredentials = default(StringValues); return true; } @@ -10312,14 +10399,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http tempBits &= ~0x800L; } - if ((tempBits & 0x2000000L) != 0) + if ((tempBits & 0x4000000L) != 0) { _headers._Server = default; - if((tempBits & ~0x2000000L) == 0) + if((tempBits & ~0x4000000L) == 0) { return; } - tempBits &= ~0x2000000L; + tempBits &= ~0x4000000L; } if ((tempBits & 0x1L) != 0) @@ -10504,7 +10591,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x200000L) != 0) { - _headers._ETag = default; + _headers._AltSvc = default; if((tempBits & ~0x200000L) == 0) { return; @@ -10514,7 +10601,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x400000L) != 0) { - _headers._Location = default; + _headers._ETag = default; if((tempBits & ~0x400000L) == 0) { return; @@ -10524,7 +10611,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x800000L) != 0) { - _headers._ProxyAuthenticate = default; + _headers._Location = default; if((tempBits & ~0x800000L) == 0) { return; @@ -10534,7 +10621,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x1000000L) != 0) { - _headers._RetryAfter = default; + _headers._ProxyAuthenticate = default; if((tempBits & ~0x1000000L) == 0) { return; @@ -10542,19 +10629,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http tempBits &= ~0x1000000L; } - if ((tempBits & 0x4000000L) != 0) + if ((tempBits & 0x2000000L) != 0) { - _headers._SetCookie = default; - if((tempBits & ~0x4000000L) == 0) + _headers._RetryAfter = default; + if((tempBits & ~0x2000000L) == 0) { return; } - tempBits &= ~0x4000000L; + tempBits &= ~0x2000000L; } if ((tempBits & 0x8000000L) != 0) { - _headers._Vary = default; + _headers._SetCookie = default; if((tempBits & ~0x8000000L) == 0) { return; @@ -10564,7 +10651,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x10000000L) != 0) { - _headers._WWWAuthenticate = default; + _headers._Vary = default; if((tempBits & ~0x10000000L) == 0) { return; @@ -10574,7 +10661,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x20000000L) != 0) { - _headers._AccessControlAllowCredentials = default; + _headers._WWWAuthenticate = default; if((tempBits & ~0x20000000L) == 0) { return; @@ -10584,7 +10671,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x40000000L) != 0) { - _headers._AccessControlAllowHeaders = default; + _headers._AccessControlAllowCredentials = default; if((tempBits & ~0x40000000L) == 0) { return; @@ -10594,7 +10681,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x80000000L) != 0) { - _headers._AccessControlAllowMethods = default; + _headers._AccessControlAllowHeaders = default; if((tempBits & ~0x80000000L) == 0) { return; @@ -10604,7 +10691,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x100000000L) != 0) { - _headers._AccessControlAllowOrigin = default; + _headers._AccessControlAllowMethods = default; if((tempBits & ~0x100000000L) == 0) { return; @@ -10614,7 +10701,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x200000000L) != 0) { - _headers._AccessControlExposeHeaders = default; + _headers._AccessControlAllowOrigin = default; if((tempBits & ~0x200000000L) == 0) { return; @@ -10624,7 +10711,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x400000000L) != 0) { - _headers._AccessControlMaxAge = default; + _headers._AccessControlExposeHeaders = default; if((tempBits & ~0x400000000L) == 0) { return; @@ -10632,6 +10719,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http tempBits &= ~0x400000000L; } + if ((tempBits & 0x800000000L) != 0) + { + _headers._AccessControlMaxAge = default; + if((tempBits & ~0x800000000L) == 0) + { + return; + } + tempBits &= ~0x800000000L; + } + } protected override bool CopyToFast(KeyValuePair[] array, int arrayIndex) @@ -10836,7 +10933,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.ETag, _headers._ETag); + array[arrayIndex] = new KeyValuePair(HeaderNames.AltSvc, _headers._AltSvc); ++arrayIndex; } if ((_bits & 0x400000L) != 0) @@ -10845,7 +10942,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.Location, _headers._Location); + array[arrayIndex] = new KeyValuePair(HeaderNames.ETag, _headers._ETag); ++arrayIndex; } if ((_bits & 0x800000L) != 0) @@ -10854,7 +10951,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.ProxyAuthenticate, _headers._ProxyAuthenticate); + array[arrayIndex] = new KeyValuePair(HeaderNames.Location, _headers._Location); ++arrayIndex; } if ((_bits & 0x1000000L) != 0) @@ -10863,7 +10960,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.RetryAfter, _headers._RetryAfter); + array[arrayIndex] = new KeyValuePair(HeaderNames.ProxyAuthenticate, _headers._ProxyAuthenticate); ++arrayIndex; } if ((_bits & 0x2000000L) != 0) @@ -10872,7 +10969,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.Server, _headers._Server); + array[arrayIndex] = new KeyValuePair(HeaderNames.RetryAfter, _headers._RetryAfter); ++arrayIndex; } if ((_bits & 0x4000000L) != 0) @@ -10881,7 +10978,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.SetCookie, _headers._SetCookie); + array[arrayIndex] = new KeyValuePair(HeaderNames.Server, _headers._Server); ++arrayIndex; } if ((_bits & 0x8000000L) != 0) @@ -10890,7 +10987,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.Vary, _headers._Vary); + array[arrayIndex] = new KeyValuePair(HeaderNames.SetCookie, _headers._SetCookie); ++arrayIndex; } if ((_bits & 0x10000000L) != 0) @@ -10899,7 +10996,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.WWWAuthenticate, _headers._WWWAuthenticate); + array[arrayIndex] = new KeyValuePair(HeaderNames.Vary, _headers._Vary); ++arrayIndex; } if ((_bits & 0x20000000L) != 0) @@ -10908,7 +11005,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlAllowCredentials, _headers._AccessControlAllowCredentials); + array[arrayIndex] = new KeyValuePair(HeaderNames.WWWAuthenticate, _headers._WWWAuthenticate); ++arrayIndex; } if ((_bits & 0x40000000L) != 0) @@ -10917,7 +11014,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlAllowHeaders, _headers._AccessControlAllowHeaders); + array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlAllowCredentials, _headers._AccessControlAllowCredentials); ++arrayIndex; } if ((_bits & 0x80000000L) != 0) @@ -10926,7 +11023,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlAllowMethods, _headers._AccessControlAllowMethods); + array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlAllowHeaders, _headers._AccessControlAllowHeaders); ++arrayIndex; } if ((_bits & 0x100000000L) != 0) @@ -10935,7 +11032,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlAllowOrigin, _headers._AccessControlAllowOrigin); + array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlAllowMethods, _headers._AccessControlAllowMethods); ++arrayIndex; } if ((_bits & 0x200000000L) != 0) @@ -10944,10 +11041,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return false; } - array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlExposeHeaders, _headers._AccessControlExposeHeaders); + array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlAllowOrigin, _headers._AccessControlAllowOrigin); ++arrayIndex; } if ((_bits & 0x400000000L) != 0) + { + if (arrayIndex == array.Length) + { + return false; + } + array[arrayIndex] = new KeyValuePair(HeaderNames.AccessControlExposeHeaders, _headers._AccessControlExposeHeaders); + ++arrayIndex; + } + if ((_bits & 0x800000000L) != 0) { if (arrayIndex == array.Length) { @@ -11030,9 +11136,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } goto case 3; case 3: // Header: "Server" - if ((tempBits & 0x2000000L) != 0) + if ((tempBits & 0x4000000L) != 0) { - tempBits ^= 0x2000000L; + tempBits ^= 0x4000000L; if (_headers._rawServer != null) { output.Write(_headers._rawServer); @@ -11040,7 +11146,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http else { values = ref _headers._Server; - keyStart = 350; + keyStart = 361; keyLength = 10; next = 4; break; // OutputHeader @@ -11051,7 +11157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if ((tempBits & 0x8000000000000000L) != 0) { tempBits ^= 0x8000000000000000L; - output.Write(HeaderBytes.Slice(592, 18)); + output.Write(HeaderBytes.Slice(603, 18)); output.WriteNumeric((ulong)ContentLength.Value); if (tempBits == 0) { @@ -11264,148 +11370,159 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http break; // OutputHeader } goto case 23; - case 23: // Header: "ETag" + case 23: // Header: "Alt-Svc" if ((tempBits & 0x200000L) != 0) { tempBits ^= 0x200000L; - values = ref _headers._ETag; + values = ref _headers._AltSvc; keyStart = 293; - keyLength = 8; + keyLength = 11; next = 24; break; // OutputHeader } goto case 24; - case 24: // Header: "Location" + case 24: // Header: "ETag" if ((tempBits & 0x400000L) != 0) { tempBits ^= 0x400000L; - values = ref _headers._Location; - keyStart = 301; - keyLength = 12; + values = ref _headers._ETag; + keyStart = 304; + keyLength = 8; next = 25; break; // OutputHeader } goto case 25; - case 25: // Header: "Proxy-Authenticate" + case 25: // Header: "Location" if ((tempBits & 0x800000L) != 0) { tempBits ^= 0x800000L; - values = ref _headers._ProxyAuthenticate; - keyStart = 313; - keyLength = 22; + values = ref _headers._Location; + keyStart = 312; + keyLength = 12; next = 26; break; // OutputHeader } goto case 26; - case 26: // Header: "Retry-After" + case 26: // Header: "Proxy-Authenticate" if ((tempBits & 0x1000000L) != 0) { tempBits ^= 0x1000000L; - values = ref _headers._RetryAfter; - keyStart = 335; - keyLength = 15; + values = ref _headers._ProxyAuthenticate; + keyStart = 324; + keyLength = 22; next = 27; break; // OutputHeader } goto case 27; - case 27: // Header: "Set-Cookie" - if ((tempBits & 0x4000000L) != 0) + case 27: // Header: "Retry-After" + if ((tempBits & 0x2000000L) != 0) { - tempBits ^= 0x4000000L; - values = ref _headers._SetCookie; - keyStart = 360; - keyLength = 14; + tempBits ^= 0x2000000L; + values = ref _headers._RetryAfter; + keyStart = 346; + keyLength = 15; next = 28; break; // OutputHeader } goto case 28; - case 28: // Header: "Vary" + case 28: // Header: "Set-Cookie" if ((tempBits & 0x8000000L) != 0) { tempBits ^= 0x8000000L; - values = ref _headers._Vary; - keyStart = 374; - keyLength = 8; + values = ref _headers._SetCookie; + keyStart = 371; + keyLength = 14; next = 29; break; // OutputHeader } goto case 29; - case 29: // Header: "WWW-Authenticate" + case 29: // Header: "Vary" if ((tempBits & 0x10000000L) != 0) { tempBits ^= 0x10000000L; - values = ref _headers._WWWAuthenticate; - keyStart = 382; - keyLength = 20; + values = ref _headers._Vary; + keyStart = 385; + keyLength = 8; next = 30; break; // OutputHeader } goto case 30; - case 30: // Header: "Access-Control-Allow-Credentials" + case 30: // Header: "WWW-Authenticate" if ((tempBits & 0x20000000L) != 0) { tempBits ^= 0x20000000L; - values = ref _headers._AccessControlAllowCredentials; - keyStart = 402; - keyLength = 36; + values = ref _headers._WWWAuthenticate; + keyStart = 393; + keyLength = 20; next = 31; break; // OutputHeader } goto case 31; - case 31: // Header: "Access-Control-Allow-Headers" + case 31: // Header: "Access-Control-Allow-Credentials" if ((tempBits & 0x40000000L) != 0) { tempBits ^= 0x40000000L; - values = ref _headers._AccessControlAllowHeaders; - keyStart = 438; - keyLength = 32; + values = ref _headers._AccessControlAllowCredentials; + keyStart = 413; + keyLength = 36; next = 32; break; // OutputHeader } goto case 32; - case 32: // Header: "Access-Control-Allow-Methods" + case 32: // Header: "Access-Control-Allow-Headers" if ((tempBits & 0x80000000L) != 0) { tempBits ^= 0x80000000L; - values = ref _headers._AccessControlAllowMethods; - keyStart = 470; + values = ref _headers._AccessControlAllowHeaders; + keyStart = 449; keyLength = 32; next = 33; break; // OutputHeader } goto case 33; - case 33: // Header: "Access-Control-Allow-Origin" + case 33: // Header: "Access-Control-Allow-Methods" if ((tempBits & 0x100000000L) != 0) { tempBits ^= 0x100000000L; - values = ref _headers._AccessControlAllowOrigin; - keyStart = 502; - keyLength = 31; + values = ref _headers._AccessControlAllowMethods; + keyStart = 481; + keyLength = 32; next = 34; break; // OutputHeader } goto case 34; - case 34: // Header: "Access-Control-Expose-Headers" + case 34: // Header: "Access-Control-Allow-Origin" if ((tempBits & 0x200000000L) != 0) { tempBits ^= 0x200000000L; - values = ref _headers._AccessControlExposeHeaders; - keyStart = 533; - keyLength = 33; + values = ref _headers._AccessControlAllowOrigin; + keyStart = 513; + keyLength = 31; next = 35; break; // OutputHeader } goto case 35; - case 35: // Header: "Access-Control-Max-Age" + case 35: // Header: "Access-Control-Expose-Headers" if ((tempBits & 0x400000000L) != 0) { tempBits ^= 0x400000000L; - values = ref _headers._AccessControlMaxAge; - keyStart = 566; - keyLength = 26; + values = ref _headers._AccessControlExposeHeaders; + keyStart = 544; + keyLength = 33; next = 36; break; // OutputHeader } + goto case 36; + case 36: // Header: "Access-Control-Max-Age" + if ((tempBits & 0x800000000L) != 0) + { + tempBits ^= 0x800000000L; + values = ref _headers._AccessControlMaxAge; + keyStart = 577; + keyLength = 26; + next = 37; + break; // OutputHeader + } return; default: return; @@ -11451,6 +11568,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public StringValues _LastModified; public StringValues _AcceptRanges; public StringValues _Age; + public StringValues _AltSvc; public StringValues _ETag; public StringValues _Location; public StringValues _ProxyAuthenticate; @@ -11522,34 +11640,36 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http case 20: goto HeaderAge; case 21: - goto HeaderETag; + goto HeaderAltSvc; case 22: - goto HeaderLocation; + goto HeaderETag; case 23: - goto HeaderProxyAuthenticate; + goto HeaderLocation; case 24: - goto HeaderRetryAfter; + goto HeaderProxyAuthenticate; case 25: - goto HeaderServer; + goto HeaderRetryAfter; case 26: - goto HeaderSetCookie; + goto HeaderServer; case 27: - goto HeaderVary; + goto HeaderSetCookie; case 28: - goto HeaderWWWAuthenticate; + goto HeaderVary; case 29: - goto HeaderAccessControlAllowCredentials; + goto HeaderWWWAuthenticate; case 30: - goto HeaderAccessControlAllowHeaders; + goto HeaderAccessControlAllowCredentials; case 31: - goto HeaderAccessControlAllowMethods; + goto HeaderAccessControlAllowHeaders; case 32: - goto HeaderAccessControlAllowOrigin; + goto HeaderAccessControlAllowMethods; case 33: - goto HeaderAccessControlExposeHeaders; + goto HeaderAccessControlAllowOrigin; case 34: - goto HeaderAccessControlMaxAge; + goto HeaderAccessControlExposeHeaders; case 35: + goto HeaderAccessControlMaxAge; + case 36: goto HeaderContentLength; default: goto ExtraHeaders; @@ -11702,109 +11822,116 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _next = 21; return true; } - HeaderETag: // case 21 + HeaderAltSvc: // case 21 if ((_bits & 0x200000L) != 0) { - _current = new KeyValuePair(HeaderNames.ETag, _collection._headers._ETag); + _current = new KeyValuePair(HeaderNames.AltSvc, _collection._headers._AltSvc); _next = 22; return true; } - HeaderLocation: // case 22 + HeaderETag: // case 22 if ((_bits & 0x400000L) != 0) { - _current = new KeyValuePair(HeaderNames.Location, _collection._headers._Location); + _current = new KeyValuePair(HeaderNames.ETag, _collection._headers._ETag); _next = 23; return true; } - HeaderProxyAuthenticate: // case 23 + HeaderLocation: // case 23 if ((_bits & 0x800000L) != 0) { - _current = new KeyValuePair(HeaderNames.ProxyAuthenticate, _collection._headers._ProxyAuthenticate); + _current = new KeyValuePair(HeaderNames.Location, _collection._headers._Location); _next = 24; return true; } - HeaderRetryAfter: // case 24 + HeaderProxyAuthenticate: // case 24 if ((_bits & 0x1000000L) != 0) { - _current = new KeyValuePair(HeaderNames.RetryAfter, _collection._headers._RetryAfter); + _current = new KeyValuePair(HeaderNames.ProxyAuthenticate, _collection._headers._ProxyAuthenticate); _next = 25; return true; } - HeaderServer: // case 25 + HeaderRetryAfter: // case 25 if ((_bits & 0x2000000L) != 0) { - _current = new KeyValuePair(HeaderNames.Server, _collection._headers._Server); + _current = new KeyValuePair(HeaderNames.RetryAfter, _collection._headers._RetryAfter); _next = 26; return true; } - HeaderSetCookie: // case 26 + HeaderServer: // case 26 if ((_bits & 0x4000000L) != 0) { - _current = new KeyValuePair(HeaderNames.SetCookie, _collection._headers._SetCookie); + _current = new KeyValuePair(HeaderNames.Server, _collection._headers._Server); _next = 27; return true; } - HeaderVary: // case 27 + HeaderSetCookie: // case 27 if ((_bits & 0x8000000L) != 0) { - _current = new KeyValuePair(HeaderNames.Vary, _collection._headers._Vary); + _current = new KeyValuePair(HeaderNames.SetCookie, _collection._headers._SetCookie); _next = 28; return true; } - HeaderWWWAuthenticate: // case 28 + HeaderVary: // case 28 if ((_bits & 0x10000000L) != 0) { - _current = new KeyValuePair(HeaderNames.WWWAuthenticate, _collection._headers._WWWAuthenticate); + _current = new KeyValuePair(HeaderNames.Vary, _collection._headers._Vary); _next = 29; return true; } - HeaderAccessControlAllowCredentials: // case 29 + HeaderWWWAuthenticate: // case 29 if ((_bits & 0x20000000L) != 0) { - _current = new KeyValuePair(HeaderNames.AccessControlAllowCredentials, _collection._headers._AccessControlAllowCredentials); + _current = new KeyValuePair(HeaderNames.WWWAuthenticate, _collection._headers._WWWAuthenticate); _next = 30; return true; } - HeaderAccessControlAllowHeaders: // case 30 + HeaderAccessControlAllowCredentials: // case 30 if ((_bits & 0x40000000L) != 0) { - _current = new KeyValuePair(HeaderNames.AccessControlAllowHeaders, _collection._headers._AccessControlAllowHeaders); + _current = new KeyValuePair(HeaderNames.AccessControlAllowCredentials, _collection._headers._AccessControlAllowCredentials); _next = 31; return true; } - HeaderAccessControlAllowMethods: // case 31 + HeaderAccessControlAllowHeaders: // case 31 if ((_bits & 0x80000000L) != 0) { - _current = new KeyValuePair(HeaderNames.AccessControlAllowMethods, _collection._headers._AccessControlAllowMethods); + _current = new KeyValuePair(HeaderNames.AccessControlAllowHeaders, _collection._headers._AccessControlAllowHeaders); _next = 32; return true; } - HeaderAccessControlAllowOrigin: // case 32 + HeaderAccessControlAllowMethods: // case 32 if ((_bits & 0x100000000L) != 0) { - _current = new KeyValuePair(HeaderNames.AccessControlAllowOrigin, _collection._headers._AccessControlAllowOrigin); + _current = new KeyValuePair(HeaderNames.AccessControlAllowMethods, _collection._headers._AccessControlAllowMethods); _next = 33; return true; } - HeaderAccessControlExposeHeaders: // case 33 + HeaderAccessControlAllowOrigin: // case 33 if ((_bits & 0x200000000L) != 0) { - _current = new KeyValuePair(HeaderNames.AccessControlExposeHeaders, _collection._headers._AccessControlExposeHeaders); + _current = new KeyValuePair(HeaderNames.AccessControlAllowOrigin, _collection._headers._AccessControlAllowOrigin); _next = 34; return true; } - HeaderAccessControlMaxAge: // case 34 + HeaderAccessControlExposeHeaders: // case 34 if ((_bits & 0x400000000L) != 0) { - _current = new KeyValuePair(HeaderNames.AccessControlMaxAge, _collection._headers._AccessControlMaxAge); + _current = new KeyValuePair(HeaderNames.AccessControlExposeHeaders, _collection._headers._AccessControlExposeHeaders); _next = 35; return true; } - HeaderContentLength: // case 35 + HeaderAccessControlMaxAge: // case 35 + if ((_bits & 0x800000000L) != 0) + { + _current = new KeyValuePair(HeaderNames.AccessControlMaxAge, _collection._headers._AccessControlMaxAge); + _next = 36; + return true; + } + HeaderContentLength: // case 36 if (_collection._contentLength.HasValue) { _current = new KeyValuePair(HeaderNames.ContentLength, HeaderUtilities.FormatNonNegativeInt64(_collection._contentLength.Value)); - _next = 36; + _next = 37; return true; } ExtraHeaders: diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 9e181fd7cf..cc4377af1d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -155,6 +155,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { return HttpUtilities.Http2Version; } + if (_httpVersion == Http.HttpVersion.Http3) + { + return HttpUtilities.Http3Version; + } return string.Empty; } @@ -176,6 +180,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _httpVersion = Http.HttpVersion.Http2; } + else if (ReferenceEquals(value, HttpUtilities.Http3Version)) + { + _httpVersion = Http.HttpVersion.Http3; + } else { HttpVersionSetSlow(value); @@ -198,6 +206,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { _httpVersion = Http.HttpVersion.Http2; } + else if (value == HttpUtilities.Http3Version) + { + _httpVersion = Http.HttpVersion.Http3; + } else { _httpVersion = Http.HttpVersion.Unknown; @@ -1044,7 +1056,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private Task WriteSuffix() { - if (_autoChunk || _httpVersion == Http.HttpVersion.Http2) + if (_autoChunk || _httpVersion >= Http.HttpVersion.Http2) { // For the same reason we call CheckLastWrite() in Content-Length responses. PreventRequestAbortedCancellation(); @@ -1160,7 +1172,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http responseHeaders.SetReadOnly(); - if (!hasConnection && _httpVersion != Http.HttpVersion.Http2) + if (!hasConnection && _httpVersion < Http.HttpVersion.Http2) { if (!_keepAlive) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpVersion.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpVersion.cs index 832a1c5616..e6e2a0dabf 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpVersion.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpVersion.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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. namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http @@ -8,6 +8,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http Unknown = -1, Http10 = 0, Http11 = 1, - Http2 + Http2 = 2, + Http3 = 3 } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3ErrorCode.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3ErrorCode.cs new file mode 100644 index 0000000000..ac01a35477 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3ErrorCode.cs @@ -0,0 +1,101 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal enum Http3ErrorCode : uint + { + /// + /// HTTP_NO_ERROR (0x100): + /// No error. This is used when the connection or stream needs to be closed, but there is no error to signal. + /// + NoError = 0x100, + /// + /// HTTP_GENERAL_PROTOCOL_ERROR (0x101): + /// Peer violated protocol requirements in a way which doesn’t match a more specific error code, + /// or endpoint declines to use the more specific error code. + /// + ProtocolError = 0x101, + /// + /// HTTP_INTERNAL_ERROR (0x102): + /// An internal error has occurred in the HTTP stack. + /// + InternalError = 0x102, + /// + /// HTTP_STREAM_CREATION_ERROR (0x103): + /// The endpoint detected that its peer created a stream that it will not accept. + /// + StreamCreationError = 0x103, + /// + /// HTTP_CLOSED_CRITICAL_STREAM (0x104): + /// A stream required by the connection was closed or reset. + /// + ClosedCriticalStream = 0x104, + /// + /// HTTP_UNEXPECTED_FRAME (0x105): + /// A frame was received which was not permitted in the current state. + /// + UnexpectedFrame = 0x105, + /// + /// HTTP_FRAME_ERROR (0x106): + /// A frame that fails to satisfy layout requirements or with an invalid size was received. + /// + FrameError = 0x106, + /// + /// HTTP_EXCESSIVE_LOAD (0x107): + /// The endpoint detected that its peer is exhibiting a behavior that might be generating excessive load. + /// + ExcessiveLoad = 0x107, + /// + /// HTTP_WRONG_STREAM (0x108): + /// A frame was received on a stream where it is not permitted. + /// + WrongStream = 0x108, + /// + /// HTTP_ID_ERROR (0x109): + /// A Stream ID, Push ID, or Placeholder ID was used incorrectly, such as exceeding a limit, reducing a limit, or being reused. + /// + IdError = 0x109, + /// + /// HTTP_SETTINGS_ERROR (0x10A): + /// An endpoint detected an error in the payload of a SETTINGS frame: a duplicate setting was detected, + /// a client-only setting was sent by a server, or a server-only setting by a client. + /// + SettingsError = 0x10a, + /// + /// HTTP_MISSING_SETTINGS (0x10B): + /// No SETTINGS frame was received at the beginning of the control stream. + /// + MissingSettings = 0x10b, + /// + /// HTTP_REQUEST_REJECTED (0x10C): + /// A server rejected a request without performing any application processing. + /// + RequestRejected = 0x10c, + /// + /// HTTP_REQUEST_CANCELLED (0x10D): + /// The request or its response (including pushed response) is cancelled. + /// + RequestCancelled = 0x10d, + /// + /// HTTP_REQUEST_INCOMPLETE (0x10E): + /// The client’s stream terminated without containing a fully-formed request. + /// + RequestIncomplete = 0x10e, + /// + /// HTTP_EARLY_RESPONSE (0x10F): + /// The remainder of the client’s request is not needed to produce a response. For use in STOP_SENDING only. + /// + EarlyResponse = 0x10f, + /// + /// HTTP_CONNECT_ERROR (0x110): + /// The connection established in response to a CONNECT request was reset or abnormally closed. + /// + ConnectError = 0x110, + /// + /// HTTP_VERSION_FALLBACK (0x111): + /// The requested operation cannot be served over HTTP/3. The peer should retry over HTTP/1.1. + /// + VersionFallback = 0x111, + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Data.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Data.cs new file mode 100644 index 0000000000..536147b1fb --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Data.cs @@ -0,0 +1,14 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal partial class Http3Frame + { + public void PrepareData() + { + Length = 0; + Type = Http3FrameType.Data; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.GoAway.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.GoAway.cs new file mode 100644 index 0000000000..a1984d93fb --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.GoAway.cs @@ -0,0 +1,14 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal partial class Http3Frame + { + public void PrepareGoAway() + { + Length = 0; + Type = Http3FrameType.GoAway; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Headers.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Headers.cs new file mode 100644 index 0000000000..bc582d13bb --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Headers.cs @@ -0,0 +1,14 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal partial class Http3Frame + { + public void PrepareHeaders() + { + Length = 0; + Type = Http3FrameType.Headers; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Settings.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Settings.cs new file mode 100644 index 0000000000..1d06d5e403 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Settings.cs @@ -0,0 +1,14 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal partial class Http3Frame + { + public void PrepareSettings() + { + Length = 0; + Type = Http3FrameType.Settings; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.cs new file mode 100644 index 0000000000..a7dff3a0a1 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.cs @@ -0,0 +1,17 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal partial class Http3Frame + { + public long Length { get; set; } + + public Http3FrameType Type { get; internal set; } + + public override string ToString() + { + return $"{Type} Length: {Length}"; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3FrameType.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3FrameType.cs new file mode 100644 index 0000000000..c690567b06 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3FrameType.cs @@ -0,0 +1,17 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal enum Http3FrameType + { + Data = 0x0, + Headers = 0x1, + CancelPush = 0x3, + Settings = 0x4, + PushPromise = 0x5, + GoAway = 0x7, + MaxPushId = 0xD, + DuplicatePush = 0xE + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Helpers/VariableLengthIntegerHelper.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Helpers/VariableLengthIntegerHelper.cs new file mode 100644 index 0000000000..c63c908ab9 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Helpers/VariableLengthIntegerHelper.cs @@ -0,0 +1,115 @@ +// 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.Diagnostics; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + /// + /// Variable length integer encoding and decoding methods. Based on https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-16. + /// Either will take up 1, 2, 4, or 8 bytes. + /// + internal static class VariableLengthIntegerHelper + { + private const int TwoByteSubtract = 0x4000; + private const uint FourByteSubtract = 0x80000000; + private const ulong EightByteSubtract = 0xC000000000000000; + private const int OneByteLimit = 64; + private const int TwoByteLimit = 16383; + private const int FourByteLimit = 1073741823; + + public static long GetInteger(in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + { + consumed = buffer.Start; + examined = buffer.End; + + if (buffer.Length == 0) + { + return -1; + } + + // The first two bits of the first byte represent the length of the + // variable length integer + // 00 = length 1 + // 01 = length 2 + // 10 = length 4 + // 11 = length 8 + + var span = buffer.Slice(0, Math.Min(buffer.Length, 8)).ToSpan(); + + var firstByte = span[0]; + + if ((firstByte & 0xC0) == 0) + { + consumed = examined = buffer.GetPosition(1); + return firstByte & 0x3F; + } + else if ((firstByte & 0xC0) == 0x40) + { + if (span.Length < 2) + { + return -1; + } + + consumed = examined = buffer.GetPosition(2); + + return BinaryPrimitives.ReadUInt16BigEndian(span) - TwoByteSubtract; + } + else if ((firstByte & 0xC0) == 0x80) + { + if (span.Length < 4) + { + return -1; + } + + consumed = examined = buffer.GetPosition(4); + + return BinaryPrimitives.ReadUInt32BigEndian(span) - FourByteSubtract; + } + else + { + if (span.Length < 8) + { + return -1; + } + + consumed = examined = buffer.GetPosition(8); + + return (long)(BinaryPrimitives.ReadUInt64BigEndian(span) - EightByteSubtract); + } + } + + public static int WriteInteger(Span buffer, long longToEncode) + { + Debug.Assert(buffer.Length >= 8); + Debug.Assert(longToEncode < long.MaxValue / 2); + + if (longToEncode < OneByteLimit) + { + buffer[0] = (byte)longToEncode; + return 1; + } + else if (longToEncode < TwoByteLimit) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)longToEncode); + buffer[0] += 0x40; + return 2; + } + else if (longToEncode < FourByteLimit) + { + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)longToEncode); + buffer[0] += 0x80; + return 4; + } + else + { + BinaryPrimitives.WriteUInt64BigEndian(buffer, (ulong)longToEncode); + buffer[0] += 0xC0; + return 8; + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs new file mode 100644 index 0000000000..ca0ac6bc49 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -0,0 +1,206 @@ +// 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.Concurrent; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Abstractions.Features; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal class Http3Connection : IRequestProcessor + { + public HttpConnectionContext Context { get; private set; } + + public DynamicTable DynamicTable { get; set; } + + public Http3ControlStream SettingsStream { get; set; } + public Http3ControlStream EncoderStream { get; set; } + public Http3ControlStream DecoderStream { get; set; } + + private readonly ConcurrentDictionary _streams = new ConcurrentDictionary(); + + // To be used by GO_AWAY + private long _highestOpenedStreamId; // TODO lock to access + //private volatile bool _haveSentGoAway; + + public Http3Connection(HttpConnectionContext context) + { + Context = context; + DynamicTable = new DynamicTable(0); + } + + internal long HighestStreamId + { + get + { + return _highestOpenedStreamId; + } + set + { + if (_highestOpenedStreamId < value) + { + _highestOpenedStreamId = value; + } + } + } + + public async Task ProcessRequestsAsync(IHttpApplication application) + { + var streamListenerFeature = Context.ConnectionFeatures.Get(); + + // Start other three unidirectional streams here. + var settingsStream = CreateSettingsStream(application); + var encoderStream = CreateEncoderStream(application); + var decoderStream = CreateDecoderStream(application); + + try + { + while (true) + { + var connectionContext = await streamListenerFeature.AcceptAsync(); + if (connectionContext == null) + { + break; + } + + //if (_haveSentGoAway) + //{ + // // error here. + //} + + var httpConnectionContext = new HttpConnectionContext + { + ConnectionId = connectionContext.ConnectionId, + ConnectionContext = connectionContext, + Protocols = Context.Protocols, + ServiceContext = Context.ServiceContext, + ConnectionFeatures = connectionContext.Features, + MemoryPool = Context.MemoryPool, + Transport = connectionContext.Transport, + TimeoutControl = Context.TimeoutControl, + LocalEndPoint = connectionContext.LocalEndPoint as IPEndPoint, + RemoteEndPoint = connectionContext.RemoteEndPoint as IPEndPoint + }; + + var streamFeature = httpConnectionContext.ConnectionFeatures.Get(); + var streamId = streamFeature.StreamId; + HighestStreamId = streamId; + + if (streamFeature.IsUnidirectional) + { + var stream = new Http3ControlStream(application, this, httpConnectionContext); + ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false); + } + else + { + var http3Stream = new Http3Stream(application, this, httpConnectionContext); + var stream = http3Stream; + _streams[streamId] = http3Stream; + ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false); + } + } + } + finally + { + await settingsStream; + await encoderStream; + await decoderStream; + foreach (var stream in _streams.Values) + { + stream.Abort(new ConnectionAbortedException("")); + } + } + } + + private async ValueTask CreateSettingsStream(IHttpApplication application) + { + var stream = await CreateNewUnidirectionalStreamAsync(application); + SettingsStream = stream; + await stream.SendStreamIdAsync(id: 0); + await stream.SendSettingsFrameAsync(); + } + + private async ValueTask CreateEncoderStream(IHttpApplication application) + { + var stream = await CreateNewUnidirectionalStreamAsync(application); + EncoderStream = stream; + await stream.SendStreamIdAsync(id: 2); + } + + private async ValueTask CreateDecoderStream(IHttpApplication application) + { + var stream = await CreateNewUnidirectionalStreamAsync(application); + DecoderStream = stream; + await stream.SendStreamIdAsync(id: 3); + } + + private async ValueTask CreateNewUnidirectionalStreamAsync(IHttpApplication application) + { + var connectionContext = await Context.ConnectionFeatures.Get().StartUnidirectionalStreamAsync(); + var httpConnectionContext = new HttpConnectionContext + { + ConnectionId = connectionContext.ConnectionId, + ConnectionContext = connectionContext, + Protocols = Context.Protocols, + ServiceContext = Context.ServiceContext, + ConnectionFeatures = connectionContext.Features, + MemoryPool = Context.MemoryPool, + Transport = connectionContext.Transport, + TimeoutControl = Context.TimeoutControl, + LocalEndPoint = connectionContext.LocalEndPoint as IPEndPoint, + RemoteEndPoint = connectionContext.RemoteEndPoint as IPEndPoint + }; + + return new Http3ControlStream(application, this, httpConnectionContext); + } + + public void StopProcessingNextRequest() + { + } + + public void HandleRequestHeadersTimeout() + { + } + + public void HandleReadDataRateTimeout() + { + } + + public void OnInputOrOutputCompleted() + { + } + + public void Tick(DateTimeOffset now) + { + } + + public void Abort(ConnectionAbortedException ex) + { + // Send goaway + // Abort currently active streams + // TODO need to figure out if there is server initiated connection close rather than stream close? + } + + public void ApplyMaxHeaderListSize(long value) + { + // TODO something here to call OnHeader? + } + + internal void ApplyBlockedStream(long value) + { + } + + internal void ApplyMaxTableCapacity(long value) + { + // TODO make sure this works + //_maxDynamicTableSize = value; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionException.cs new file mode 100644 index 0000000000..c410c34a3f --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionException.cs @@ -0,0 +1,28 @@ +// 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.Runtime.Serialization; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal +{ + [Serializable] + internal class Http3ConnectionException : Exception + { + public Http3ConnectionException() + { + } + + public Http3ConnectionException(string message) : base(message) + { + } + + public Http3ConnectionException(string message, Exception innerException) : base(message, innerException) + { + } + + protected Http3ConnectionException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs new file mode 100644 index 0000000000..8163d65161 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs @@ -0,0 +1,388 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal abstract class Http3ControlStream : IThreadPoolWorkItem + { + private const int ControlStream = 0; + private const int EncoderStream = 2; + private const int DecoderStream = 3; + private Http3FrameWriter _frameWriter; + private readonly Http3Connection _http3Connection; + private HttpConnectionContext _context; + private readonly Http3Frame _incomingFrame = new Http3Frame(); + private volatile int _isClosed; + private int _gracefulCloseInitiator; + + private bool _haveReceivedSettingsFrame; + + public Http3ControlStream(Http3Connection http3Connection, HttpConnectionContext context) + { + var httpLimits = context.ServiceContext.ServerOptions.Limits; + + _http3Connection = http3Connection; + _context = context; + + _frameWriter = new Http3FrameWriter( + context.Transport.Output, + context.ConnectionContext, + context.TimeoutControl, + httpLimits.MinResponseDataRate, + context.ConnectionId, + context.MemoryPool, + context.ServiceContext.Log); + } + + private void OnStreamClosed() + { + Abort(new ConnectionAbortedException("HTTP_CLOSED_CRITICAL_STREAM")); + } + + public PipeReader Input => _context.Transport.Input; + + + public void Abort(ConnectionAbortedException ex) + { + } + + public void HandleReadDataRateTimeout() + { + //Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond); + Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout)); + } + + public void HandleRequestHeadersTimeout() + { + //Log.ConnectionBadRequest(ConnectionId, BadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); + Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout)); + } + + public void OnInputOrOutputCompleted() + { + TryClose(); + _frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); + } + + private bool TryClose() + { + if (Interlocked.Exchange(ref _isClosed, 1) == 0) + { + return true; + } + + // TODO make this actually close the Http3Stream by telling msquic to close the stream. + return false; + } + + private async ValueTask HandleEncodingTask() + { + var encoder = new EncoderStreamReader(10000); // TODO get value from limits + while (_isClosed == 0) + { + var result = await Input.ReadAsync(); + var readableBuffer = result.Buffer; + if (!readableBuffer.IsEmpty) + { + // This should always read all bytes in the input no matter what. + encoder.Read(readableBuffer); + } + Input.AdvanceTo(readableBuffer.End); + } + } + + private async ValueTask HandleDecodingTask() + { + var decoder = new DecoderStreamReader(); + while (_isClosed == 0) + { + var result = await Input.ReadAsync(); + var readableBuffer = result.Buffer; + var consumed = readableBuffer.Start; + var examined = readableBuffer.Start; + if (!readableBuffer.IsEmpty) + { + decoder.Read(readableBuffer); + } + Input.AdvanceTo(readableBuffer.End); + } + } + + internal async ValueTask SendStreamIdAsync(int id) + { + await _frameWriter.WriteStreamIdAsync(id); + } + + internal async ValueTask SendSettingsFrameAsync() + { + await _frameWriter.WriteSettingsAsync(null); + } + + private async ValueTask TryReadStreamIdAsync() + { + while (_isClosed == 0) + { + var result = await Input.ReadAsync(); + var readableBuffer = result.Buffer; + var consumed = readableBuffer.Start; + var examined = readableBuffer.End; + + try + { + if (!readableBuffer.IsEmpty) + { + var id = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined); + if (id != -1) + { + return id; + } + } + + if (result.IsCompleted) + { + return -1; + } + } + finally + { + Input.AdvanceTo(consumed, examined); + } + } + + return -1; + } + + public async Task ProcessRequestAsync(IHttpApplication application) + { + var streamType = await TryReadStreamIdAsync(); + + if (streamType == -1) + { + return; + } + + if (streamType == ControlStream) + { + if (_http3Connection.SettingsStream != null) + { + throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR"); + } + + await HandleControlStream(); + } + else if (streamType == EncoderStream) + { + if (_http3Connection.EncoderStream != null) + { + throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR"); + } + await HandleEncodingTask(); + return; + } + else if (streamType == DecoderStream) + { + if (_http3Connection.DecoderStream != null) + { + throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR"); + } + await HandleDecodingTask(); + } + else + { + // TODO Close the control stream as it's unexpected. + } + return; + } + + private async Task HandleControlStream() + { + while (_isClosed == 0) + { + var result = await Input.ReadAsync(); + var readableBuffer = result.Buffer; + var consumed = readableBuffer.Start; + var examined = readableBuffer.End; + + try + { + if (!readableBuffer.IsEmpty) + { + // need to kick off httpprotocol process request async here. + while (Http3FrameReader.TryReadFrame(ref readableBuffer, _incomingFrame, 16 * 1024, out var framePayload)) + { + //Log.Http2FrameReceived(ConnectionId, _incomingFrame); + consumed = examined = framePayload.End; + await ProcessHttp3ControlStream(framePayload); + } + } + + if (result.IsCompleted) + { + return; + } + } + catch (Http3StreamErrorException) + { + } + finally + { + Input.AdvanceTo(consumed, examined); + } + } + } + + private ValueTask ProcessHttp3ControlStream(in ReadOnlySequence payload) + { + // Two things: + // settings must be sent as the first frame of each control stream by each peer + // Can't send more than two settings frames. + switch (_incomingFrame.Type) + { + case Http3FrameType.Data: + case Http3FrameType.Headers: + case Http3FrameType.DuplicatePush: + case Http3FrameType.PushPromise: + throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + case Http3FrameType.Settings: + return ProcessSettingsFrameAsync(payload); + case Http3FrameType.GoAway: + return ProcessGoAwayFrameAsync(); + case Http3FrameType.CancelPush: + return ProcessCancelPushFrameAsync(); + case Http3FrameType.MaxPushId: + return ProcessMaxPushIdFrameAsync(); + default: + return ProcessUnknownFrameAsync(); + } + } + + private ValueTask ProcessSettingsFrameAsync(ReadOnlySequence payload) + { + if (_haveReceivedSettingsFrame) + { + throw new Http3ConnectionException("H3_SETTINGS_ERROR"); + } + + _haveReceivedSettingsFrame = true; + using var closedRegistration = _context.ConnectionContext.ConnectionClosed.Register(state => ((Http3ControlStream)state).OnStreamClosed(), this); + + while (true) + { + var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out var examinded); + if (id == -1) + { + break; + } + + payload = payload.Slice(consumed); + + var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out examinded); + if (id == -1) + { + break; + } + + payload = payload.Slice(consumed); + ProcessSetting(id, value); + } + + return default; + } + + private void ProcessSetting(long id, long value) + { + // These are client settings, for outbound traffic. + switch (id) + { + case (long)Http3SettingType.QPackMaxTableCapacity: + _http3Connection.ApplyMaxTableCapacity(value); + break; + case (long)Http3SettingType.MaxHeaderListSize: + _http3Connection.ApplyMaxHeaderListSize(value); + break; + case (long)Http3SettingType.QPackBlockedStreams: + _http3Connection.ApplyBlockedStream(value); + break; + default: + // Ignore all unknown settings. + break; + } + } + + private ValueTask ProcessGoAwayFrameAsync() + { + if (!_haveReceivedSettingsFrame) + { + throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + } + // Get highest stream id and write that to the response. + return default; + } + + private ValueTask ProcessCancelPushFrameAsync() + { + if (!_haveReceivedSettingsFrame) + { + throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + } + // This should just noop. + return default; + } + + private ValueTask ProcessMaxPushIdFrameAsync() + { + if (!_haveReceivedSettingsFrame) + { + throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + } + return default; + } + + private ValueTask ProcessUnknownFrameAsync() + { + if (!_haveReceivedSettingsFrame) + { + throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + } + return default; + } + + public void StopProcessingNextRequest() + => StopProcessingNextRequest(serverInitiated: true); + + public void StopProcessingNextRequest(bool serverInitiated) + { + var initiator = serverInitiated ? GracefulCloseInitiator.Server : GracefulCloseInitiator.Client; + + if (Interlocked.CompareExchange(ref _gracefulCloseInitiator, initiator, GracefulCloseInitiator.None) == GracefulCloseInitiator.None) + { + Input.CancelPendingRead(); + } + } + + public void Tick(DateTimeOffset now) + { + } + + /// + /// Used to kick off the request processing loop by derived classes. + /// + public abstract void Execute(); + + private static class GracefulCloseInitiator + { + public const int None = 0; + public const int Server = 1; + public const int Client = 2; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs new file mode 100644 index 0000000000..880f0f4c1b --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs @@ -0,0 +1,26 @@ +// 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 Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Abstractions; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal sealed class Http3ControlStream : Http3ControlStream, IHostContextContainer + { + private readonly IHttpApplication _application; + + public Http3ControlStream(IHttpApplication application, Http3Connection connection, HttpConnectionContext context) : base(connection, context) + { + _application = application; + } + + public override void Execute() + { + _ = ProcessRequestAsync(_application); + } + + // Pooled Host context + TContext IHostContextContainer.HostContext { get; set; } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs new file mode 100644 index 0000000000..020006ae27 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs @@ -0,0 +1,59 @@ +// 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.Buffers; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal class Http3FrameReader + { + /* https://quicwg.org/base-drafts/draft-ietf-quic-http.html#frame-layout + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type (i) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Length (i) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Frame Payload (*) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + internal static bool TryReadFrame(ref ReadOnlySequence readableBuffer, Http3Frame frame, uint maxFrameSize, out ReadOnlySequence framePayload) + { + framePayload = ReadOnlySequence.Empty; + var consumed = readableBuffer.Start; + var examined = readableBuffer.Start; + + var type = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined); + if (type == -1) + { + return false; + } + + var firstLengthBuffer = readableBuffer.Slice(consumed); + + var length = VariableLengthIntegerHelper.GetInteger(firstLengthBuffer, out consumed, out examined); + + // Make sure the whole frame is buffered + if (length == -1) + { + return false; + } + + var startOfFramePayload = readableBuffer.Slice(consumed); + if (startOfFramePayload.Length < length) + { + return false; + } + + frame.Length = length; + frame.Type = (Http3FrameType)type; + + // The remaining payload minus the extra fields + framePayload = startOfFramePayload.Slice(0, length); + readableBuffer = readableBuffer.Slice(framePayload.End); + + return true; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs new file mode 100644 index 0000000000..f7905fdd76 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs @@ -0,0 +1,326 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Threading; +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.Http3.QPack; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWriterHelpers; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal class Http3FrameWriter + { + private readonly object _writeLock = new object(); + private readonly QPackEncoder _qpackEncoder = new QPackEncoder(); + + private readonly PipeWriter _outputWriter; + private readonly ConnectionContext _connectionContext; + private readonly ITimeoutControl _timeoutControl; + private readonly MinDataRate _minResponseDataRate; + private readonly MemoryPool _memoryPool; + private readonly IKestrelTrace _log; + private readonly Http3Frame _outgoingFrame; + private readonly TimingPipeFlusher _flusher; + + // TODO update max frame size + private uint _maxFrameSize = 10000; //Http3PeerSettings.MinAllowedMaxFrameSize; + private byte[] _headerEncodingBuffer; + + private long _unflushedBytes; + private bool _completed; + private bool _aborted; + + //private int _unflushedBytes; + + public Http3FrameWriter(PipeWriter output, ConnectionContext connectionContext, ITimeoutControl timeoutControl, MinDataRate minResponseDataRate, string connectionId, MemoryPool memoryPool, IKestrelTrace log) + { + _outputWriter = output; + _connectionContext = connectionContext; + _timeoutControl = timeoutControl; + _minResponseDataRate = minResponseDataRate; + _memoryPool = memoryPool; + _log = log; + _outgoingFrame = new Http3Frame(); + _flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log); + _headerEncodingBuffer = new byte[_maxFrameSize]; + } + + public void UpdateMaxFrameSize(uint maxFrameSize) + { + lock (_writeLock) + { + if (_maxFrameSize != maxFrameSize) + { + _maxFrameSize = maxFrameSize; + _headerEncodingBuffer = new byte[_maxFrameSize]; + } + } + } + + // TODO actually write settings here. + internal Task WriteSettingsAsync(IList settings) + { + _outgoingFrame.PrepareSettings(); + var buffer = _outputWriter.GetSpan(2); + + buffer[0] = (byte)_outgoingFrame.Type; + buffer[1] = 0; + + _outputWriter.Advance(2); + + return _outputWriter.FlushAsync().AsTask(); + } + + internal Task WriteStreamIdAsync(long id) + { + var buffer = _outputWriter.GetSpan(8); + _outputWriter.Advance(VariableLengthIntegerHelper.WriteInteger(buffer, id)); + return _outputWriter.FlushAsync().AsTask(); + } + + public ValueTask WriteDataAsync(in ReadOnlySequence data) + { + // The Length property of a ReadOnlySequence can be expensive, so we cache the value. + var dataLength = data.Length; + + lock (_writeLock) + { + if (_completed) + { + return default; + } + + WriteDataUnsynchronized(data, dataLength); + return TimeFlushUnsynchronizedAsync(); + } + } + + private void WriteDataUnsynchronized(in ReadOnlySequence data, long dataLength) + { + Debug.Assert(dataLength == data.Length); + + _outgoingFrame.PrepareData(); + + if (dataLength > _maxFrameSize) + { + SplitAndWriteDataUnsynchronized(in data, dataLength); + return; + } + + _outgoingFrame.Length = (int)dataLength; + + WriteHeaderUnsynchronized(); + + foreach (var buffer in data) + { + _outputWriter.Write(buffer.Span); + } + + return; + + void SplitAndWriteDataUnsynchronized(in ReadOnlySequence data, long dataLength) + { + Debug.Assert(dataLength == data.Length); + + var dataPayloadLength = (int)_maxFrameSize; + + Debug.Assert(dataLength > dataPayloadLength); + + var remainingData = data; + do + { + var currentData = remainingData.Slice(0, dataPayloadLength); + _outgoingFrame.Length = dataPayloadLength; + + WriteHeaderUnsynchronized(); + + foreach (var buffer in currentData) + { + _outputWriter.Write(buffer.Span); + } + + dataLength -= dataPayloadLength; + remainingData = remainingData.Slice(dataPayloadLength); + + } while (dataLength > dataPayloadLength); + + _outgoingFrame.Length = (int)dataLength; + + WriteHeaderUnsynchronized(); + + foreach (var buffer in remainingData) + { + _outputWriter.Write(buffer.Span); + } + } + } + + private void WriteHeaderUnsynchronized() + { + var headerLength = WriteHeader(_outgoingFrame, _outputWriter); + + // We assume the payload will be written prior to the next flush. + _unflushedBytes += headerLength + _outgoingFrame.Length; + } + + internal static int WriteHeader(Http3Frame frame, PipeWriter output) + { + // max size of the header is 16, most likely it will be smaller. + var buffer = output.GetSpan(16); + + var typeLength = VariableLengthIntegerHelper.WriteInteger(buffer, (int)frame.Type); + + buffer = buffer.Slice(typeLength); + + var lengthLength = VariableLengthIntegerHelper.WriteInteger(buffer, (int)frame.Length); + + var totalLength = typeLength + lengthLength; + output.Advance(typeLength + lengthLength); + + return totalLength; + } + + public ValueTask WriteResponseTrailers(int streamId, HttpResponseTrailers headers) + { + lock (_writeLock) + { + if (_completed) + { + return default; + } + + try + { + _outgoingFrame.PrepareHeaders(); + var buffer = _headerEncodingBuffer.AsSpan(); + var done = _qpackEncoder.BeginEncode(EnumerateHeaders(headers), buffer, out var payloadLength); + FinishWritingHeaders(streamId, payloadLength, done); + } + catch (QPackEncodingException) + { + //_log.HPackEncodingError(_connectionId, streamId, hex); + //_http3Stream.Abort(new ConnectionAbortedException(hex.Message, hex)); + } + + return TimeFlushUnsynchronizedAsync(); + } + } + + private ValueTask TimeFlushUnsynchronizedAsync() + { + var bytesWritten = _unflushedBytes; + _unflushedBytes = 0; + + return _flusher.FlushAsync(_minResponseDataRate, bytesWritten); + } + + public ValueTask FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken) + { + lock (_writeLock) + { + if (_completed) + { + return default; + } + + var bytesWritten = _unflushedBytes; + _unflushedBytes = 0; + + return _flusher.FlushAsync(_minResponseDataRate, bytesWritten, outputAborter, cancellationToken); + } + } + + internal void WriteResponseHeaders(int streamId, int statusCode, IHeaderDictionary headers) + { + lock (_writeLock) + { + if (_completed) + { + return; + } + + try + { + _outgoingFrame.PrepareHeaders(); + var buffer = _headerEncodingBuffer.AsSpan(); + var done = _qpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), buffer, out var payloadLength); + FinishWritingHeaders(streamId, payloadLength, done); + } + catch (QPackEncodingException hex) + { + // TODO figure out how to abort the stream here. + //_http3Stream.Abort(new ConnectionAbortedException(hex.Message, hex)); + throw new InvalidOperationException(hex.Message, hex); // Report the error to the user if this was the first write. + } + } + } + + private void FinishWritingHeaders(int streamId, int payloadLength, bool done) + { + var buffer = _headerEncodingBuffer.AsSpan(); + _outgoingFrame.Length = payloadLength; + + WriteHeaderUnsynchronized(); + _outputWriter.Write(buffer.Slice(0, payloadLength)); + + while (!done) + { + done = _qpackEncoder.Encode(buffer, out payloadLength); + _outgoingFrame.Length = payloadLength; + + WriteHeaderUnsynchronized(); + _outputWriter.Write(buffer.Slice(0, payloadLength)); + } + } + + public void Complete() + { + lock (_writeLock) + { + if (_completed) + { + return; + } + + _completed = true; + _outputWriter.Complete(); + } + } + + public void Abort(ConnectionAbortedException error) + { + lock (_writeLock) + { + if (_aborted) + { + return; + } + + _aborted = true; + _connectionContext.Abort(error); + + Complete(); + } + } + + private static IEnumerable> EnumerateHeaders(IHeaderDictionary headers) + { + foreach (var header in headers) + { + foreach (var value in header.Value) + { + yield return new KeyValuePair(header.Key, value); + } + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3MessageBody.cs new file mode 100644 index 0000000000..5af5f8df47 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3MessageBody.cs @@ -0,0 +1,126 @@ +// 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.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal sealed class Http3MessageBody : MessageBody + { + private readonly Http3Stream _context; + private ReadResult _readResult; + + private Http3MessageBody(Http3Stream context) + : base(context) + { + _context = context; + } + protected override void OnReadStarting() + { + // Note ContentLength or MaxRequestBodySize may be null + if (_context.RequestHeaders.ContentLength > _context.MaxRequestBodySize) + { + BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); + } + } + + protected override void OnReadStarted() + { + } + + protected override void OnDataRead(long bytesRead) + { + AddAndCheckConsumedBytes(bytesRead); + } + + public static MessageBody For(Http3Stream context) + { + return new Http3MessageBody(context); + } + + public override void AdvanceTo(SequencePosition consumed) + { + AdvanceTo(consumed, consumed); + } + + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + OnAdvance(_readResult, consumed, examined); + _context.RequestBodyPipe.Reader.AdvanceTo(consumed, examined); + } + + public override bool TryRead(out ReadResult readResult) + { + TryStart(); + + var hasResult = _context.RequestBodyPipe.Reader.TryRead(out readResult); + + if (hasResult) + { + _readResult = readResult; + + CountBytesRead(readResult.Buffer.Length); + + if (readResult.IsCompleted) + { + TryStop(); + } + } + + return hasResult; + } + + public override async ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + TryStart(); + + try + { + var readAwaitable = _context.RequestBodyPipe.Reader.ReadAsync(cancellationToken); + + _readResult = await StartTimingReadAsync(readAwaitable, cancellationToken); + } + catch (ConnectionAbortedException ex) + { + throw new TaskCanceledException("The request was aborted", ex); + } + + StopTimingRead(_readResult.Buffer.Length); + + if (_readResult.IsCompleted) + { + TryStop(); + } + + return _readResult; + } + + public override void Complete(Exception exception) + { + _context.RequestBodyPipe.Reader.Complete(); + _context.ReportApplicationError(exception); + } + + public override void CancelPendingRead() + { + _context.RequestBodyPipe.Reader.CancelPendingRead(); + } + + protected override Task OnStopAsync() + { + if (!_context.HasStartedConsumingRequestBody) + { + return Task.CompletedTask; + } + + _context.RequestBodyPipe.Reader.Complete(); + + return Task.CompletedTask; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs new file mode 100644 index 0000000000..bb94b99de1 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs @@ -0,0 +1,406 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWriterHelpers; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using Microsoft.AspNetCore.Internal; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal class Http3OutputProducer : IHttpOutputProducer, IHttpOutputAborter + { + private readonly int _streamId; + private readonly Http3FrameWriter _frameWriter; + private readonly TimingPipeFlusher _flusher; + private readonly IKestrelTrace _log; + private readonly MemoryPool _memoryPool; + private readonly Http3Stream _stream; + private readonly PipeWriter _pipeWriter; + private readonly PipeReader _pipeReader; + private readonly object _dataWriterLock = new object(); + private readonly ValueTask _dataWriteProcessingTask; + private bool _startedWritingDataFrames; + private bool _completed; + private bool _disposed; + private bool _suffixSent; + private IMemoryOwner _fakeMemoryOwner; + + public Http3OutputProducer( + int streamId, + Http3FrameWriter frameWriter, + MemoryPool pool, + Http3Stream stream, + IKestrelTrace log) + { + _streamId = streamId; + _frameWriter = frameWriter; + _memoryPool = pool; + _stream = stream; + _log = log; + + var pipe = CreateDataPipe(pool); + + _pipeWriter = pipe.Writer; + _pipeReader = pipe.Reader; + + _flusher = new TimingPipeFlusher(_pipeWriter, timeoutControl: null, log); + _dataWriteProcessingTask = ProcessDataWrites(); + } + + public void Dispose() + { + lock (_dataWriterLock) + { + if (_disposed) + { + return; + } + + _disposed = true; + + Stop(); + + if (_fakeMemoryOwner != null) + { + _fakeMemoryOwner.Dispose(); + _fakeMemoryOwner = null; + } + } + } + + void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason) + { + _stream.Abort(abortReason, Http3ErrorCode.InternalError); + } + + public void Advance(int bytes) + { + lock (_dataWriterLock) + { + ThrowIfSuffixSent(); + + if (_completed) + { + return; + } + + _startedWritingDataFrames = true; + + _pipeWriter.Advance(bytes); + } + } + + public void CancelPendingFlush() + { + lock (_dataWriterLock) + { + if (_completed) + { + return; + } + + _pipeWriter.CancelPendingFlush(); + } + } + + public ValueTask FirstWriteAsync(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan data, CancellationToken cancellationToken) + { + lock (_dataWriterLock) + { + WriteResponseHeaders(statusCode, reasonPhrase, responseHeaders, autoChunk, appCompleted: false); + + return WriteDataToPipeAsync(data, cancellationToken); + } + } + + public ValueTask FirstWriteChunkedAsync(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, ReadOnlySpan data, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public ValueTask FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + lock (_dataWriterLock) + { + ThrowIfSuffixSent(); + + if (_completed) + { + return default; + } + + if (_startedWritingDataFrames) + { + // If there's already been response data written to the stream, just wait for that. Any header + // should be in front of the data frames in the connection pipe. Trailers could change things. + return _flusher.FlushAsync(this, cancellationToken); + } + else + { + // Flushing the connection pipe ensures headers already in the pipe are flushed even if no data + // frames have been written. + return _frameWriter.FlushAsync(this, cancellationToken); + } + } + } + + public Memory GetMemory(int sizeHint = 0) + { + lock (_dataWriterLock) + { + ThrowIfSuffixSent(); + + if (_completed) + { + return GetFakeMemory(sizeHint); + } + + return _pipeWriter.GetMemory(sizeHint); + } + } + + + public Span GetSpan(int sizeHint = 0) + { + lock (_dataWriterLock) + { + ThrowIfSuffixSent(); + + if (_completed) + { + return GetFakeMemory(sizeHint).Span; + } + + return _pipeWriter.GetSpan(sizeHint); + } + } + + private Memory GetFakeMemory(int sizeHint) + { + if (_fakeMemoryOwner == null) + { + _fakeMemoryOwner = _memoryPool.Rent(sizeHint); + } + + return _fakeMemoryOwner.Memory; + } + + [StackTraceHidden] + private void ThrowIfSuffixSent() + { + if (_suffixSent) + { + ThrowSuffixSent(); + } + } + + [StackTraceHidden] + private static void ThrowSuffixSent() + { + throw new InvalidOperationException("Writing is not allowed after writer was completed."); + } + + public void Reset() + { + } + + public void Stop() + { + lock (_dataWriterLock) + { + if (_completed) + { + return; + } + + _completed = true; + + _pipeWriter.Complete(new OperationCanceledException()); + + } + } + + public ValueTask Write100ContinueAsync() + { + throw new NotImplementedException(); + } + + public ValueTask WriteChunkAsync(ReadOnlySpan data, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task WriteDataAsync(ReadOnlySpan data, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + lock (_dataWriterLock) + { + ThrowIfSuffixSent(); + + // This length check is important because we don't want to set _startedWritingDataFrames unless a data + // frame will actually be written causing the headers to be flushed. + if (_completed || data.Length == 0) + { + return Task.CompletedTask; + } + + _startedWritingDataFrames = true; + + _pipeWriter.Write(data); + return _flusher.FlushAsync(this, cancellationToken).GetAsTask(); + } + } + + public ValueTask WriteDataToPipeAsync(ReadOnlySpan data, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + lock (_dataWriterLock) + { + ThrowIfSuffixSent(); + + // This length check is important because we don't want to set _startedWritingDataFrames unless a data + // frame will actually be written causing the headers to be flushed. + if (_completed || data.Length == 0) + { + return default; + } + + _startedWritingDataFrames = true; + + _pipeWriter.Write(data); + return _flusher.FlushAsync(this, cancellationToken); + } + } + + public void WriteResponseHeaders(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders, bool autoChunk, bool appCompleted) + { + lock (_dataWriterLock) + { + if (_completed) + { + return; + } + + if (appCompleted && !_startedWritingDataFrames && (_stream.ResponseTrailers == null || _stream.ResponseTrailers.Count == 0)) + { + // TODO figure out something to do here. + } + + _frameWriter.WriteResponseHeaders(_streamId, statusCode, responseHeaders); + } + } + + public ValueTask WriteStreamSuffixAsync() + { + lock (_dataWriterLock) + { + if (_completed) + { + return _dataWriteProcessingTask; + } + + _completed = true; + _suffixSent = true; + + _pipeWriter.Complete(); + return _dataWriteProcessingTask; + } + } + + private async ValueTask ProcessDataWrites() + { + FlushResult flushResult = default; + try + { + ReadResult readResult; + + do + { + readResult = await _pipeReader.ReadAsync(); + + if (readResult.IsCompleted && _stream.ResponseTrailers?.Count > 0) + { + // Output is ending and there are trailers to write + // Write any remaining content then write trailers + if (readResult.Buffer.Length > 0) + { + flushResult = await _frameWriter.WriteDataAsync(readResult.Buffer); + } + + _stream.ResponseTrailers.SetReadOnly(); + flushResult = await _frameWriter.WriteResponseTrailers(_streamId, _stream.ResponseTrailers); + } + else if (readResult.IsCompleted) + { + if (readResult.Buffer.Length != 0) + { + ThrowUnexpectedState(); + } + + // Headers have already been written and there is no other content to write + // TODO complete something here. + flushResult = await _frameWriter.FlushAsync(outputAborter: null, cancellationToken: default); + _frameWriter.Complete(); + } + else + { + flushResult = await _frameWriter.WriteDataAsync(readResult.Buffer); + } + + _pipeReader.AdvanceTo(readResult.Buffer.End); + } while (!readResult.IsCompleted); + } + catch (OperationCanceledException) + { + // Writes should not throw for aborted streams/connections. + } + catch (Exception ex) + { + _log.LogCritical(ex, nameof(Http3OutputProducer) + "." + nameof(ProcessDataWrites) + " observed an unexpected exception."); + } + + _pipeReader.Complete(); + + return flushResult; + + static void ThrowUnexpectedState() + { + throw new InvalidOperationException(nameof(Http3OutputProducer) + "." + nameof(ProcessDataWrites) + " observed an unexpected state where the streams output ended with data still remaining in the pipe."); + } + } + + private static Pipe CreateDataPipe(MemoryPool pool) + => new Pipe(new PipeOptions + ( + pool: pool, + readerScheduler: PipeScheduler.Inline, + writerScheduler: PipeScheduler.ThreadPool, + pauseWriterThreshold: 1, + resumeWriterThreshold: 1, + useSynchronizationContext: false, + minimumSegmentSize: pool.GetMinimumSegmentSize() + )); + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs new file mode 100644 index 0000000000..4a2961d7ec --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3PeerSettings.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal class Http3PeerSettings + { + internal const uint DefaultMaxFrameSize = 16 * 1024; + + public static int MinAllowedMaxFrameSize { get; internal set; } = 16 * 1024; + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs new file mode 100644 index 0000000000..325ac0fd91 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3SettingType.cs @@ -0,0 +1,15 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + enum Http3SettingType : long + { + QPackMaxTableCapacity = 0x1, + /// + /// SETTINGS_MAX_HEADER_LIST_SIZE, default is unlimited. + /// + MaxHeaderListSize = 0x6, + QPackBlockedStreams = 0x7 + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs new file mode 100644 index 0000000000..e858ddac06 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -0,0 +1,471 @@ +// 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.Diagnostics; +using System.IO.Pipelines; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + internal abstract class Http3Stream : HttpProtocol, IHttpHeadersHandler, IThreadPoolWorkItem + { + private Http3FrameWriter _frameWriter; + private Http3OutputProducer _http3Output; + private int _isClosed; + private int _gracefulCloseInitiator; + private readonly HttpConnectionContext _context; + private readonly Http3Frame _incomingFrame = new Http3Frame(); + + private readonly Http3Connection _http3Connection; + private bool _receivedHeaders; + public Pipe RequestBodyPipe { get; } + + public Http3Stream(Http3Connection http3Connection, HttpConnectionContext context) : base(context) + { + // First, determine how we know if an Http3stream is unidirectional or bidirectional + var httpLimits = context.ServiceContext.ServerOptions.Limits; + var http3Limits = httpLimits.Http3; + _http3Connection = http3Connection; + _context = context; + + _frameWriter = new Http3FrameWriter( + context.Transport.Output, + context.ConnectionContext, + context.TimeoutControl, + httpLimits.MinResponseDataRate, + context.ConnectionId, + context.MemoryPool, + context.ServiceContext.Log); + + // ResponseHeaders aren't set, kind of ugly that we need to reset. + Reset(); + + _http3Output = new Http3OutputProducer( + 0, // TODO streamid + _frameWriter, + context.MemoryPool, + this, + context.ServiceContext.Log); + RequestBodyPipe = CreateRequestBodyPipe(64 * 1024); // windowSize? + Output = _http3Output; + } + + public QPackDecoder QPackDecoder { get; set; } = new QPackDecoder(10000, 10000); + + public PipeReader Input => _context.Transport.Input; + + public ISystemClock SystemClock => _context.ServiceContext.SystemClock; + public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits; + + public void Abort(ConnectionAbortedException ex) + { + Abort(ex, Http3ErrorCode.InternalError); + } + + public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode) + { + } + + public void OnHeadersComplete(bool endStream) + { + OnHeadersComplete(); + } + + public void HandleReadDataRateTimeout() + { + Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond); + Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout), Http3ErrorCode.RequestRejected); + } + + public void HandleRequestHeadersTimeout() + { + Log.ConnectionBadRequest(ConnectionId, BadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); + Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout), Http3ErrorCode.RequestRejected); + } + + public void OnInputOrOutputCompleted() + { + TryClose(); + _frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); + } + + private bool TryClose() + { + if (Interlocked.Exchange(ref _isClosed, 1) == 0) + { + return true; + } + + // TODO make this actually close the Http3Stream by telling msquic to close the stream. + return false; + } + + public async Task ProcessRequestAsync(IHttpApplication application) + { + try + { + + while (_isClosed == 0) + { + var result = await Input.ReadAsync(); + var readableBuffer = result.Buffer; + var consumed = readableBuffer.Start; + var examined = readableBuffer.End; + + try + { + if (!readableBuffer.IsEmpty) + { + while (Http3FrameReader.TryReadFrame(ref readableBuffer, _incomingFrame, 16 * 1024, out var framePayload)) + { + consumed = examined = framePayload.End; + await ProcessHttp3Stream(application, framePayload); + } + } + + if (result.IsCompleted) + { + return; + } + } + catch (Http3StreamErrorException) + { + // TODO + } + finally + { + Input.AdvanceTo(consumed, examined); + } + } + } + catch (Exception) + { + // TODO + } + finally + { + await RequestBodyPipe.Writer.CompleteAsync(); + } + } + + + private Task ProcessHttp3Stream(IHttpApplication application, in ReadOnlySequence payload) + { + switch (_incomingFrame.Type) + { + case Http3FrameType.Data: + return ProcessDataFrameAsync(payload); + case Http3FrameType.Headers: + return ProcessHeadersFrameAsync(application, payload); + // need to be on control stream + case Http3FrameType.DuplicatePush: + case Http3FrameType.PushPromise: + case Http3FrameType.Settings: + case Http3FrameType.GoAway: + case Http3FrameType.CancelPush: + case Http3FrameType.MaxPushId: + throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); + default: + return ProcessUnknownFrameAsync(); + } + } + + private Task ProcessUnknownFrameAsync() + { + // Unknown frames must be explicitly ignored. + return Task.CompletedTask; + } + + private Task ProcessHeadersFrameAsync(IHttpApplication application, ReadOnlySequence payload) + { + QPackDecoder.Decode(payload, handler: this); + + // start off a request once qpack has decoded + // Make sure to await this task. + if (_receivedHeaders) + { + // trailers + // TODO figure out if there is anything else to do here. + return Task.CompletedTask; + } + + _receivedHeaders = true; + + Task.Run(() => base.ProcessRequestsAsync(application)); + return Task.CompletedTask; + } + + private Task ProcessDataFrameAsync(in ReadOnlySequence payload) + { + foreach (var segment in payload) + { + RequestBodyPipe.Writer.Write(segment.Span); + } + + // TODO this can be better. + return RequestBodyPipe.Writer.FlushAsync().AsTask(); + } + + public void StopProcessingNextRequest() + => StopProcessingNextRequest(serverInitiated: true); + + public void StopProcessingNextRequest(bool serverInitiated) + { + var initiator = serverInitiated ? GracefulCloseInitiator.Server : GracefulCloseInitiator.Client; + + if (Interlocked.CompareExchange(ref _gracefulCloseInitiator, initiator, GracefulCloseInitiator.None) == GracefulCloseInitiator.None) + { + Input.CancelPendingRead(); + } + } + + public void Tick(DateTimeOffset now) + { + } + + protected override void OnReset() + { + } + + protected override void ApplicationAbort() + { + } + + protected override string CreateRequestId() + { + // TODO include stream id. + return ConnectionId; + } + + protected override MessageBody CreateMessageBody() + => Http3MessageBody.For(this); + + + protected override bool TryParseRequest(ReadResult result, out bool endConnection) + { + endConnection = !TryValidatePseudoHeaders(); + return true; + } + + private bool TryValidatePseudoHeaders() + { + _httpVersion = Http.HttpVersion.Http3; + + if (!TryValidateMethod()) + { + return false; + } + + if (!TryValidateAuthorityAndHost(out var hostText)) + { + return false; + } + + // CONNECT - :scheme and :path must be excluded + if (Method == Http.HttpMethod.Connect) + { + if (!string.IsNullOrEmpty(RequestHeaders[HeaderNames.Scheme]) || !string.IsNullOrEmpty(RequestHeaders[HeaderNames.Path])) + { + //ResetAndAbort(new ConnectionAbortedException(CoreStrings.Http2ErrorConnectMustNotSendSchemeOrPath), Http2ErrorCode.PROTOCOL_ERROR); + return false; + } + + RawTarget = hostText; + + return true; + } + + // :scheme https://tools.ietf.org/html/rfc7540#section-8.1.2.3 + // ":scheme" is not restricted to "http" and "https" schemed URIs. A + // proxy or gateway can translate requests for non - HTTP schemes, + // enabling the use of HTTP to interact with non - HTTP services. + + // - That said, we shouldn't allow arbitrary values or use them to populate Request.Scheme, right? + // - For now we'll restrict it to http/s and require it match the transport. + // - We'll need to find some concrete scenarios to warrant unblocking this. + if (!string.Equals(RequestHeaders[HeaderNames.Scheme], Scheme, StringComparison.OrdinalIgnoreCase)) + { + //ResetAndAbort(new ConnectionAbortedException( + // CoreStrings.FormatHttp2StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme)), Http2ErrorCode.PROTOCOL_ERROR); + return false; + } + + // :path (and query) - Required + // Must start with / except may be * for OPTIONS + var path = RequestHeaders[HeaderNames.Path].ToString(); + RawTarget = path; + + // OPTIONS - https://tools.ietf.org/html/rfc7540#section-8.1.2.3 + // This pseudo-header field MUST NOT be empty for "http" or "https" + // URIs; "http" or "https" URIs that do not contain a path component + // MUST include a value of '/'. The exception to this rule is an + // OPTIONS request for an "http" or "https" URI that does not include + // a path component; these MUST include a ":path" pseudo-header field + // with a value of '*'. + if (Method == Http.HttpMethod.Options && path.Length == 1 && path[0] == '*') + { + // * is stored in RawTarget only since HttpRequest expects Path to be empty or start with a /. + Path = string.Empty; + QueryString = string.Empty; + return true; + } + + // Approximate MaxRequestLineSize by totaling the required pseudo header field lengths. + var requestLineLength = _methodText.Length + Scheme.Length + hostText.Length + path.Length; + if (requestLineLength > ServerOptions.Limits.MaxRequestLineSize) + { + //ResetAndAbort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestLineTooLong), Http2ErrorCode.PROTOCOL_ERROR); + return false; + } + + var queryIndex = path.IndexOf('?'); + QueryString = queryIndex == -1 ? string.Empty : path.Substring(queryIndex); + + var pathSegment = queryIndex == -1 ? path.AsSpan() : path.AsSpan(0, queryIndex); + + return TryValidatePath(pathSegment); + } + + + private bool TryValidateMethod() + { + // :method + _methodText = RequestHeaders[HeaderNames.Method].ToString(); + Method = HttpUtilities.GetKnownMethod(_methodText); + + if (Method == Http.HttpMethod.None) + { + // TODO + //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR); + return false; + } + + if (Method == Http.HttpMethod.Custom) + { + if (HttpCharacters.IndexOfInvalidTokenChar(_methodText) >= 0) + { + //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR); + return false; + } + } + + return true; + } + + private bool TryValidateAuthorityAndHost(out string hostText) + { + // :authority (optional) + // Prefer this over Host + + var authority = RequestHeaders[HeaderNames.Authority]; + var host = HttpRequestHeaders.HeaderHost; + if (!StringValues.IsNullOrEmpty(authority)) + { + // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 + // Clients that generate HTTP/2 requests directly SHOULD use the ":authority" + // pseudo - header field instead of the Host header field. + // An intermediary that converts an HTTP/2 request to HTTP/1.1 MUST + // create a Host header field if one is not present in a request by + // copying the value of the ":authority" pseudo - header field. + + // We take this one step further, we don't want mismatched :authority + // and Host headers, replace Host if :authority is defined. The application + // will operate on the Host header. + HttpRequestHeaders.HeaderHost = authority; + host = authority; + } + + // https://tools.ietf.org/html/rfc7230#section-5.4 + // A server MUST respond with a 400 (Bad Request) status code to any + // HTTP/1.1 request message that lacks a Host header field and to any + // request message that contains more than one Host header field or a + // Host header field with an invalid field-value. + hostText = host.ToString(); + if (host.Count > 1 || !HttpUtilities.IsHostHeaderValid(hostText)) + { + // RST replaces 400 + //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(hostText)), Http2ErrorCode.PROTOCOL_ERROR); + return false; + } + + return true; + } + + private bool TryValidatePath(ReadOnlySpan pathSegment) + { + // Must start with a leading slash + if (pathSegment.Length == 0 || pathSegment[0] != '/') + { + //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR); + return false; + } + + var pathEncoded = pathSegment.Contains('%'); + + // Compare with Http1Connection.OnOriginFormTarget + + // URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11 + // Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8; + // then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs" + + try + { + // The decoder operates only on raw bytes + var pathBuffer = new byte[pathSegment.Length].AsSpan(); + for (int i = 0; i < pathSegment.Length; i++) + { + var ch = pathSegment[i]; + // The header parser should already be checking this + Debug.Assert(32 < ch && ch < 127); + pathBuffer[i] = (byte)ch; + } + + Path = PathNormalizer.DecodePath(pathBuffer, pathEncoded, RawTarget, QueryString.Length); + + return true; + } + catch (InvalidOperationException) + { + //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR); + return false; + } + } + + private Pipe CreateRequestBodyPipe(uint windowSize) + => new Pipe(new PipeOptions + ( + pool: _context.MemoryPool, + readerScheduler: ServiceContext.Scheduler, + writerScheduler: PipeScheduler.Inline, + // Never pause within the window range. Flow control will prevent more data from being added. + // See the assert in OnDataAsync. + pauseWriterThreshold: windowSize + 1, + resumeWriterThreshold: windowSize + 1, + useSynchronizationContext: false, + minimumSegmentSize: _context.MemoryPool.GetMinimumSegmentSize() + )); + + /// + /// Used to kick off the request processing loop by derived classes. + /// + public abstract void Execute(); + + private static class GracefulCloseInitiator + { + public const int None = 0; + public const int Server = 1; + public const int Client = 2; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs new file mode 100644 index 0000000000..86952e6a3d --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + class Http3StreamErrorException : Exception + { + public Http3StreamErrorException(string message, Http3ErrorCode errorCode) + : base($"HTTP/3 stream error ({errorCode}): {message}") + { + ErrorCode = errorCode; + } + + public Http3ErrorCode ErrorCode { get; } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs new file mode 100644 index 0000000000..8d8d6a663f --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs @@ -0,0 +1,26 @@ +// 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 Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Abstractions; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +{ + class Http3Stream : Http3Stream, IHostContextContainer + { + private readonly IHttpApplication _application; + + public Http3Stream(IHttpApplication application, Http3Connection connection, HttpConnectionContext context) : base(connection, context) + { + _application = application; + } + + public override void Execute() + { + _ = ProcessRequestAsync(_application); + } + + // Pooled Host context + TContext IHostContextContainer.HostContext { get; set; } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DecoderStreamReader.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DecoderStreamReader.cs new file mode 100644 index 0000000000..6b53b79900 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DecoderStreamReader.cs @@ -0,0 +1,130 @@ +// 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.Buffers; +using System.Net.Http.HPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +{ + internal class DecoderStreamReader + { + private enum State + { + Ready, + HeaderAckowledgement, + StreamCancellation, + InsertCountIncrement + } + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 1 | Stream ID(7+) | + //+---+---------------------------+ + private const byte HeaderAcknowledgementMask = 0x80; + private const byte HeaderAcknowledgementRepresentation = 0x80; + private const byte HeaderAcknowledgementPrefixMask = 0x7F; + private const int HeaderAcknowledgementPrefix = 7; + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 1 | Stream ID(6+) | + //+---+---+-----------------------+ + private const byte StreamCancellationMask = 0xC0; + private const byte StreamCancellationRepresentation = 0x40; + private const byte StreamCancellationPrefixMask = 0x3F; + private const int StreamCancellationPrefix = 6; + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 0 | Increment(6+) | + //+---+---+-----------------------+ + private const byte InsertCountIncrementMask = 0xC0; + private const byte InsertCountIncrementRepresentation = 0x00; + private const byte InsertCountIncrementPrefixMask = 0x3F; + private const int InsertCountIncrementPrefix = 6; + + private IntegerDecoder _integerDecoder = new IntegerDecoder(); + private State _state; + + public DecoderStreamReader() + { + } + + public void Read(ReadOnlySequence data) + { + foreach (var segment in data) + { + var span = segment.Span; + for (var i = 0; i < span.Length; i++) + { + OnByte(span[i]); + } + } + } + + private void OnByte(byte b) + { + int intResult; + int prefixInt; + switch (_state) + { + case State.Ready: + if ((b & HeaderAcknowledgementMask) == HeaderAcknowledgementRepresentation) + { + prefixInt = HeaderAcknowledgementPrefixMask & b; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, HeaderAcknowledgementPrefix, out intResult)) + { + OnHeaderAcknowledgement(intResult); + } + else + { + _state = State.HeaderAckowledgement; + } + } + else if ((b & StreamCancellationMask) == StreamCancellationRepresentation) + { + prefixInt = StreamCancellationPrefixMask & b; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, StreamCancellationPrefix, out intResult)) + { + OnStreamCancellation(intResult); + } + else + { + _state = State.StreamCancellation; + } + } + else if ((b & InsertCountIncrementMask) == InsertCountIncrementRepresentation) + { + prefixInt = InsertCountIncrementPrefixMask & b; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, InsertCountIncrementPrefix, out intResult)) + { + OnInsertCountIncrement(intResult); + } + else + { + _state = State.InsertCountIncrement; + } + } + break; + } + } + + private void OnInsertCountIncrement(int intResult) + { + // increment some count. + _state = State.Ready; + } + + private void OnStreamCancellation(int streamId) + { + // Remove stream? + _state = State.Ready; + } + + private void OnHeaderAcknowledgement(int intResult) + { + // Acknowledge header somehow + _state = State.Ready; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DynamicTable.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DynamicTable.cs new file mode 100644 index 0000000000..bd48915bea --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DynamicTable.cs @@ -0,0 +1,46 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +{ + // The size of the dynamic table is the sum of the size of its entries. + // The size of an entry is the sum of its name's length in bytes (as + // defined in Section 4.1.2), its value's length in bytes, and 32. + + internal class DynamicTable + { + + // The encoder sends a Set Dynamic Table Capacity + // instruction(Section 4.3.1) with a non-zero capacity to begin using + // the dynamic table. + public DynamicTable(int maxSize) + { + } + + public HeaderField this[int index] + { + get + { + return new HeaderField(); + } + } + + // TODO + public void Insert(Span name, Span value) + { + } + + // TODO + public void Resize(int maxSize) + { + } + + // TODO + internal void Duplicate(int index) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/EncoderStreamReader.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/EncoderStreamReader.cs new file mode 100644 index 0000000000..e2d769507f --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/EncoderStreamReader.cs @@ -0,0 +1,332 @@ +// 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.Net.Http.HPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +{ + internal class EncoderStreamReader + { + private enum State + { + Ready, + DynamicTableCapcity, + NameIndex, + NameLength, + Name, + ValueLength, + ValueLengthContinue, + Value, + Duplicate + } + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 0 | 1 | Capacity(5+) | + //+---+---+---+-------------------+ + private const byte DynamicTableCapacityMask = 0xE0; + private const byte DynamicTableCapacityRepresentation = 0x20; + private const byte DynamicTableCapacityPrefixMask = 0x1F; + private const int DynamicTableCapacityPrefix = 5; + + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 1 | S | Name Index(6+) | + //+---+---+-----------------------+ + //| H | Value Length(7+) | + //+---+---------------------------+ + //| Value String(Length bytes) | + //+-------------------------------+ + private const byte InsertWithNameReferenceMask = 0x80; + private const byte InsertWithNameReferenceRepresentation = 0x80; + private const byte InsertWithNameReferencePrefixMask = 0x3F; + private const byte InsertWithNameReferenceStaticMask = 0x40; + private const int InsertWithNameReferencePrefix = 6; + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 1 | H | Name Length(5+) | + //+---+---+---+-------------------+ + //| Name String(Length bytes) | + //+---+---------------------------+ + //| H | Value Length(7+) | + //+---+---------------------------+ + //| Value String(Length bytes) | + //+-------------------------------+ + private const byte InsertWithoutNameReferenceMask = 0xC0; + private const byte InsertWithoutNameReferenceRepresentation = 0x40; + private const byte InsertWithoutNameReferencePrefixMask = 0x1F; + private const byte InsertWithoutNameReferenceHuffmanMask = 0x20; + private const int InsertWithoutNameReferencePrefix = 5; + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 0 | 0 | Index(5+) | + //+---+---+---+-------------------+ + private const byte DuplicateMask = 0xE0; + private const byte DuplicateRepresentation = 0x00; + private const byte DuplicatePrefixMask = 0x1F; + private const int DuplicatePrefix = 5; + + private const int StringLengthPrefix = 7; + private const byte HuffmanMask = 0x80; + + private bool _s; + private byte[] _stringOctets; + private byte[] _headerNameOctets; + private byte[] _headerValueOctets; + private byte[] _headerName; + private int _headerNameLength; + private int _headerValueLength; + private int _stringLength; + private int _stringIndex; + private DynamicTable _dynamicTable = new DynamicTable(1000); // TODO figure out architecture. + + private readonly IntegerDecoder _integerDecoder = new IntegerDecoder(); + private State _state = State.Ready; + private bool _huffman; + + public EncoderStreamReader(int maxRequestHeaderFieldSize) + { + // TODO how to propagate dynamic table around. + + _stringOctets = new byte[maxRequestHeaderFieldSize]; + _headerNameOctets = new byte[maxRequestHeaderFieldSize]; + _headerValueOctets = new byte[maxRequestHeaderFieldSize]; + } + + public void Read(ReadOnlySequence data) + { + foreach (var segment in data) + { + var span = segment.Span; + for (var i = 0; i < span.Length; i++) + { + OnByte(span[i]); + } + } + } + + private void OnByte(byte b) + { + int intResult; + int prefixInt; + switch (_state) + { + case State.Ready: + if ((b & DynamicTableCapacityMask) == DynamicTableCapacityRepresentation) + { + prefixInt = DynamicTableCapacityPrefixMask & b; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, DynamicTableCapacityPrefix, out intResult)) + { + OnDynamicTableCapacity(intResult); + } + else + { + _state = State.DynamicTableCapcity; + } + } + else if ((b & InsertWithNameReferenceMask) == InsertWithNameReferenceRepresentation) + { + prefixInt = InsertWithNameReferencePrefixMask & b; + _s = (InsertWithNameReferenceStaticMask & b) == InsertWithNameReferenceStaticMask; + + if (_integerDecoder.BeginTryDecode((byte)prefixInt, InsertWithNameReferencePrefix, out intResult)) + { + OnNameIndex(intResult); + } + else + { + _state = State.NameIndex; + } + } + else if ((b & InsertWithoutNameReferenceMask) == InsertWithoutNameReferenceRepresentation) + { + prefixInt = InsertWithoutNameReferencePrefixMask & b; + _huffman = (InsertWithoutNameReferenceHuffmanMask & b) == InsertWithoutNameReferenceHuffmanMask; + + if (_integerDecoder.BeginTryDecode((byte)prefixInt, InsertWithoutNameReferencePrefix, out intResult)) + { + OnStringLength(intResult, State.Name); + } + else + { + _state = State.NameIndex; + } + } + else if ((b & DuplicateMask) == DuplicateRepresentation) + { + prefixInt = DuplicatePrefixMask & b; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, DuplicatePrefix, out intResult)) + { + OnDuplicate(intResult); + } + else + { + _state = State.Duplicate; + } + } + break; + case State.DynamicTableCapcity: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnDynamicTableCapacity(intResult); + } + break; + case State.NameIndex: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnNameIndex(intResult); + } + break; + case State.NameLength: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnStringLength(intResult, nextState: State.Name); + } + break; + case State.Name: + _stringOctets[_stringIndex++] = b; + + if (_stringIndex == _stringLength) + { + OnString(nextState: State.ValueLength); + } + + break; + case State.ValueLength: + _huffman = (b & HuffmanMask) != 0; + + // TODO confirm this. + if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) + { + OnStringLength(intResult, nextState: State.Value); + if (intResult == 0) + { + ProcessValue(); + } + } + else + { + _state = State.ValueLengthContinue; + } + break; + case State.ValueLengthContinue: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnStringLength(intResult, nextState: State.Value); + if (intResult == 0) + { + ProcessValue(); + } + } + break; + case State.Value: + _stringOctets[_stringIndex++] = b; + if (_stringIndex == _stringLength) + { + ProcessValue(); + } + break; + case State.Duplicate: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnDuplicate(intResult); + } + break; + } + } + + + private void OnStringLength(int length, State nextState) + { + if (length > _stringOctets.Length) + { + throw new QPackDecodingException(/*CoreStrings.FormatQPackStringLengthTooLarge(length, _stringOctets.Length)*/); + } + + _stringLength = length; + _stringIndex = 0; + _state = nextState; + } + + private void ProcessValue() + { + OnString(nextState: State.Ready); + var headerNameSpan = new Span(_headerName, 0, _headerNameLength); + var headerValueSpan = new Span(_headerValueOctets, 0, _headerValueLength); + _dynamicTable.Insert(headerNameSpan, headerValueSpan); + } + + private void OnString(State nextState) + { + int Decode(byte[] dst) + { + if (_huffman) + { + return Huffman.Decode(new ReadOnlySpan(_stringOctets, 0, _stringLength), ref dst); + } + else + { + Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength); + return _stringLength; + } + } + + try + { + if (_state == State.Name) + { + _headerName = _headerNameOctets; + _headerNameLength = Decode(_headerNameOctets); + } + else + { + _headerValueLength = Decode(_headerValueOctets); + } + } + catch (HuffmanDecodingException ex) + { + throw new QPackDecodingException(""/*CoreStrings.QPackHuffmanError*/, ex); + } + + _state = nextState; + } + + private void OnNameIndex(int index) + { + var header = GetHeader(index); + _headerName = header.Name; + _headerNameLength = header.Name.Length; + _state = State.ValueLength; + } + + private void OnDynamicTableCapacity(int dynamicTableSize) + { + // Call Decoder to update the table size. + _dynamicTable.Resize(dynamicTableSize); + _state = State.Ready; + } + + private void OnDuplicate(int index) + { + _dynamicTable.Duplicate(index); + _state = State.Ready; + } + + private HeaderField GetHeader(int index) + { + try + { + return _s ? StaticTable.Instance[index] : _dynamicTable[index]; + } + catch (IndexOutOfRangeException ex) + { + throw new QPackDecodingException( "" /*CoreStrings.FormatQPackErrorIndexOutOfRange(index)*/, ex); + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/HeaderField.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/HeaderField.cs new file mode 100644 index 0000000000..d929187de5 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/HeaderField.cs @@ -0,0 +1,27 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +{ + internal readonly struct HeaderField + { + public HeaderField(Span name, Span value) + { + Name = new byte[name.Length]; + name.CopyTo(Name); + + Value = new byte[value.Length]; + value.CopyTo(Value); + } + + public byte[] Name { get; } + + public byte[] Value { get; } + + public int Length => GetLength(Name.Length, Value.Length); + + public static int GetLength(int nameLength, int valueLength) => nameLength + valueLength; + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecoder.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecoder.cs new file mode 100644 index 0000000000..a52ad04e81 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecoder.cs @@ -0,0 +1,513 @@ +// 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.Net.Http; +using System.Net.Http.HPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +{ + internal class QPackDecoder + { + private enum State + { + Ready, + RequiredInsertCount, + RequiredInsertCountDone, + Base, + CompressedHeaders, + HeaderFieldIndex, + HeaderNameIndex, + HeaderNameLength, + HeaderNameLengthContinue, + HeaderName, + HeaderValueLength, + HeaderValueLengthContinue, + HeaderValue, + DynamicTableSizeUpdate, + PostBaseIndex, + LiteralHeaderFieldWithNameReference, + HeaderNameIndexPostBase + } + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| Required Insert Count(8+) | + //+---+---------------------------+ + //| S | Delta Base(7+) | + //+---+---------------------------+ + //| Compressed Headers ... + private const int RequiredInsertCountPrefix = 8; + private const int BaseMask = 0x80; + private const int BasePrefix = 7; + //+-------------------------------+ + + //https://tools.ietf.org/html/draft-ietf-quic-qpack-09#section-4.5.2 + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 1 | S | Index(6+) | + //+---+---+-----------------------+ + private const byte IndexedHeaderFieldMask = 0x80; + private const byte IndexedHeaderFieldRepresentation = 0x80; + private const byte IndexedHeaderStaticMask = 0x40; + private const byte IndexedHeaderStaticRepresentation = 0x40; + private const byte IndexedHeaderFieldPrefixMask = 0x3F; + private const int IndexedHeaderFieldPrefix = 6; + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 0 | 0 | 1 | Index(4+) | + //+---+---+---+---+---------------+ + private const byte PostBaseIndexMask = 0xF0; + private const byte PostBaseIndexRepresentation = 0x10; + private const int PostBaseIndexPrefix = 4; + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 1 | N | S |Name Index(4+)| + //+---+---+---+---+---------------+ + //| H | Value Length(7+) | + //+---+---------------------------+ + //| Value String(Length bytes) | + //+-------------------------------+ + private const byte LiteralHeaderFieldMask = 0xC0; + private const byte LiteralHeaderFieldRepresentation = 0x40; + private const byte LiteralHeaderFieldNMask = 0x20; + private const byte LiteralHeaderFieldStaticMask = 0x10; + private const byte LiteralHeaderFieldPrefixMask = 0x0F; + private const int LiteralHeaderFieldPrefix = 4; + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 0 | 0 | 0 | N |NameIdx(3+)| + //+---+---+---+---+---+-----------+ + //| H | Value Length(7+) | + //+---+---------------------------+ + //| Value String(Length bytes) | + //+-------------------------------+ + private const byte LiteralHeaderFieldPostBaseMask = 0xF0; + private const byte LiteralHeaderFieldPostBaseRepresentation = 0x00; + private const byte LiteralHeaderFieldPostBaseNMask = 0x08; + private const byte LiteralHeaderFieldPostBasePrefixMask = 0x07; + private const int LiteralHeaderFieldPostBasePrefix = 3; + + //0 1 2 3 4 5 6 7 + //+---+---+---+---+---+---+---+---+ + //| 0 | 0 | 1 | N | H |NameLen(3+)| + //+---+---+---+---+---+-----------+ + //| Name String(Length bytes) | + //+---+---------------------------+ + //| H | Value Length(7+) | + //+---+---------------------------+ + //| Value String(Length bytes) | + //+-------------------------------+ + private const byte LiteralHeaderFieldWithoutNameReferenceMask = 0xE0; + private const byte LiteralHeaderFieldWithoutNameReferenceRepresentation = 0x20; + private const byte LiteralHeaderFieldWithoutNameReferenceNMask = 0x10; + private const byte LiteralHeaderFieldWithoutNameReferenceHuffmanMask = 0x08; + private const byte LiteralHeaderFieldWithoutNameReferencePrefixMask = 0x07; + private const int LiteralHeaderFieldWithoutNameReferencePrefix = 3; + + private const int StringLengthPrefix = 7; + private const byte HuffmanMask = 0x80; + + private State _state = State.Ready; + // TODO break out dynamic table entirely. + private long _maxDynamicTableSize; + private DynamicTable _dynamicTable; + + // TODO idk what these are for. + private byte[] _stringOctets; + private byte[] _headerNameOctets; + private byte[] _headerValueOctets; + private int _requiredInsertCount; + //private int _insertCount; + private int _base; + + // s is used for whatever s is in each field. This has multiple definition + private bool _s; + private bool _n; + private bool _huffman; + private bool _index; + + private byte[] _headerName; + private int _headerNameLength; + private int _headerValueLength; + private int _stringLength; + private int _stringIndex; + private readonly IntegerDecoder _integerDecoder = new IntegerDecoder(); + + // Decoders are on the http3stream now, each time we see a header block + public QPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize) + : this(maxDynamicTableSize, maxRequestHeaderFieldSize, new DynamicTable(maxDynamicTableSize)) { } + + + // For testing. + internal QPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize, DynamicTable dynamicTable) + { + _maxDynamicTableSize = maxDynamicTableSize; + _dynamicTable = dynamicTable; + + _stringOctets = new byte[maxRequestHeaderFieldSize]; + _headerNameOctets = new byte[maxRequestHeaderFieldSize]; + _headerValueOctets = new byte[maxRequestHeaderFieldSize]; + } + + // sequence will probably be a header block instead. + public void Decode(in ReadOnlySequence headerBlock, IHttpHeadersHandler handler) + { + // TODO I need to get the RequiredInsertCount and DeltaBase + // These are always present in the header block + // TODO need to figure out if I have read an entire header block. + + // (I think this can be done based on length outside of this) + foreach (var segment in headerBlock) + { + var span = segment.Span; + for (var i = 0; i < span.Length; i++) + { + OnByte(span[i], handler); + } + } + } + + private void OnByte(byte b, IHttpHeadersHandler handler) + { + int intResult; + int prefixInt; + switch (_state) + { + case State.Ready: + if (_integerDecoder.BeginTryDecode(b, RequiredInsertCountPrefix, out intResult)) + { + OnRequiredInsertCount(intResult); + } + else + { + _state = State.RequiredInsertCount; + } + break; + case State.RequiredInsertCount: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnRequiredInsertCount(intResult); + } + break; + case State.RequiredInsertCountDone: + prefixInt = ~BaseMask & b; + + _s = (b & BaseMask) == BaseMask; + + if (_integerDecoder.BeginTryDecode(b, BasePrefix, out intResult)) + { + OnBase(intResult); + } + else + { + _state = State.Base; + } + break; + case State.Base: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnBase(intResult); + } + break; + case State.CompressedHeaders: + if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation) + { + prefixInt = IndexedHeaderFieldPrefixMask & b; + _s = (b & IndexedHeaderStaticMask) == IndexedHeaderStaticRepresentation; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, IndexedHeaderFieldPrefix, out intResult)) + { + OnIndexedHeaderField(intResult, handler); + } + else + { + _state = State.HeaderFieldIndex; + } + } + else if ((b & PostBaseIndexMask) == PostBaseIndexRepresentation) + { + prefixInt = ~PostBaseIndexMask & b; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, PostBaseIndexPrefix, out intResult)) + { + OnPostBaseIndex(intResult, handler); + } + else + { + _state = State.PostBaseIndex; + } + } + else if ((b & LiteralHeaderFieldMask) == LiteralHeaderFieldRepresentation) + { + _index = true; + // Represents whether an intermediary is permitted to add this header to the dynamic header table on + // subsequent hops. + // if n is set, the encoded header must always be encoded with a literal representation + + _n = (LiteralHeaderFieldNMask & b) == LiteralHeaderFieldNMask; + _s = (LiteralHeaderFieldStaticMask & b) == LiteralHeaderFieldStaticMask; + prefixInt = b & LiteralHeaderFieldPrefixMask; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPrefix, out intResult)) + { + OnIndexedHeaderName(intResult); + } + else + { + _state = State.HeaderNameIndex; + } + } + else if ((b & LiteralHeaderFieldPostBaseMask) == LiteralHeaderFieldPostBaseRepresentation) + { + _index = true; + _n = (LiteralHeaderFieldPostBaseNMask & b) == LiteralHeaderFieldPostBaseNMask; + prefixInt = b & LiteralHeaderFieldPostBasePrefixMask; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPostBasePrefix, out intResult)) + { + OnIndexedHeaderNamePostBase(intResult); + } + else + { + _state = State.HeaderNameIndexPostBase; + } + } + else if ((b & LiteralHeaderFieldWithoutNameReferenceMask) == LiteralHeaderFieldWithoutNameReferenceRepresentation) + { + _index = false; + _n = (LiteralHeaderFieldWithoutNameReferenceNMask & b) == LiteralHeaderFieldWithoutNameReferenceNMask; + _huffman = (b & LiteralHeaderFieldWithoutNameReferenceHuffmanMask) != 0; + prefixInt = b & LiteralHeaderFieldWithoutNameReferencePrefixMask; + + if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldWithoutNameReferencePrefix, out intResult)) + { + OnStringLength(intResult, State.HeaderName); + } + else + { + _state = State.HeaderNameLength; + } + } + break; + case State.HeaderNameLength: + // huffman has already been processed. + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnStringLength(intResult, nextState: State.HeaderName); + } + break; + case State.HeaderName: + _stringOctets[_stringIndex++] = b; + + if (_stringIndex == _stringLength) + { + OnString(nextState: State.HeaderValueLength); + } + + break; + case State.HeaderNameIndex: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnIndexedHeaderName(intResult); + } + break; + case State.HeaderNameIndexPostBase: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnIndexedHeaderNamePostBase(intResult); + } + break; + case State.HeaderValueLength: + _huffman = (b & HuffmanMask) != 0; + + // TODO confirm this. + if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) + { + OnStringLength(intResult, nextState: State.HeaderValue); + if (intResult == 0) + { + ProcessHeaderValue(handler); + } + } + else + { + _state = State.HeaderValueLengthContinue; + } + break; + case State.HeaderValueLengthContinue: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnStringLength(intResult, nextState: State.HeaderValue); + if (intResult == 0) + { + ProcessHeaderValue(handler); + } + } + break; + case State.HeaderValue: + _stringOctets[_stringIndex++] = b; + if (_stringIndex == _stringLength) + { + ProcessHeaderValue(handler); + } + break; + case State.HeaderFieldIndex: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnIndexedHeaderField(intResult, handler); + } + break; + case State.PostBaseIndex: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnPostBaseIndex(intResult, handler); + } + break; + case State.LiteralHeaderFieldWithNameReference: + break; + } + } + + private void OnStringLength(int length, State nextState) + { + if (length > _stringOctets.Length) + { + throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.FormatQPackStringLengthTooLarge(length, _stringOctets.Length)*/); + } + + _stringLength = length; + _stringIndex = 0; + _state = nextState; + } + + private void ProcessHeaderValue(IHttpHeadersHandler handler) + { + OnString(nextState: State.CompressedHeaders); + + var headerNameSpan = new Span(_headerName, 0, _headerNameLength); + var headerValueSpan = new Span(_headerValueOctets, 0, _headerValueLength); + + handler.OnHeader(headerNameSpan, headerValueSpan); + + if (_index) + { + _dynamicTable.Insert(headerNameSpan, headerValueSpan); + } + } + + private void OnString(State nextState) + { + int Decode(byte[] dst) + { + if (_huffman) + { + return Huffman.Decode(new ReadOnlySpan(_stringOctets, 0, _stringLength), ref dst); + } + else + { + Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength); + return _stringLength; + } + } + + try + { + if (_state == State.HeaderName) + { + _headerName = _headerNameOctets; + _headerNameLength = Decode(_headerNameOctets); + } + else + { + _headerValueLength = Decode(_headerValueOctets); + } + } + catch (HuffmanDecodingException ex) + { + throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.QPackHuffmanError, */, ex); + } + + _state = nextState; + } + + + private void OnIndexedHeaderName(int index) + { + var header = GetHeader(index); + _headerName = header.Name; + _headerNameLength = header.Name.Length; + _state = State.HeaderValueLength; + } + + private void OnIndexedHeaderNamePostBase(int index) + { + // TODO update with postbase index + var header = GetHeader(index); + _headerName = header.Name; + _headerNameLength = header.Name.Length; + _state = State.HeaderValueLength; + } + + private void OnPostBaseIndex(int intResult, IHttpHeadersHandler handler) + { + // TODO + _state = State.CompressedHeaders; + } + + private void OnBase(int deltaBase) + { + _state = State.CompressedHeaders; + if (_s) + { + _base = _requiredInsertCount - deltaBase - 1; + } + else + { + _base = _requiredInsertCount + deltaBase; + } + } + + // TODO + private void OnRequiredInsertCount(int requiredInsertCount) + { + _requiredInsertCount = requiredInsertCount; + _state = State.RequiredInsertCountDone; + // This is just going to noop for now. I don't get this algorithm at all. + // var encoderInsertCount = 0; + // var maxEntries = _maxDynamicTableSize / HeaderField.RfcOverhead; + + // if (requiredInsertCount != 0) + // { + // encoderInsertCount = (requiredInsertCount % ( 2 * maxEntries)) + 1; + // } + + // // Dude I don't get this algorithm... + // var fullRange = 2 * maxEntries; + // if (encoderInsertCount == 0) + // { + + // } + } + + private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler) + { + // Indexes start at 0 in QPack + var header = GetHeader(index); + handler.OnHeader(new Span(header.Name), new Span(header.Value)); + _state = State.CompressedHeaders; + } + + private HeaderField GetHeader(int index) + { + try + { + return _s ? StaticTable.Instance[index] : _dynamicTable[index]; + } + catch (IndexOutOfRangeException ex) + { + throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.FormatQPackErrorIndexOutOfRange(index), */, ex); + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecodingException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecodingException.cs new file mode 100644 index 0000000000..fb648d3045 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecodingException.cs @@ -0,0 +1,28 @@ +// 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.Runtime.Serialization; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +{ + [Serializable] + internal class QPackDecodingException : Exception + { + public QPackDecodingException() + { + } + + public QPackDecodingException(string message) : base(message) + { + } + + public QPackDecodingException(string message, Exception innerException) : base(message, innerException) + { + } + + protected QPackDecodingException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncoder.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncoder.cs new file mode 100644 index 0000000000..44a5e20ea5 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncoder.cs @@ -0,0 +1,504 @@ +// 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.Net.Http; +using System.Net.Http.HPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +{ + internal class QPackEncoder + { + private IEnumerator> _enumerator; + + // TODO these all need to be updated! + + /* + * 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 1 | S | Index (6+) | + +---+---+-----------------------+ + */ + public static bool EncodeIndexedHeaderField(int index, Span destination, out int bytesWritten) + { + if (destination.IsEmpty) + { + bytesWritten = 0; + return false; + } + + EncodeHeaderBlockPrefix(destination, out bytesWritten); + destination = destination.Slice(bytesWritten); + + return IntegerEncoder.Encode(index, 6, destination, out bytesWritten); + } + + public static bool EncodeIndexHeaderFieldWithPostBaseIndex(int index, Span destination, out int bytesWritten) + { + bytesWritten = 0; + return false; + } + + /// Encodes a "Literal Header Field without Indexing". + public static bool EncodeLiteralHeaderFieldWithNameReference(int index, string value, Span destination, out int bytesWritten) + { + if (destination.IsEmpty) + { + bytesWritten = 0; + return false; + } + + EncodeHeaderBlockPrefix(destination, out bytesWritten); + destination = destination.Slice(bytesWritten); + + return IntegerEncoder.Encode(index, 6, destination, out bytesWritten); + } + + /* + * 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | 0 | 1 | N | S |Name Index (4+)| + +---+---+---+---+---------------+ + | H | Value Length (7+) | + +---+---------------------------+ + | Value String (Length bytes) | + +-------------------------------+ + */ + public static bool EncodeLiteralHeaderFieldWithPostBaseNameReference(int index, Span destination, out int bytesWritten) + { + bytesWritten = 0; + return false; + } + + public static bool EncodeLiteralHeaderFieldWithoutNameReference(int index, Span destination, out int bytesWritten) + { + bytesWritten = 0; + return false; + } + + /* + * 0 1 2 3 4 5 6 7 + +---+---+---+---+---+---+---+---+ + | Required Insert Count (8+) | + +---+---------------------------+ + | S | Delta Base (7+) | + +---+---------------------------+ + | Compressed Headers ... + +-------------------------------+ + * + */ + private static bool EncodeHeaderBlockPrefix(Span destination, out int bytesWritten) + { + int length; + bytesWritten = 0; + // Required insert count as first int + if (!IntegerEncoder.Encode(0, 8, destination, out length)) + { + return false; + } + + bytesWritten += length; + destination = destination.Slice(length); + + // Delta base + if (destination.IsEmpty) + { + return false; + } + + destination[0] = 0x00; + if (!IntegerEncoder.Encode(0, 7, destination, out length)) + { + return false; + } + + bytesWritten += length; + + return true; + } + + private static bool EncodeLiteralHeaderName(string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-5.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | H | String Length (7+) | + // +---+---------------------------+ + // | String Data (Length octets) | + // +-------------------------------+ + + if (!destination.IsEmpty) + { + destination[0] = 0; // TODO: Use Huffman encoding + if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength)) + { + Debug.Assert(integerLength >= 1); + + destination = destination.Slice(integerLength); + if (value.Length <= destination.Length) + { + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + destination[i] = (byte)((uint)(c - 'A') <= ('Z' - 'A') ? c | 0x20 : c); + } + + bytesWritten = integerLength + value.Length; + return true; + } + } + } + + bytesWritten = 0; + return false; + } + + private static bool EncodeStringLiteralValue(string value, Span destination, out int bytesWritten) + { + if (value.Length <= destination.Length) + { + for (int i = 0; i < value.Length; i++) + { + char c = value[i]; + if ((c & 0xFF80) != 0) + { + throw new HttpRequestException(""); + } + + destination[i] = (byte)c; + } + + bytesWritten = value.Length; + return true; + } + + bytesWritten = 0; + return false; + } + + public static bool EncodeStringLiteral(string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-5.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | H | String Length (7+) | + // +---+---------------------------+ + // | String Data (Length octets) | + // +-------------------------------+ + + if (!destination.IsEmpty) + { + destination[0] = 0; // TODO: Use Huffman encoding + if (IntegerEncoder.Encode(value.Length, 7, destination, out int integerLength)) + { + Debug.Assert(integerLength >= 1); + + if (EncodeStringLiteralValue(value, destination.Slice(integerLength), out int valueLength)) + { + bytesWritten = integerLength + valueLength; + return true; + } + } + } + + bytesWritten = 0; + return false; + } + + public static bool EncodeStringLiterals(string[] values, string separator, Span destination, out int bytesWritten) + { + bytesWritten = 0; + + if (values.Length == 0) + { + return EncodeStringLiteral("", destination, out bytesWritten); + } + else if (values.Length == 1) + { + return EncodeStringLiteral(values[0], destination, out bytesWritten); + } + + if (!destination.IsEmpty) + { + int valueLength = 0; + + // Calculate length of all parts and separators. + foreach (string part in values) + { + valueLength = checked((int)(valueLength + part.Length)); + } + + valueLength = checked((int)(valueLength + (values.Length - 1) * separator.Length)); + + if (IntegerEncoder.Encode(valueLength, 7, destination, out int integerLength)) + { + Debug.Assert(integerLength >= 1); + + int encodedLength = 0; + for (int j = 0; j < values.Length; j++) + { + if (j != 0 && !EncodeStringLiteralValue(separator, destination.Slice(integerLength), out encodedLength)) + { + return false; + } + + integerLength += encodedLength; + + if (!EncodeStringLiteralValue(values[j], destination.Slice(integerLength), out encodedLength)) + { + return false; + } + + integerLength += encodedLength; + } + + bytesWritten = integerLength; + return true; + } + } + + return false; + } + + /// + /// Encodes a "Literal Header Field without Indexing" to a new array, but only the index portion; + /// a subsequent call to must be used to encode the associated value. + /// + public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index) + { + Span span = stackalloc byte[256]; + bool success = EncodeLiteralHeaderFieldWithPostBaseNameReference(index, span, out int length); + Debug.Assert(success, $"Stack-allocated space was too small for index '{index}'."); + return span.Slice(0, length).ToArray(); + } + + /// + /// Encodes a "Literal Header Field without Indexing - New Name" to a new array, but only the name portion; + /// a subsequent call to must be used to encode the associated value. + /// + public static byte[] EncodeLiteralHeaderFieldWithoutIndexingNewNameToAllocatedArray(string name) + { + Span span = stackalloc byte[256]; + bool success = EncodeLiteralHeaderFieldWithoutIndexingNewName(name, span, out int length); + Debug.Assert(success, $"Stack-allocated space was too small for \"{name}\"."); + return span.Slice(0, length).ToArray(); + } + + private static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, Span span, out int length) + { + throw new NotImplementedException(); + } + + /// Encodes a "Literal Header Field without Indexing" to a new array. + public static byte[] EncodeLiteralHeaderFieldWithoutIndexingToAllocatedArray(int index, string value) + { + Span span = +#if DEBUG + stackalloc byte[4]; // to validate growth algorithm +#else + stackalloc byte[512]; +#endif + while (true) + { + if (EncodeLiteralHeaderFieldWithNameReference(index, value, span, out int length)) + { + return span.Slice(0, length).ToArray(); + } + + // This is a rare path, only used once per HTTP/2 connection and only + // for very long host names. Just allocate rather than complicate + // the code with ArrayPool usage. In practice we should never hit this, + // as hostnames should be <= 255 characters. + span = new byte[span.Length * 2]; + } + } + + // TODO these are fairly hard coded for the first two bytes to be zero. + public bool BeginEncode(IEnumerable> headers, Span buffer, out int length) + { + _enumerator = headers.GetEnumerator(); + _enumerator.MoveNext(); + buffer[0] = 0; + buffer[1] = 0; + + return Encode(buffer.Slice(2), out length); + } + + public bool BeginEncode(int statusCode, IEnumerable> headers, Span buffer, out int length) + { + _enumerator = headers.GetEnumerator(); + _enumerator.MoveNext(); + + // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#header-prefix + buffer[0] = 0; + buffer[1] = 0; + + var statusCodeLength = EncodeStatusCode(statusCode, buffer.Slice(2)); + var done = Encode(buffer.Slice(statusCodeLength + 2), throwIfNoneEncoded: false, out var headersLength); + length = statusCodeLength + headersLength + 2; + + return done; + } + + public bool Encode(Span buffer, out int length) + { + return Encode(buffer, throwIfNoneEncoded: true, out length); + } + + private bool Encode(Span buffer, bool throwIfNoneEncoded, out int length) + { + length = 0; + + do + { + if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(length), out var headerLength)) + { + if (length == 0 && throwIfNoneEncoded) + { + throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */); + } + return false; + } + + length += headerLength; + } while (_enumerator.MoveNext()); + + return true; + } + + private bool EncodeHeader(string name, string value, Span buffer, out int length) + { + var i = 0; + length = 0; + + if (buffer.IsEmpty) + { + return false; + } + + if (!EncodeNameString(name, buffer.Slice(i), out var nameLength, lowercase: true)) + { + return false; + } + + i += nameLength; + + if (i >= buffer.Length) + { + return false; + } + + if (!EncodeValueString(value, buffer.Slice(i), out var valueLength, lowercase: false)) + { + return false; + } + + i += valueLength; + + length = i; + return true; + } + + private bool EncodeValueString(string s, Span buffer, out int length, bool lowercase) + { + const int toLowerMask = 0x20; + + var i = 0; + length = 0; + + if (buffer.IsEmpty) + { + return false; + } + + buffer[0] = 0; + + if (!IntegerEncoder.Encode(s.Length, 7, buffer, out var nameLength)) + { + return false; + } + + i += nameLength; + + // TODO: use huffman encoding + for (var j = 0; j < s.Length; j++) + { + if (i >= buffer.Length) + { + return false; + } + + buffer[i++] = (byte)(s[j] | (lowercase && s[j] >= (byte)'A' && s[j] <= (byte)'Z' ? toLowerMask : 0)); + } + + length = i; + return true; + } + + private bool EncodeNameString(string s, Span buffer, out int length, bool lowercase) + { + const int toLowerMask = 0x20; + + var i = 0; + length = 0; + + if (buffer.IsEmpty) + { + return false; + } + + buffer[0] = 0x30; + + if (!IntegerEncoder.Encode(s.Length, 3, buffer, out var nameLength)) + { + return false; + } + + i += nameLength; + + // TODO: use huffman encoding + for (var j = 0; j < s.Length; j++) + { + if (i >= buffer.Length) + { + return false; + } + + buffer[i++] = (byte)(s[j] | (lowercase && s[j] >= (byte)'A' && s[j] <= (byte)'Z' ? toLowerMask : 0)); + } + + length = i; + return true; + } + + private int EncodeStatusCode(int statusCode, Span buffer) + { + switch (statusCode) + { + case 200: + case 204: + case 206: + case 304: + case 400: + case 404: + case 500: + // TODO this isn't safe, some index can be larger than 64. Encoded here! + buffer[0] = (byte)(0xC0 | StaticTable.Instance.StatusIndex[statusCode]); + return 1; + default: + // Send as Literal Header Field Without Indexing - Indexed Name + buffer[0] = 0x08; + + var statusBytes = StatusCodes.ToStatusBytes(statusCode); + buffer[1] = (byte)statusBytes.Length; + ((ReadOnlySpan)statusBytes).CopyTo(buffer.Slice(2)); + + return 2 + statusBytes.Length; + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncodingException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncodingException.cs new file mode 100644 index 0000000000..306850ab4b --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncodingException.cs @@ -0,0 +1,19 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +{ + internal sealed class QPackEncodingException : Exception + { + public QPackEncodingException(string message) + : base(message) + { + } + public QPackEncodingException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/StaticTable.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/StaticTable.cs new file mode 100644 index 0000000000..4bf15a9b45 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/StaticTable.cs @@ -0,0 +1,162 @@ +// 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.Collections.Generic; +using System.Net.Http; +using System.Text; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +{ + internal class StaticTable + { + private static readonly StaticTable _instance = new StaticTable(); + + private readonly Dictionary _statusIndex = new Dictionary + { + [103] = 24, + [200] = 25, + [304] = 26, + [404] = 27, + [503] = 28, + [100] = 63, + [204] = 64, + [206] = 65, + [302] = 66, + [400] = 67, + [403] = 68, + [421] = 69, + [425] = 70, + [500] = 71, + }; + + private readonly Dictionary _methodIndex = new Dictionary + { + // TODO connect is intenral to system.net.http + [HttpMethod.Delete] = 16, + [HttpMethod.Get] = 17, + [HttpMethod.Head] = 18, + [HttpMethod.Options] = 19, + [HttpMethod.Post] = 20, + [HttpMethod.Put] = 21, + }; + + private StaticTable() + { + } + + public static StaticTable Instance => _instance; + + public int Count => _staticTable.Length; + + public HeaderField this[int index] => _staticTable[index]; + + public IReadOnlyDictionary StatusIndex => _statusIndex; + public IReadOnlyDictionary MethodIndex => _methodIndex; + + private readonly HeaderField[] _staticTable = new HeaderField[] + { + CreateHeaderField(":authority", ""), // 0 + CreateHeaderField(":path", "/"), // 1 + CreateHeaderField("age", "0"), // 2 + CreateHeaderField("content-disposition", ""), + CreateHeaderField("content-length", "0"), + CreateHeaderField("cookie", ""), + CreateHeaderField("date", ""), + CreateHeaderField("etag", ""), + CreateHeaderField("if-modified-since", ""), + CreateHeaderField("if-none-match", ""), + CreateHeaderField("last-modified", ""), // 10 + CreateHeaderField("link", ""), + CreateHeaderField("location", ""), + CreateHeaderField("referer", ""), + CreateHeaderField("set-cookie", ""), + CreateHeaderField(":method", "CONNECT"), + CreateHeaderField(":method", "DELETE"), + CreateHeaderField(":method", "GET"), + CreateHeaderField(":method", "HEAD"), + CreateHeaderField(":method", "OPTIONS"), + CreateHeaderField(":method", "POST"), // 20 + CreateHeaderField(":method", "PUT"), + CreateHeaderField(":scheme", "http"), + CreateHeaderField(":scheme", "https"), + CreateHeaderField(":status", "103"), + CreateHeaderField(":status", "200"), + CreateHeaderField(":status", "304"), + CreateHeaderField(":status", "404"), + CreateHeaderField(":status", "503"), + CreateHeaderField("accept", "*/*"), + CreateHeaderField("accept", "application/dns-message"), // 30 + CreateHeaderField("accept-encoding", "gzip, deflate, br"), + CreateHeaderField("accept-ranges", "bytes"), + CreateHeaderField("access-control-allow-headers", "cache-control"), + CreateHeaderField("access-control-allow-origin", "content-type"), + CreateHeaderField("access-control-allow-origin", "*"), + CreateHeaderField("cache-control", "max-age=0"), + CreateHeaderField("cache-control", "max-age=2592000"), + CreateHeaderField("cache-control", "max-age=604800"), + CreateHeaderField("cache-control", "no-cache"), + CreateHeaderField("cache-control", "no-store"), // 40 + CreateHeaderField("cache-control", "public, max-age=31536000"), + CreateHeaderField("content-encoding", "br"), + CreateHeaderField("content-encoding", "gzip"), + CreateHeaderField("content-type", "application/dns-message"), + CreateHeaderField("content-type", "application/javascript"), + CreateHeaderField("content-type", "application/json"), + CreateHeaderField("content-type", "application/x-www-form-urlencoded"), + CreateHeaderField("content-type", "image/gif"), + CreateHeaderField("content-type", "image/jpeg"), + CreateHeaderField("content-type", "image/png"), // 50 + CreateHeaderField("content-type", "text/css"), + CreateHeaderField("content-type", "text/html; charset=utf-8"), + CreateHeaderField("content-type", "text/plain"), + CreateHeaderField("content-type", "text/plain;charset=utf-8"), + CreateHeaderField("range", "bytes=0-"), + CreateHeaderField("strict-transport-security", "max-age=31536000"), + CreateHeaderField("strict-transport-security", "max-age=31536000;includesubdomains"), // TODO confirm spaces here don't matter? + CreateHeaderField("strict-transport-security", "max-age=31536000;includesubdomains; preload"), + CreateHeaderField("vary", "accept-encoding"), + CreateHeaderField("vary", "origin"), // 60 + CreateHeaderField("x-content-type-options", "nosniff"), + CreateHeaderField("x-xss-protection", "1; mode=block"), + CreateHeaderField(":status", "100"), + CreateHeaderField(":status", "204"), + CreateHeaderField(":status", "206"), + CreateHeaderField(":status", "302"), + CreateHeaderField(":status", "400"), + CreateHeaderField(":status", "403"), + CreateHeaderField(":status", "421"), + CreateHeaderField(":status", "425"), // 70 + CreateHeaderField(":status", "500"), + CreateHeaderField("accept-language", ""), + CreateHeaderField("access-control-allow-credentials", "FALSE"), + CreateHeaderField("access-control-allow-credentials", "TRUE"), + CreateHeaderField("access-control-allow-headers", "*"), + CreateHeaderField("access-control-allow-methods", "get"), + CreateHeaderField("access-control-allow-methods", "get, post, options"), + CreateHeaderField("access-control-allow-methods", "options"), + CreateHeaderField("access-control-expose-headers", "content-length"), + CreateHeaderField("access-control-request-headers", "content-type"), // 80 + CreateHeaderField("access-control-request-method", "get"), + CreateHeaderField("access-control-request-method", "post"), + CreateHeaderField("alt-svc", "clear"), + CreateHeaderField("authorization", ""), + CreateHeaderField("content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"), + CreateHeaderField("early-data", "1"), + CreateHeaderField("expect-ct", ""), + CreateHeaderField("forwarded", ""), + CreateHeaderField("if-range", ""), + CreateHeaderField("origin", ""), // 90 + CreateHeaderField("purpose", "prefetch"), + CreateHeaderField("server", ""), + CreateHeaderField("timing-allow-origin", "*"), + CreateHeaderField("upgrading-insecure-requests", "1"), + CreateHeaderField("user-agent", ""), + CreateHeaderField("x-forwarded-for", ""), + CreateHeaderField("x-frame-options", "deny"), + CreateHeaderField("x-frame-options", "sameorigin"), + }; + + private static HeaderField CreateHeaderField(string name, string value) + => new HeaderField(Encoding.ASCII.GetBytes(name), Encoding.ASCII.GetBytes(value)); + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs index de8411d7f8..5c92ae816d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; @@ -11,6 +12,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; @@ -66,13 +68,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal requestProcessor = new Http2Connection(_context); _protocolSelectionState = ProtocolSelectionState.Selected; break; + case HttpProtocols.Http3: + requestProcessor = new Http3Connection(_context); + _protocolSelectionState = ProtocolSelectionState.Selected; + break; case HttpProtocols.None: // An error was already logged in SelectProtocol(), but we should close the connection. break; + default: // SelectProtocol() only returns Http1, Http2 or None. - throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2 or None."); - } + throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2, Http3 or None."); + } _requestProcessor = requestProcessor; @@ -197,6 +204,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal private HttpProtocols SelectProtocol() { + if (_context.Protocols == HttpProtocols.Http3) + { + return HttpProtocols.Http3; + } + var hasTls = _context.ConnectionFeatures.Get() != null; var applicationProtocol = _context.ConnectionFeatures.Get()?.ApplicationProtocol ?? new ReadOnlyMemory(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs index 04069db3e8..fa3c7803ed 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs @@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure public const string Http10Version = "HTTP/1.0"; public const string Http11Version = "HTTP/1.1"; public const string Http2Version = "HTTP/2"; + public const string Http3Version = "HTTP/3"; public const string HttpUriScheme = "http://"; public const string HttpsUriScheme = "https://"; diff --git a/src/Servers/Kestrel/Core/src/KestrelServer.cs b/src/Servers/Kestrel/Core/src/KestrelServer.cs index 6eb5ada99d..802232326e 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -23,26 +23,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core { private readonly List<(IConnectionListener, Task)> _transports = new List<(IConnectionListener, Task)>(); private readonly IServerAddressesFeature _serverAddresses; - private readonly IConnectionListenerFactory _transportFactory; + private readonly IEnumerable _transportFactories; private bool _hasStarted; private int _stopping; private readonly TaskCompletionSource _stoppedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - public KestrelServer(IOptions options, IConnectionListenerFactory transportFactory, ILoggerFactory loggerFactory) - : this(transportFactory, CreateServiceContext(options, loggerFactory)) + public KestrelServer(IOptions options, IEnumerable transportFactories, ILoggerFactory loggerFactory) + : this(transportFactories, CreateServiceContext(options, loggerFactory)) { } // For testing - internal KestrelServer(IConnectionListenerFactory transportFactory, ServiceContext serviceContext) + internal KestrelServer(IEnumerable transportFactories, ServiceContext serviceContext) { - if (transportFactory == null) + if (transportFactories == null) { - throw new ArgumentNullException(nameof(transportFactory)); + throw new ArgumentNullException(nameof(transportFactories)); } - _transportFactory = transportFactory; + _transportFactories = transportFactories; ServiceContext = serviceContext; Features = new FeatureCollection(); @@ -135,7 +135,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } var connectionDispatcher = new ConnectionDispatcher(ServiceContext, connectionDelegate); - var transport = await _transportFactory.BindAsync(options.EndPoint).ConfigureAwait(false); + + IConnectionListenerFactory factory = null; + if (options.Protocols >= HttpProtocols.Http3) + { + foreach (var transportFactory in _transportFactories) + { + if (transportFactory is IMultiplexedConnectionListenerFactory) + { + factory = transportFactory; + break; + } + } + + if (factory == null) + { + throw new Exception("Quic transport not found when using HTTP/3"); + } + } + else + { + factory = _transportFactories.Single(); + } + + var transport = await factory.BindAsync(options.EndPoint).ConfigureAwait(false); // Update the endpoint options.EndPoint = transport.EndPoint; diff --git a/src/Servers/Kestrel/Core/src/KestrelServerLimits.cs b/src/Servers/Kestrel/Core/src/KestrelServerLimits.cs index 10db0fe662..46a916e963 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerLimits.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerLimits.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -258,6 +258,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public Http2Limits Http2 { get; } = new Http2Limits(); + /// + /// Limits only applicable to HTTP/3 connections. + /// + public Http3Limits Http3 { get; } = new Http3Limits(); + /// /// Gets or sets the request body minimum data rate in bytes/second. /// Setting this property to null indicates no minimum data rate should be enforced. diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index c383945c98..19bbce9362 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -72,6 +72,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public KestrelConfigurationLoader ConfigurationLoader { get; set; } + /// + /// Controls whether to return the AltSvcHeader from on an HTTP/2 or lower response for HTTP/3 + /// + public bool EnableAltSvc { get; set; } = false; + /// /// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs. /// diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index 296f965b2e..67d886556f 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -79,11 +79,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal _options = options; _logger = loggerFactory.CreateLogger(); } + public async Task OnConnectionAsync(ConnectionContext context) { await Task.Yield(); bool certificateRequired; + if (context.Features.Get() != null) + { + await _next(context); + return; + } + var feature = new Core.Internal.TlsConnectionFeature(); context.Features.Set(feature); context.Features.Set(feature); diff --git a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs index 36add68a21..d29b50e1f0 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerTests.cs @@ -2,6 +2,7 @@ // 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.Linq; using System.Net; using System.Threading; @@ -203,7 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny())).Returns(mockLogger.Object); - new KestrelServer(Options.Create(null), Mock.Of(), mockLoggerFactory.Object); + new KestrelServer(Options.Create(null), Mock.Of>(), mockLoggerFactory.Object); mockLoggerFactory.Verify(factory => factory.CreateLogger("Microsoft.AspNetCore.Server.Kestrel")); } @@ -216,7 +217,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var exception = Assert.Throws(() => new KestrelServer(Options.Create(null), null, mockLoggerFactory.Object)); - Assert.Equal("transportFactory", exception.ParamName); + Assert.Equal("transportFactories", exception.ParamName); } [Fact] @@ -257,7 +258,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny())).Returns(mockLogger.Object); - var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); + var server = new KestrelServer(Options.Create(options), new List() { mockTransportFactory.Object }, mockLoggerFactory.Object); await server.StartAsync(new DummyApplication(), CancellationToken.None); var stopTask1 = server.StopAsync(default); @@ -315,7 +316,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny())).Returns(mockLogger.Object); - var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); + var server = new KestrelServer(Options.Create(options), new List() { mockTransportFactory.Object }, mockLoggerFactory.Object); await server.StartAsync(new DummyApplication(), CancellationToken.None); var stopTask1 = server.StopAsync(default); @@ -370,7 +371,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var mockLoggerFactory = new Mock(); var mockLogger = new Mock(); mockLoggerFactory.Setup(m => m.CreateLogger(It.IsAny())).Returns(mockLogger.Object); - var server = new KestrelServer(Options.Create(options), mockTransportFactory.Object, mockLoggerFactory.Object); + var server = new KestrelServer(Options.Create(options), new List() { mockTransportFactory.Object }, mockLoggerFactory.Object); await server.StartAsync(new DummyApplication(), default); var stopTask1 = server.StopAsync(default); @@ -416,7 +417,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests DebuggerWrapper.Singleton, testContext.Log); - using (var server = new KestrelServer(new MockTransportFactory(), testContext)) + using (var server = new KestrelServer(new List() { new MockTransportFactory() }, testContext)) { Assert.Null(testContext.DateHeaderValueManager.GetDateHeaderValues()); @@ -433,12 +434,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private static KestrelServer CreateServer(KestrelServerOptions options, ILogger testLogger) { - return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) })); + return new KestrelServer(Options.Create(options), new List() { new MockTransportFactory() }, new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) })); } private static KestrelServer CreateServer(KestrelServerOptions options, bool throwOnCriticalErrors = true) { - return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(throwOnCriticalErrors) })); + return new KestrelServer(Options.Create(options), new List() { new MockTransportFactory() }, new LoggerFactory(new[] { new KestrelTestLoggerProvider(throwOnCriticalErrors) })); } private static void StartDummyApplication(IServer server) diff --git a/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs b/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs new file mode 100644 index 0000000000..e35ad26b98 --- /dev/null +++ b/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs @@ -0,0 +1,46 @@ +using System; +using System.Buffers; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class VariableIntHelperTests + { + [Theory] + [MemberData(nameof(IntegerData))] + public void CheckDecoding(long expected, byte[] input) + { + var decoded = VariableLengthIntegerHelper.GetInteger(new ReadOnlySequence(input), out _, out _); + Assert.Equal(expected, decoded); + } + + [Theory] + [MemberData(nameof(IntegerData))] + public void CheckEncoding(long input, byte[] expected) + { + var outputBuffer = new Span(new byte[8]); + var encodedLength = VariableLengthIntegerHelper.WriteInteger(outputBuffer, input); + Assert.Equal(expected.Length, encodedLength); + for(var i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], outputBuffer[i]); + } + } + + public static TheoryData IntegerData + { + get + { + var data = new TheoryData(); + + data.Add(151288809941952652, new byte[] { 0xc2, 0x19, 0x7c, 0x5e, 0xff, 0x14, 0xe8, 0x8c }); + data.Add(494878333, new byte[] { 0x9d, 0x7f, 0x3e, 0x7d }); + data.Add(15293, new byte[] { 0x7b, 0xbd }); + data.Add(37, new byte[] { 0x25 }); + + return data; + } + } + } +} diff --git a/src/Servers/Kestrel/Kestrel.sln b/src/Servers/Kestrel/Kestrel.sln index 34e55eb7b2..2c80bcef71 100644 --- a/src/Servers/Kestrel/Kestrel.sln +++ b/src/Servers/Kestrel/Kestrel.sln @@ -88,7 +88,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuicSampleApp", "samples\Qu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic", "Transport.MsQuic\src\Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.csproj", "{62CFF861-807E-43F6-9403-22AA7F06C9A6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuicSampleClient", "samples\QuicSampleClient\QuicSampleClient.csproj", "{F39A942B-85A8-4C1B-A5BC-435555E79F20}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuicSampleClient", "samples\QuicSampleClient\QuicSampleClient.csproj", "{F39A942B-85A8-4C1B-A5BC-435555E79F20}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Http3SampleApp", "samples\Http3SampleApp\Http3SampleApp.csproj", "{B3CDC83A-A9C5-45DF-9828-6BC419C24308}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -508,6 +510,18 @@ Global {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x64.Build.0 = Release|Any CPU {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.ActiveCfg = Release|Any CPU {F39A942B-85A8-4C1B-A5BC-435555E79F20}.Release|x86.Build.0 = Release|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x64.ActiveCfg = Debug|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x64.Build.0 = Debug|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x86.ActiveCfg = Debug|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Debug|x86.Build.0 = Debug|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|Any CPU.Build.0 = Release|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x64.ActiveCfg = Release|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x64.Build.0 = Release|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x86.ActiveCfg = Release|Any CPU + {B3CDC83A-A9C5-45DF-9828-6BC419C24308}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -548,6 +562,7 @@ Global {53A8634C-DFC5-4A5B-8864-9EF1707E3F18} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0} {62CFF861-807E-43F6-9403-22AA7F06C9A6} = {2B456D08-F72B-4EB8-B663-B6D78FC04BF8} {F39A942B-85A8-4C1B-A5BC-435555E79F20} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0} + {B3CDC83A-A9C5-45DF-9828-6BC419C24308} = {F826BA61-60A9-45B6-AF29-FD1A6E313EF0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {48207B50-7D05-4B10-B585-890FE0F4FCE1} diff --git a/src/Servers/Kestrel/Transport.MsQuic/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.netcoreapp.cs b/src/Servers/Kestrel/Transport.MsQuic/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.netcoreapp.cs index 56555304f1..979742fbd3 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.netcoreapp.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/ref/Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.netcoreapp.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic [System.Diagnostics.DebuggerStepThroughAttribute] public System.Threading.Tasks.ValueTask ConnectAsync(System.Net.EndPoint endPoint, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } - public partial class MsQuicTransportFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory + public partial class MsQuicTransportFactory : Microsoft.AspNetCore.Connections.IConnectionListenerFactory, Microsoft.AspNetCore.Connections.IMultiplexedConnectionListenerFactory { public MsQuicTransportFactory(Microsoft.Extensions.Hosting.IHostApplicationLifetime applicationLifetime, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions options) { } [System.Diagnostics.DebuggerStepThroughAttribute] diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicApi.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicApi.cs index 0bcbecbc94..ae17ff5eb2 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicApi.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicApi.cs @@ -163,6 +163,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal Buffer.Buffer); } + internal unsafe uint UnsafeGetParam( + IntPtr Handle, + uint Level, + uint Param, + ref MsQuicNativeMethods.QuicBuffer Buffer) + { + return GetParamDelegate( + Handle, + Level, + Param, + out Buffer.Length, + out Buffer.Buffer); + } + public async ValueTask CreateSecurityConfig(X509Certificate2 certificate) { QuicSecConfig secConfig = null; diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnection.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnection.cs index 617fa31caf..b1b10ae1dd 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnection.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnection.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Abstractions.Features; using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http.Features; using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQuicNativeMethods; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal @@ -38,6 +39,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal SetIdleTimeout(_context.Options.IdleTimeout); + Features.Set(new FakeTlsConnectionFeature()); Features.Set(this); Features.Set(this); diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnectionListener.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnectionListener.cs index 44a0434727..27e2dd5912 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicConnectionListener.cs @@ -99,6 +99,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal _session.SetPeerUnidirectionalStreamCount(_transportContext.Options.MaxBidirectionalStreamCount); var address = MsQuicNativeMethods.Convert(EndPoint as IPEndPoint); + MsQuicStatusException.ThrowIfFailed(_api.ListenerStartDelegate( _nativeObjPtr, ref address)); @@ -111,6 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal { case QUIC_LISTENER_EVENT.NEW_CONNECTION: { + evt.Data.NewConnection.SecurityConfig = _secConfig.NativeObjPtr; var msQuicConnection = new MsQuicConnection(_api, _transportContext, evt.Data.NewConnection.Connection); _acceptConnectionQueue.Writer.TryWrite(msQuicConnection); diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicNativeMethods.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicNativeMethods.cs index 8d583e13e8..d5512c5687 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicNativeMethods.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicNativeMethods.cs @@ -82,8 +82,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal IntPtr Handle, uint Level, uint Param, - IntPtr BufferLength, - IntPtr Buffer); + out uint BufferLength, + out byte* Buffer); internal delegate uint RegistrationOpenDelegate(byte[] appName, out IntPtr RegistrationContext); @@ -533,36 +533,39 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal { var socketAddress = new SOCKADDR_INET(); var buffer = endpoint.Address.GetAddressBytes(); - switch (endpoint.Address.AddressFamily) + if (endpoint.Address != IPAddress.Any && endpoint.Address != IPAddress.IPv6Any) { - case AddressFamily.InterNetwork: - socketAddress.Ipv4.sin_addr0 = buffer[0]; - socketAddress.Ipv4.sin_addr1 = buffer[1]; - socketAddress.Ipv4.sin_addr2 = buffer[2]; - socketAddress.Ipv4.sin_addr3 = buffer[3]; - socketAddress.Ipv4.sin_family = IPv4; - break; - case AddressFamily.InterNetworkV6: - socketAddress.Ipv6.sin6_addr0 = buffer[0]; - socketAddress.Ipv6.sin6_addr1 = buffer[1]; - socketAddress.Ipv6.sin6_addr2 = buffer[2]; - socketAddress.Ipv6.sin6_addr3 = buffer[3]; - socketAddress.Ipv6.sin6_addr4 = buffer[4]; - socketAddress.Ipv6.sin6_addr5 = buffer[5]; - socketAddress.Ipv6.sin6_addr6 = buffer[6]; - socketAddress.Ipv6.sin6_addr7 = buffer[7]; - socketAddress.Ipv6.sin6_addr8 = buffer[8]; - socketAddress.Ipv6.sin6_addr9 = buffer[9]; - socketAddress.Ipv6.sin6_addr10 = buffer[10]; - socketAddress.Ipv6.sin6_addr11 = buffer[11]; - socketAddress.Ipv6.sin6_addr12 = buffer[12]; - socketAddress.Ipv6.sin6_addr13 = buffer[13]; - socketAddress.Ipv6.sin6_addr14 = buffer[14]; - socketAddress.Ipv6.sin6_addr15 = buffer[15]; - socketAddress.Ipv6.sin6_family = IPv6; - break; - default: - throw new ArgumentException("Only IPv4 or IPv6 are supported"); + switch (endpoint.Address.AddressFamily) + { + case AddressFamily.InterNetwork: + socketAddress.Ipv4.sin_addr0 = buffer[0]; + socketAddress.Ipv4.sin_addr1 = buffer[1]; + socketAddress.Ipv4.sin_addr2 = buffer[2]; + socketAddress.Ipv4.sin_addr3 = buffer[3]; + socketAddress.Ipv4.sin_family = IPv4; + break; + case AddressFamily.InterNetworkV6: + socketAddress.Ipv6.sin6_addr0 = buffer[0]; + socketAddress.Ipv6.sin6_addr1 = buffer[1]; + socketAddress.Ipv6.sin6_addr2 = buffer[2]; + socketAddress.Ipv6.sin6_addr3 = buffer[3]; + socketAddress.Ipv6.sin6_addr4 = buffer[4]; + socketAddress.Ipv6.sin6_addr5 = buffer[5]; + socketAddress.Ipv6.sin6_addr6 = buffer[6]; + socketAddress.Ipv6.sin6_addr7 = buffer[7]; + socketAddress.Ipv6.sin6_addr8 = buffer[8]; + socketAddress.Ipv6.sin6_addr9 = buffer[9]; + socketAddress.Ipv6.sin6_addr10 = buffer[10]; + socketAddress.Ipv6.sin6_addr11 = buffer[11]; + socketAddress.Ipv6.sin6_addr12 = buffer[12]; + socketAddress.Ipv6.sin6_addr13 = buffer[13]; + socketAddress.Ipv6.sin6_addr14 = buffer[14]; + socketAddress.Ipv6.sin6_addr15 = buffer[15]; + socketAddress.Ipv6.sin6_family = IPv6; + break; + default: + throw new ArgumentException("Only IPv4 or IPv6 are supported"); + } } SetPort(endpoint.Address.AddressFamily, ref socketAddress, endpoint.Port); diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicStream.cs b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicStream.cs index ddb2891ed4..a384455f45 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicStream.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/src/Internal/MsQuicStream.cs @@ -17,7 +17,7 @@ using static Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal.MsQui namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal { - internal class MsQuicStream : TransportConnection, IUnidirectionalStreamFeature + internal class MsQuicStream : TransportConnection, IQuicStreamFeature { private Task _processingTask; private MsQuicConnection _connection; @@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal private GCHandle _handle; private StreamCallbackDelegate _delegate; private string _connectionId; + private long _streamId = -1; internal ResettableCompletionSource _resettableCompletion; private MemoryHandle[] _bufferArrays; @@ -56,15 +57,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); - // TODO when stream is unidirectional, don't create an output pipe. - if (flags.HasFlag(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL)) - { - Features.Set(this); - } + Features.Set(this); // TODO populate the ITlsConnectionFeature (requires client certs). - var feature = new FakeTlsConnectionFeature(); - Features.Set(feature); + Features.Set(new FakeTlsConnectionFeature()); + if (flags.HasFlag(QUIC_STREAM_OPEN_FLAG.UNIDIRECTIONAL)) + { + IsUnidirectional = true; + } Transport = pair.Transport; Application = pair.Application; @@ -72,21 +72,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal SetCallbackHandler(); _processingTask = ProcessSends(); - - // Concatenate stream id with ConnectionId. - _log.NewStream(ConnectionId); } public override MemoryPool MemoryPool { get; } public PipeWriter Input => Application.Output; public PipeReader Output => Application.Input; + public bool IsUnidirectional { get; } + + public long StreamId + { + get + { + if (_streamId == -1) + { + _streamId = GetStreamId(); + } + + return _streamId; + } + } + public override string ConnectionId { get { if (_connectionId == null) { - _connectionId = $"{_connection.ConnectionId}:{base.ConnectionId}"; + _connectionId = $"{_connection.ConnectionId}:{StreamId}"; } return _connectionId; } @@ -390,20 +402,42 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic.Internal SetParam(QUIC_PARAM_STREAM.RECEIVE_ENABLED, buffer); } + private unsafe long GetStreamId() + { + byte* ptr = stackalloc byte[sizeof(long)]; + var buffer = new QuicBuffer + { + Length = sizeof(long), + Buffer = ptr + }; + GetParam(QUIC_PARAM_STREAM.ID, buffer); + return *(long*)ptr; + } + + private void GetParam( + QUIC_PARAM_STREAM param, + QuicBuffer buf) + { + MsQuicStatusException.ThrowIfFailed(Api.UnsafeGetParam( + _nativeObjPtr, + (uint)QUIC_PARAM_LEVEL.STREAM, + (uint)param, + ref buf)); + } + private void SetParam( QUIC_PARAM_STREAM param, QuicBuffer buf) { MsQuicStatusException.ThrowIfFailed(Api.UnsafeSetParam( _nativeObjPtr, - (uint)QUIC_PARAM_LEVEL.SESSION, + (uint)QUIC_PARAM_LEVEL.STREAM, (uint)param, buf)); } ~MsQuicStream() { - _log.LogDebug("Destructor"); Dispose(false); } diff --git a/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportFactory.cs b/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportFactory.cs index 2871fb5612..c3200982c5 100644 --- a/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportFactory.cs +++ b/src/Servers/Kestrel/Transport.MsQuic/src/MsQuicTransportFactory.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.MsQuic { - public class MsQuicTransportFactory : IConnectionListenerFactory + public class MsQuicTransportFactory : IMultiplexedConnectionListenerFactory { private MsQuicTrace _log; private IHostApplicationLifetime _applicationLifetime; diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Http3SampleApp.csproj b/src/Servers/Kestrel/samples/Http3SampleApp/Http3SampleApp.csproj new file mode 100644 index 0000000000..4bc61b8465 --- /dev/null +++ b/src/Servers/Kestrel/samples/Http3SampleApp/Http3SampleApp.csproj @@ -0,0 +1,14 @@ + + + + $(DefaultNetCoreTargetFramework) + false + + + + + + + + + diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs new file mode 100644 index 0000000000..9b39054948 --- /dev/null +++ b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Http3SampleApp +{ + public class Program + { + public static void Main(string[] args) + { + var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, true); + var hostBuilder = new HostBuilder() + .ConfigureLogging((_, factory) => + { + factory.SetMinimumLevel(LogLevel.Trace); + factory.AddConsole(); + }) + .ConfigureWebHost(webHost => + { + webHost.UseKestrel() + // Things like APLN and cert should be able to be passed from corefx into bedrock + .UseMsQuic(options => + { + options.Certificate = cert; + options.RegistrationName = "Quic"; + options.Alpn = "h3-24"; + options.IdleTimeout = TimeSpan.FromHours(1); + }) + .ConfigureKestrel((context, options) => + { + var basePort = 5555; + + options.Listen(IPAddress.Any, basePort, listenOptions => + { + listenOptions.UseHttps(); + listenOptions.Protocols = HttpProtocols.Http3; + }); + }) + .UseStartup(); + }); + + hostBuilder.Build().Run(); + } + } +} diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs b/src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs new file mode 100644 index 0000000000..0434605cfc --- /dev/null +++ b/src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Http3SampleApp +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + app.Run(async context => + { + var memory = new Memory(new byte[4096]); + var length = await context.Request.Body.ReadAsync(memory); + context.Response.Headers["test"] = "foo"; + // for testing + await context.Response.WriteAsync("Hello World! " + context.Request.Protocol); + }); + } + } +} diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/appsettings.Development.json b/src/Servers/Kestrel/samples/Http3SampleApp/appsettings.Development.json new file mode 100644 index 0000000000..e203e9407e --- /dev/null +++ b/src/Servers/Kestrel/samples/Http3SampleApp/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/appsettings.json b/src/Servers/Kestrel/samples/Http3SampleApp/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/src/Servers/Kestrel/samples/Http3SampleApp/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs b/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs index fa4156c09e..e5600d06ff 100644 --- a/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.Extensions.Logging; using System.Diagnostics; +using Microsoft.AspNetCore.Server.Kestrel.Core; namespace QuicSampleApp { @@ -46,6 +47,7 @@ namespace QuicSampleApp options.Listen(IPAddress.Any, basePort, listenOptions => { + listenOptions.Protocols = HttpProtocols.Http3; listenOptions.Use((next) => { return async connection => diff --git a/src/Servers/Kestrel/samples/QuicSampleApp/appsettings.Development.json b/src/Servers/Kestrel/samples/QuicSampleApp/appsettings.Development.json new file mode 100644 index 0000000000..e203e9407e --- /dev/null +++ b/src/Servers/Kestrel/samples/QuicSampleApp/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Servers/Kestrel/samples/QuicSampleApp/appsettings.json b/src/Servers/Kestrel/samples/QuicSampleApp/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/src/Servers/Kestrel/samples/QuicSampleApp/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs b/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs index 48f4be9e94..b1b656cc4e 100644 --- a/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs +++ b/src/Servers/Kestrel/samples/QuicSampleClient/Program.cs @@ -53,6 +53,8 @@ namespace QuicSampleClient public async Task RunAsync() { + var start = Console.ReadLine(); + Console.WriteLine("Starting"); var connectionContext = await _connectionFactory.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5555)); var createStreamFeature = connectionContext.Features.Get(); var streamContext = await createStreamFeature.StartBidirectionalStreamAsync(); diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index 69058b75c5..f26fb786d5 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -145,6 +145,7 @@ namespace CodeGenerator { "Accept-Ranges", "Age", + "Alt-Svc", "ETag", "Location", "Proxy-Authenticate", diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index d79a6d9055..7ff959bb1f 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs @@ -2,6 +2,7 @@ // 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.Linq; using System.Net; using System.Reflection; @@ -89,7 +90,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests c.Configure(context.ServerOptions); } - return new KestrelServer(sp.GetRequiredService(), context); + return new KestrelServer(new List() { sp.GetRequiredService() }, context); }); configureServices(services); }) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs new file mode 100644 index 0000000000..d56624e4f9 --- /dev/null +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class Http3StreamTests : Http3TestBase + { + [Fact] + public async Task HelloWorldTest() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers); + await requestStream.SendDataAsync(Encoding.ASCII.GetBytes("Hello world")); + + var responseHeaders = await requestStream.ExpectHeadersAsync(); + var responseData = await requestStream.ExpectDataAsync(); + Assert.Equal("Hello world", Encoding.ASCII.GetString(responseData.ToArray())); + } + } +} diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs new file mode 100644 index 0000000000..aab3b504d9 --- /dev/null +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -0,0 +1,386 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Net.Http; +using System.Reflection; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Abstractions.Features; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Testing; +using Moq; +using Xunit; +using Xunit.Abstractions; +using static Microsoft.AspNetCore.Server.Kestrel.Core.Tests.Http2TestBase; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable, IQuicCreateStreamFeature, IQuicStreamListenerFeature + { + internal TestServiceContext _serviceContext; + internal Http3Connection _connection; + internal readonly TimeoutControl _timeoutControl; + internal readonly Mock _mockKestrelTrace = new Mock(); + protected readonly Mock _mockConnectionContext = new Mock(); + internal readonly Mock _mockTimeoutHandler = new Mock(); + internal readonly Mock _mockTimeoutControl; + internal readonly MemoryPool _memoryPool = SlabMemoryPoolFactory.Create(); + protected Task _connectionTask; + protected readonly RequestDelegate _echoApplication; + + private readonly Channel _acceptConnectionQueue = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + + public Http3TestBase() + { + _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object); + _mockTimeoutControl = new Mock(_timeoutControl) { CallBase = true }; + _timeoutControl.Debugger = Mock.Of(); + _echoApplication = async context => + { + var buffer = new byte[Http3PeerSettings.MinAllowedMaxFrameSize]; + var received = 0; + + while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await context.Response.Body.WriteAsync(buffer, 0, received); + } + }; + } + + public override void Initialize(TestContext context, MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper) + { + base.Initialize(context, methodInfo, testMethodArguments, testOutputHelper); + + _serviceContext = new TestServiceContext(LoggerFactory, _mockKestrelTrace.Object) + { + Scheduler = PipeScheduler.Inline, + }; + } + + protected async Task InitializeConnectionAsync(RequestDelegate application) + { + if (_connection == null) + { + CreateConnection(); + } + + _connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(application)); + + await Task.CompletedTask; + } + + internal async ValueTask InitializeConnectionAndStreamsAsync(RequestDelegate application) + { + await InitializeConnectionAsync(application); + + var controlStream1 = await CreateControlStream(0); + var controlStream2 = await CreateControlStream(2); + var controlStream3 = await CreateControlStream(3); + + return await CreateRequestStream(); + } + + protected void CreateConnection() + { + var limits = _serviceContext.ServerOptions.Limits; + + var features = new FeatureCollection(); + features.Set(this); + features.Set(this); + + var httpConnectionContext = new HttpConnectionContext + { + ConnectionContext = _mockConnectionContext.Object, + ConnectionFeatures = features, + ServiceContext = _serviceContext, + MemoryPool = _memoryPool, + Transport = null, // Make sure it's null + TimeoutControl = _mockTimeoutControl.Object + }; + + _connection = new Http3Connection(httpConnectionContext); + var httpConnection = new HttpConnection(httpConnectionContext); + httpConnection.Initialize(_connection); + _mockTimeoutHandler.Setup(h => h.OnTimeout(It.IsAny())) + .Callback(r => httpConnection.OnTimeout(r)); + } + + private static PipeOptions GetInputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler writerScheduler) => new PipeOptions + ( + pool: memoryPool, + readerScheduler: serviceContext.Scheduler, + writerScheduler: writerScheduler, + pauseWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, + resumeWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, + useSynchronizationContext: false, + minimumSegmentSize: memoryPool.GetMinimumSegmentSize() + ); + + private static PipeOptions GetOutputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler readerScheduler) => new PipeOptions + ( + pool: memoryPool, + readerScheduler: readerScheduler, + writerScheduler: serviceContext.Scheduler, + pauseWriterThreshold: GetOutputResponseBufferSize(serviceContext), + resumeWriterThreshold: GetOutputResponseBufferSize(serviceContext), + useSynchronizationContext: false, + minimumSegmentSize: memoryPool.GetMinimumSegmentSize() + ); + + private static long GetOutputResponseBufferSize(ServiceContext serviceContext) + { + var bufferSize = serviceContext.ServerOptions.Limits.MaxResponseBufferSize; + if (bufferSize == 0) + { + // 0 = no buffering so we need to configure the pipe so the writer waits on the reader directly + return 1; + } + + // null means that we have no back pressure + return bufferSize ?? 0; + } + + public ValueTask StartUnidirectionalStreamAsync() + { + var stream = new Http3ControlStream(this, _connection); + // TODO put these somewhere to be read. + return new ValueTask(stream.ConnectionContext); + } + + public async ValueTask AcceptAsync() + { + while (await _acceptConnectionQueue.Reader.WaitToReadAsync()) + { + while (_acceptConnectionQueue.Reader.TryRead(out var connection)) + { + return connection; + } + } + + return null; + } + + internal async ValueTask CreateControlStream(int id) + { + var stream = new Http3ControlStream(this, _connection); + _acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext); + await stream.WriteStreamIdAsync(id); + return stream; + } + + internal ValueTask CreateRequestStream() + { + var stream = new Http3RequestStream(this, _connection); + _acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext); + return new ValueTask(stream); + } + + public ValueTask StartBidirectionalStreamAsync() + { + var stream = new Http3RequestStream(this, _connection); + // TODO put these somewhere to be read. + return new ValueTask(stream.ConnectionContext); + } + + internal class Http3StreamBase + { + protected DuplexPipe.DuplexPipePair _pair; + protected Http3TestBase _testBase; + protected Http3Connection _connection; + + protected Task SendAsync(ReadOnlySpan span) + { + var writableBuffer = _pair.Application.Output; + writableBuffer.Write(span); + return FlushAsync(writableBuffer); + } + + protected static async Task FlushAsync(PipeWriter writableBuffer) + { + await writableBuffer.FlushAsync().AsTask().DefaultTimeout(); + } + } + + internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler, IQuicStreamFeature + { + internal ConnectionContext ConnectionContext { get; } + + public bool IsUnidirectional => false; + + public long StreamId => 0; + + private readonly byte[] _headerEncodingBuffer = new byte[Http3PeerSettings.MinAllowedMaxFrameSize]; + private QPackEncoder _qpackEncoder = new QPackEncoder(); + private QPackDecoder _qpackDecoder = new QPackDecoder(10000, 10000); + private long _bytesReceived; + protected readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public Http3RequestStream(Http3TestBase testBase, Http3Connection connection) + { + _testBase = testBase; + _connection = connection; + var inputPipeOptions = GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); + var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); + + _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); + + ConnectionContext = new DefaultConnectionContext(); + ConnectionContext.Transport = _pair.Transport; + ConnectionContext.Features.Set(this); + } + + public async Task SendHeadersAsync(IEnumerable> headers) + { + var outputWriter = _pair.Application.Output; + var frame = new Http3Frame(); + frame.PrepareHeaders(); + var buffer = _headerEncodingBuffer.AsMemory(); + var done = _qpackEncoder.BeginEncode(headers, buffer.Span, out var length); + frame.Length = length; + // TODO may want to modify behavior of input frames to mock different client behavior (client can send anything). + Http3FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(buffer.Span.Slice(0, length)); + return done; + } + + internal async Task SendDataAsync(Memory data) + { + var outputWriter = _pair.Application.Output; + var frame = new Http3Frame(); + frame.PrepareData(); + frame.Length = data.Length; + Http3FrameWriter.WriteHeader(frame, outputWriter); + await SendAsync(data.Span); + } + + internal async Task>> ExpectHeadersAsync() + { + var http3WithPayload = await ReceiveFrameAsync(); + _qpackDecoder.Decode(http3WithPayload.PayloadSequence, this); + return _decodedHeaders; + } + + internal async Task> ExpectDataAsync() + { + var http3WithPayload = await ReceiveFrameAsync(); + return http3WithPayload.Payload; + } + + internal async Task ReceiveFrameAsync(uint maxFrameSize = Http3PeerSettings.DefaultMaxFrameSize) + { + var frame = new Http3FrameWithPayload(); + + while (true) + { + var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); + var buffer = result.Buffer; + var consumed = buffer.Start; + var examined = buffer.Start; + var copyBuffer = buffer; + + try + { + Assert.True(buffer.Length > 0); + + if (Http3FrameReader.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 += copyBuffer.Slice(copyBuffer.Start, consumed).Length; + _pair.Application.Input.AdvanceTo(consumed, examined); + } + } + } + + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) + { + _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); + } + + public void OnHeadersComplete(bool endHeaders) + { + } + } + + internal class Http3FrameWithPayload : Http3Frame + { + public Http3FrameWithPayload() : base() + { + } + + // This does not contain extended headers + public Memory Payload { get; set; } + + public ReadOnlySequence PayloadSequence => new ReadOnlySequence(Payload); + } + + + internal class Http3ControlStream : Http3StreamBase, IQuicStreamFeature + { + internal ConnectionContext ConnectionContext { get; } + + public bool IsUnidirectional => true; + + // TODO + public long StreamId => 0; + + public Http3ControlStream(Http3TestBase testBase, Http3Connection connection) + { + _testBase = testBase; + _connection = connection; + var inputPipeOptions = GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); + var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); + + _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); + + ConnectionContext = new DefaultConnectionContext(); + ConnectionContext.Transport = _pair.Transport; + ConnectionContext.Features.Set(this); + } + + public async Task WriteStreamIdAsync(int id) + { + var writableBuffer = _pair.Application.Output; + + void WriteSpan(PipeWriter pw) + { + var buffer = pw.GetSpan(sizeHint: 8); + var lengthWritten = VariableLengthIntegerHelper.WriteInteger(buffer, id); + pw.Advance(lengthWritten); + } + + WriteSpan(writableBuffer); + + await FlushAsync(writableBuffer); + } + } + } +} diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj index bab7248341..c985b7d77d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index 1c76309881..0c8e40921d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -3,11 +3,13 @@ using System; using System.Buffers; +using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; @@ -80,7 +82,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTrans { context.ServerOptions.ApplicationServices = sp; configureKestrel(context.ServerOptions); - return new KestrelServer(_transportFactory, context); + return new KestrelServer(new List() { _transportFactory }, context); }); });