From 6042fab58103315524bfeb102a351201c41f2a52 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Tue, 11 Feb 2020 11:17:42 -0800 Subject: [PATCH] Replat on HTTP/3 changes, fixing up minor nits to be compatible with h3-25 (#18912) --- ...pNetCore.Server.Kestrel.Core.netcoreapp.cs | 2 + src/Servers/Kestrel/Core/src/CoreStrings.resx | 2 +- .../src/Internal/Http/Http1ParsingHandler.cs | 10 + .../Core/src/Internal/Http/HttpProtocol.cs | 13 + .../src/Internal/Http2/Http2Connection.cs | 10 + .../Internal/Http3/Frames/Http3FrameType.cs | 17 - ...tp3Frame.Data.cs => Http3RawFrame.Data.cs} | 4 +- ...rame.GoAway.cs => Http3RawFrame.GoAway.cs} | 4 +- ...me.Headers.cs => Http3RawFrame.Headers.cs} | 4 +- ....Settings.cs => Http3RawFrame.Settings.cs} | 4 +- .../{Http3Frame.cs => Http3RawFrame.cs} | 4 +- .../Helpers/VariableLengthIntegerHelper.cs | 115 ---- .../src/Internal/Http3/Http3Connection.cs | 65 ++- .../src/Internal/Http3/Http3ControlStream.cs | 34 +- .../src/Internal/Http3/Http3FrameReader.cs | 3 +- .../src/Internal/Http3/Http3FrameWriter.cs | 33 +- .../src/Internal/Http3/Http3OutputProducer.cs | 8 +- .../Core/src/Internal/Http3/Http3Stream.cs | 72 ++- .../Http3/Http3StreamErrorException.cs | 1 + .../src/Internal/Http3/QPack/DynamicTable.cs | 1 + .../Http3/QPack/EncoderStreamReader.cs | 5 +- .../src/Internal/Http3/QPack/HeaderField.cs | 27 - .../Http3/QPack/QPackDecodingException.cs | 28 - .../src/Internal/Http3/QPack/QPackEncoder.cs | 504 ------------------ .../Http3/QPack/QPackEncodingException.cs | 19 - src/Servers/Kestrel/Core/src/KestrelServer.cs | 8 +- ...soft.AspNetCore.Server.Kestrel.Core.csproj | 1 + .../Kestrel/Core/test/HttpParserTests.cs | 10 + .../Core/test/VariableIntHelperTests.cs | 2 +- src/Servers/Kestrel/Kestrel.sln | 2 +- .../src/Internal/QuicConnectionContext.cs | 12 + .../src/Internal/QuicConnectionListener.cs | 12 + .../src/Internal/QuicStreamContext.cs | 15 +- .../Http1ConnectionBenchmark.cs | 10 + .../HttpParserBenchmark.cs | 20 + .../Http3SampleApp/Http3SampleApp.csproj | 8 +- .../Kestrel/samples/Http3SampleApp/Program.cs | 21 +- .../Kestrel/samples/QuicSampleApp/Program.cs | 1 - .../Http2/Http2TestBase.cs | 10 + .../Http3/Http3StreamTests.cs | 21 + .../Http3/Http3TestBase.cs | 32 +- .../{StaticTable.cs => H2StaticTable.cs} | 2 +- src/Shared/Http2/Hpack/HPackDecoder.cs | 6 +- src/Shared/Http2/Hpack/HPackEncoder.cs | 2 +- src/Shared/Http2/Hpack/Huffman.cs | 3 +- src/Shared/Http2/Hpack/IntegerEncoder.cs | 5 + src/Shared/Http2/IHttpHeadersHandler.cs | 2 + src/Shared/Http2/ReadMe.SharedCode.md | 18 +- src/Shared/Http2cat/Http2Utilities.cs | 10 + src/Shared/Http3/CopyToAspNetCore.cmd | 14 + src/Shared/Http3/CopyToAspNetCore.sh | 19 + src/Shared/Http3/CopyToRuntime.cmd | 14 + src/Shared/Http3/CopyToRuntime.sh | 19 + .../Http3/Frames/Http3ErrorCode.cs | 67 +-- src/Shared/Http3/Frames/Http3Frame.cs | 62 +++ src/Shared/Http3/Frames/Http3FrameType.cs | 18 + .../Helpers/VariableLengthIntegerHelper.cs | 209 ++++++++ src/Shared/Http3/Http3SettingType.cs | 30 ++ src/Shared/Http3/Http3StreamType.cs | 32 ++ .../Http3/QPack/H3StaticTable.cs} | 83 ++- src/Shared/Http3/QPack/HeaderField.cs | 21 + .../Http3/QPack/QPackDecoder.cs | 376 +++++++------ .../Http3/QPack/QPackDecodingException.cs | 28 + src/Shared/Http3/QPack/QPackEncoder.cs | 428 +++++++++++++++ .../Http3/QPack/QPackEncodingException.cs | 25 + src/Shared/Http3/ReadMe.SharedCode.md | 36 ++ .../Shared.Tests/Http2/HPackDecoderTest.cs | 12 + 67 files changed, 1647 insertions(+), 1068 deletions(-) delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3FrameType.cs rename src/Servers/Kestrel/Core/src/Internal/Http3/Frames/{Http3Frame.Data.cs => Http3RawFrame.Data.cs} (74%) rename src/Servers/Kestrel/Core/src/Internal/Http3/Frames/{Http3Frame.GoAway.cs => Http3RawFrame.GoAway.cs} (74%) rename src/Servers/Kestrel/Core/src/Internal/Http3/Frames/{Http3Frame.Headers.cs => Http3RawFrame.Headers.cs} (74%) rename src/Servers/Kestrel/Core/src/Internal/Http3/Frames/{Http3Frame.Settings.cs => Http3RawFrame.Settings.cs} (74%) rename src/Servers/Kestrel/Core/src/Internal/Http3/Frames/{Http3Frame.cs => Http3RawFrame.cs} (79%) delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/Helpers/VariableLengthIntegerHelper.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/HeaderField.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecodingException.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncoder.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncodingException.cs rename src/Shared/Http2/Hpack/{StaticTable.cs => H2StaticTable.cs} (99%) create mode 100644 src/Shared/Http3/CopyToAspNetCore.cmd create mode 100644 src/Shared/Http3/CopyToAspNetCore.sh create mode 100644 src/Shared/Http3/CopyToRuntime.cmd create mode 100644 src/Shared/Http3/CopyToRuntime.sh rename src/{Servers/Kestrel/Core/src/Internal => Shared}/Http3/Frames/Http3ErrorCode.cs (62%) create mode 100644 src/Shared/Http3/Frames/Http3Frame.cs create mode 100644 src/Shared/Http3/Frames/Http3FrameType.cs create mode 100644 src/Shared/Http3/Helpers/VariableLengthIntegerHelper.cs create mode 100644 src/Shared/Http3/Http3SettingType.cs create mode 100644 src/Shared/Http3/Http3StreamType.cs rename src/{Servers/Kestrel/Core/src/Internal/Http3/QPack/StaticTable.cs => Shared/Http3/QPack/H3StaticTable.cs} (68%) create mode 100644 src/Shared/Http3/QPack/HeaderField.cs rename src/{Servers/Kestrel/Core/src/Internal => Shared}/Http3/QPack/QPackDecoder.cs (54%) create mode 100644 src/Shared/Http3/QPack/QPackDecodingException.cs create mode 100644 src/Shared/Http3/QPack/QPackEncoder.cs create mode 100644 src/Shared/Http3/QPack/QPackEncodingException.cs create mode 100644 src/Shared/Http3/ReadMe.SharedCode.md 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 5bd3bcd410..4bc6818046 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 @@ -239,6 +239,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { void OnHeader(System.ReadOnlySpan name, System.ReadOnlySpan value); void OnHeadersComplete(bool endStream); + void OnStaticIndexedHeader(int index); + void OnStaticIndexedHeader(int index, System.ReadOnlySpan value); } public partial interface IHttpRequestLineHandler { diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index 3a25467bcc..20c0b928cb 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -581,4 +581,4 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Unable to resolve service for type 'Microsoft.AspNetCore.Connections.IConnectionListenerFactory' while attempting to activate 'Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer'. - \ No newline at end of file + diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs index f76ab52a8f..322e46190d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs @@ -49,5 +49,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) => Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded); + + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index f4419c44c7..4e6cd3f8f0 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -1192,6 +1192,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } + // TODO allow customization of this. + if (ServerOptions.EnableAltSvc && _httpVersion < Http.HttpVersion.Http3) + { + foreach (var option in ServerOptions.ListenOptions) + { + if (option.Protocols == HttpProtocols.Http3) + { + responseHeaders.HeaderAltSvc = $"h3-25=\":{option.IPEndPoint.Port}\"; ma=84600"; + break; + } + } + } + if (ServerOptions.AddServerHeader && !responseHeaders.HasServer) { responseHeaders.SetRawServer(Constants.ServerName, _bytesServer); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 5ef8372da3..eae49b48a1 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -1375,6 +1375,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } + private class StreamCloseAwaitable : ICriticalNotifyCompletion { private static readonly Action _callbackCompleted = () => { }; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3FrameType.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3FrameType.cs deleted file mode 100644 index c690567b06..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3FrameType.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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/Frames/Http3Frame.Data.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs similarity index 74% rename from src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Data.cs rename to src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs index 536147b1fb..b852ed8b5b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Data.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Data.cs @@ -1,9 +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.Server.Kestrel.Core.Internal.Http3 +namespace System.Net.Http { - internal partial class Http3Frame + internal partial class Http3RawFrame { public void PrepareData() { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.GoAway.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs similarity index 74% rename from src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.GoAway.cs rename to src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs index a1984d93fb..53bdc4d4bd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.GoAway.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.GoAway.cs @@ -1,9 +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.Server.Kestrel.Core.Internal.Http3 +namespace System.Net.Http { - internal partial class Http3Frame + internal partial class Http3RawFrame { public void PrepareGoAway() { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Headers.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs similarity index 74% rename from src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Headers.cs rename to src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs index bc582d13bb..9913c010bd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Headers.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Headers.cs @@ -1,9 +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.Server.Kestrel.Core.Internal.Http3 +namespace System.Net.Http { - internal partial class Http3Frame + internal partial class Http3RawFrame { public void PrepareHeaders() { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Settings.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs similarity index 74% rename from src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Settings.cs rename to src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs index 1d06d5e403..a90902470d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.Settings.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.Settings.cs @@ -1,9 +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.Server.Kestrel.Core.Internal.Http3 +namespace System.Net.Http { - internal partial class Http3Frame + internal partial class Http3RawFrame { public void PrepareSettings() { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs similarity index 79% rename from src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.cs rename to src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs index a7dff3a0a1..f174f4b326 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3Frame.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3RawFrame.cs @@ -1,9 +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.Server.Kestrel.Core.Internal.Http3 +namespace System.Net.Http { - internal partial class Http3Frame + internal partial class Http3RawFrame { public long Length { get; set; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Helpers/VariableLengthIntegerHelper.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Helpers/VariableLengthIntegerHelper.cs deleted file mode 100644 index c63c908ab9..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Helpers/VariableLengthIntegerHelper.cs +++ /dev/null @@ -1,115 +0,0 @@ -// 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 index 070931ad42..9c1c4e888f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Concurrent; using System.Net; +using System.Net.Http; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -20,15 +22,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 public DynamicTable DynamicTable { get; set; } - public Http3ControlStream SettingsStream { get; set; } + public Http3ControlStream ControlStream { 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; + private volatile bool _haveSentGoAway; + private object _sync = new object(); public Http3Connection(HttpConnectionContext context) { @@ -56,25 +58,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 var streamListenerFeature = Context.ConnectionFeatures.Get(); // Start other three unidirectional streams here. - var settingsStream = CreateSettingsStream(application); - var encoderStream = CreateEncoderStream(application); - var decoderStream = CreateDecoderStream(application); + var controlTask = CreateControlStream(application); + var encoderTask = CreateEncoderStream(application); + var decoderTask = CreateDecoderStream(application); try { while (true) { var connectionContext = await streamListenerFeature.AcceptAsync(); - if (connectionContext == null) + if (connectionContext == null || _haveSentGoAway) { break; } - //if (_haveSentGoAway) - //{ - // // error here. - //} - var httpConnectionContext = new HttpConnectionContext { ConnectionId = connectionContext.ConnectionId, @@ -90,16 +87,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 }; var streamFeature = httpConnectionContext.ConnectionFeatures.Get(); - var streamId = streamFeature.StreamId; - HighestStreamId = streamId; if (!streamFeature.CanWrite) { + // Unidirectional stream var stream = new Http3ControlStream(application, this, httpConnectionContext); ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false); } else { + // Keep track of highest stream id seen for GOAWAY + var streamId = streamFeature.StreamId; + + HighestStreamId = streamId; + var http3Stream = new Http3Stream(application, this, httpConnectionContext); var stream = http3Stream; _streams[streamId] = http3Stream; @@ -109,20 +110,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 } finally { - await settingsStream; - await encoderStream; - await decoderStream; + // Abort all streams as connection has shutdown. foreach (var stream in _streams.Values) { - stream.Abort(new ConnectionAbortedException("")); + stream.Abort(new ConnectionAbortedException("Connection is shutting down.")); } + + ControlStream.Abort(new ConnectionAbortedException("Connection is shutting down.")); + EncoderStream.Abort(new ConnectionAbortedException("Connection is shutting down.")); + DecoderStream.Abort(new ConnectionAbortedException("Connection is shutting down.")); + + await controlTask; + await encoderTask; + await decoderTask; } } - private async ValueTask CreateSettingsStream(IHttpApplication application) + private async ValueTask CreateControlStream(IHttpApplication application) { var stream = await CreateNewUnidirectionalStreamAsync(application); - SettingsStream = stream; + ControlStream = stream; await stream.SendStreamIdAsync(id: 0); await stream.SendSettingsFrameAsync(); } @@ -146,7 +153,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 var connectionContext = await Context.ConnectionFeatures.Get().StartUnidirectionalStreamAsync(); var httpConnectionContext = new HttpConnectionContext { - ConnectionId = connectionContext.ConnectionId, + //ConnectionId = "", TODO getting stream ID from stream that isn't started throws an exception. ConnectionContext = connectionContext, Protocols = Context.Protocols, ServiceContext = Context.ServiceContext, @@ -183,8 +190,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 public void Abort(ConnectionAbortedException ex) { - // Send goaway + lock (_sync) + { + if (ControlStream != null) + { + // TODO need to await this somewhere or allow this to be called elsewhere? + ControlStream.SendGoAway(_highestOpenedStreamId).GetAwaiter().GetResult(); + } + } + + _haveSentGoAway = true; + // Abort currently active streams + foreach (var stream in _streams.Values) + { + stream.Abort(new ConnectionAbortedException("The Http3Connection has been aborted"), Http3ErrorCode.UnexpectedFrame); + } // TODO need to figure out if there is server initiated connection close rather than stream close? } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs index cca8f59837..a75d8bc69a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs @@ -3,7 +3,9 @@ using System; using System.Buffers; +using System.Collections.Generic; using System.IO.Pipelines; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -17,10 +19,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 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 readonly Http3RawFrame _incomingFrame = new Http3RawFrame(); private volatile int _isClosed; private int _gracefulCloseInitiator; @@ -53,6 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 public void Abort(ConnectionAbortedException ex) { + } public void HandleReadDataRateTimeout() @@ -117,11 +121,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 } } - internal async ValueTask SendStreamIdAsync(int id) + internal async ValueTask SendStreamIdAsync(long id) { await _frameWriter.WriteStreamIdAsync(id); } + internal async ValueTask SendGoAway(long id) + { + await _frameWriter.WriteGoAway(id); + } + internal async ValueTask SendSettingsFrameAsync() { await _frameWriter.WriteSettingsAsync(null); @@ -172,7 +181,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 if (streamType == ControlStream) { - if (_http3Connection.SettingsStream != null) + if (_http3Connection.ControlStream != null) { throw new Http3ConnectionException("HTTP_STREAM_CREATION_ERROR"); } @@ -255,7 +264,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 case Http3FrameType.Settings: return ProcessSettingsFrameAsync(payload); case Http3FrameType.GoAway: - return ProcessGoAwayFrameAsync(); + return ProcessGoAwayFrameAsync(payload); case Http3FrameType.CancelPush: return ProcessCancelPushFrameAsync(); case Http3FrameType.MaxPushId: @@ -318,14 +327,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 } } - private ValueTask ProcessGoAwayFrameAsync() + private ValueTask ProcessGoAwayFrameAsync(ReadOnlySequence payload) { - if (!_haveReceivedSettingsFrame) - { - throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); - } - // Get highest stream id and write that to the response. - return default; + throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); } private ValueTask ProcessCancelPushFrameAsync() @@ -334,7 +338,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); } - // This should just noop. + return default; } @@ -344,6 +348,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); } + return default; } @@ -353,6 +358,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { throw new Http3ConnectionException("HTTP_FRAME_UNEXPECTED"); } + return default; } @@ -369,10 +375,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 } } - public void Tick(DateTimeOffset now) - { - } - /// /// Used to kick off the request processing loop by derived classes. /// diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs index 020006ae27..98ce27e871 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameReader.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.Buffers; +using System.Net.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { @@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 | Frame Payload (*) ... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ - internal static bool TryReadFrame(ref ReadOnlySequence readableBuffer, Http3Frame frame, uint maxFrameSize, out ReadOnlySequence framePayload) + internal static bool TryReadFrame(ref ReadOnlySequence readableBuffer, Http3RawFrame frame, uint maxFrameSize, out ReadOnlySequence framePayload) { framePayload = ReadOnlySequence.Empty; var consumed = readableBuffer.Start; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs index f7905fdd76..bb2e33a4cb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs @@ -6,6 +6,8 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.IO.Pipelines; +using System.Net.Http; +using System.Net.Http.QPack; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -28,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 private readonly MinDataRate _minResponseDataRate; private readonly MemoryPool _memoryPool; private readonly IKestrelTrace _log; - private readonly Http3Frame _outgoingFrame; + private readonly Http3RawFrame _outgoingFrame; private readonly TimingPipeFlusher _flusher; // TODO update max frame size @@ -49,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 _minResponseDataRate = minResponseDataRate; _memoryPool = memoryPool; _log = log; - _outgoingFrame = new Http3Frame(); + _outgoingFrame = new Http3RawFrame(); _flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log); _headerEncodingBuffer = new byte[_maxFrameSize]; } @@ -164,6 +166,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 } } + internal Task WriteGoAway(long id) + { + _outgoingFrame.PrepareGoAway(); + var buffer = _outputWriter.GetSpan(9); + buffer[0] = (byte)_outgoingFrame.Type; + + var length = VariableLengthIntegerHelper.WriteInteger(buffer.Slice(1), id); + + _outgoingFrame.Length = length; + + WriteHeaderUnsynchronized(); + + return _outputWriter.FlushAsync().AsTask(); + } + private void WriteHeaderUnsynchronized() { var headerLength = WriteHeader(_outgoingFrame, _outputWriter); @@ -172,7 +189,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 _unflushedBytes += headerLength + _outgoingFrame.Length; } - internal static int WriteHeader(Http3Frame frame, PipeWriter output) + internal static int WriteHeader(Http3RawFrame frame, PipeWriter output) { // max size of the header is 16, most likely it will be smaller. var buffer = output.GetSpan(16); @@ -189,7 +206,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 return totalLength; } - public ValueTask WriteResponseTrailers(int streamId, HttpResponseTrailers headers) + public ValueTask WriteResponseTrailers(HttpResponseTrailers headers) { lock (_writeLock) { @@ -203,7 +220,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 _outgoingFrame.PrepareHeaders(); var buffer = _headerEncodingBuffer.AsSpan(); var done = _qpackEncoder.BeginEncode(EnumerateHeaders(headers), buffer, out var payloadLength); - FinishWritingHeaders(streamId, payloadLength, done); + FinishWritingHeaders(payloadLength, done); } catch (QPackEncodingException) { @@ -239,7 +256,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 } } - internal void WriteResponseHeaders(int streamId, int statusCode, IHeaderDictionary headers) + internal void WriteResponseHeaders(int statusCode, IHeaderDictionary headers) { lock (_writeLock) { @@ -253,7 +270,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 _outgoingFrame.PrepareHeaders(); var buffer = _headerEncodingBuffer.AsSpan(); var done = _qpackEncoder.BeginEncode(statusCode, EnumerateHeaders(headers), buffer, out var payloadLength); - FinishWritingHeaders(streamId, payloadLength, done); + FinishWritingHeaders(payloadLength, done); } catch (QPackEncodingException hex) { @@ -264,7 +281,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 } } - private void FinishWritingHeaders(int streamId, int payloadLength, bool done) + private void FinishWritingHeaders(int payloadLength, bool done) { var buffer = _headerEncodingBuffer.AsSpan(); _outgoingFrame.Length = payloadLength; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs index bb94b99de1..6c2f142591 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs @@ -13,12 +13,12 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWrite using Microsoft.Extensions.Logging; using System.Diagnostics; using Microsoft.AspNetCore.Internal; +using System.Net.Http; 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; @@ -35,13 +35,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 private IMemoryOwner _fakeMemoryOwner; public Http3OutputProducer( - int streamId, Http3FrameWriter frameWriter, MemoryPool pool, Http3Stream stream, IKestrelTrace log) { - _streamId = streamId; _frameWriter = frameWriter; _memoryPool = pool; _stream = stream; @@ -308,7 +306,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 // TODO figure out something to do here. } - _frameWriter.WriteResponseHeaders(_streamId, statusCode, responseHeaders); + _frameWriter.WriteResponseHeaders(statusCode, responseHeaders); } } @@ -350,7 +348,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 } _stream.ResponseTrailers.SetReadOnly(); - flushResult = await _frameWriter.WriteResponseTrailers(_streamId, _stream.ResponseTrailers); + flushResult = await _frameWriter.WriteResponseTrailers(_stream.ResponseTrailers); } else if (readResult.IsCompleted) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index fb7679fa7f..087013a6cc 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -6,13 +6,14 @@ using System.Buffers; using System.Diagnostics; using System.IO.Pipelines; using System.Net.Http; +using System.Net.Http.QPack; 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.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -25,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 private int _isClosed; private int _gracefulCloseInitiator; private readonly HttpConnectionContext _context; - private readonly Http3Frame _incomingFrame = new Http3Frame(); + private readonly Http3RawFrame _incomingFrame = new Http3RawFrame(); private readonly Http3Connection _http3Connection; private bool _receivedHeaders; @@ -53,16 +54,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 Reset(); _http3Output = new Http3OutputProducer( - 0, // TODO streamid _frameWriter, context.MemoryPool, this, context.ServiceContext.Log); RequestBodyPipe = CreateRequestBodyPipe(64 * 1024); // windowSize? Output = _http3Output; + QPackDecoder = new QPackDecoder(_context.ServiceContext.ServerOptions.Limits.Http3.MaxRequestHeaderFieldSize); } - public QPackDecoder QPackDecoder { get; set; } = new QPackDecoder(10000, 10000); + public QPackDecoder QPackDecoder { get; } public PipeReader Input => _context.Transport.Input; @@ -76,6 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode) { + // TODO something with request aborted? } public void OnHeadersComplete(bool endStream) @@ -83,6 +85,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 OnHeadersComplete(); } + public void OnStaticIndexedHeader(int index) + { + var knownHeader = H3StaticTable.Instance[index]; + OnHeader(knownHeader.Name, knownHeader.Value); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + var knownHeader = H3StaticTable.Instance[index]; + OnHeader(knownHeader.Name, value); + } + public void HandleReadDataRateTimeout() { Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond); @@ -114,6 +128,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 public async Task ProcessRequestAsync(IHttpApplication application) { + Exception error = null; + try { while (_isClosed == 0) @@ -139,23 +155,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 return; } } - catch (Http3StreamErrorException) - { - // TODO - } + finally { Input.AdvanceTo(consumed, examined); } } } - catch (Exception) + catch (Exception ex) { - // TODO + error = ex; + Log.LogWarning(0, ex, "Stream threw an exception."); } finally { - await RequestBodyPipe.Writer.CompleteAsync(); + var streamError = error as ConnectionAbortedException + ?? new ConnectionAbortedException("The stream has completed.", error); + try + { + _frameWriter.Complete(); + } + catch + { + _frameWriter.Abort(streamError); + throw; + } + finally + { + Input.Complete(); + _context.Transport.Input.CancelPendingRead(); + await RequestBodyPipe.Writer.CompleteAsync(); + } } } @@ -277,7 +307,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { if (!string.IsNullOrEmpty(RequestHeaders[HeaderNames.Scheme]) || !string.IsNullOrEmpty(RequestHeaders[HeaderNames.Path])) { - //ResetAndAbort(new ConnectionAbortedException(CoreStrings.Http2ErrorConnectMustNotSendSchemeOrPath), Http2ErrorCode.PROTOCOL_ERROR); + Abort(new ConnectionAbortedException(CoreStrings.Http2ErrorConnectMustNotSendSchemeOrPath), Http3ErrorCode.ProtocolError); return false; } @@ -296,8 +326,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 // - 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); + Abort(new ConnectionAbortedException( + CoreStrings.FormatHttp2StreamErrorSchemeMismatch(RequestHeaders[HeaderNames.Scheme], Scheme)), Http3ErrorCode.ProtocolError); return false; } @@ -325,7 +355,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 var requestLineLength = _methodText.Length + Scheme.Length + hostText.Length + path.Length; if (requestLineLength > ServerOptions.Limits.MaxRequestLineSize) { - //ResetAndAbort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestLineTooLong), Http2ErrorCode.PROTOCOL_ERROR); + Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestLineTooLong), Http3ErrorCode.ProtocolError); return false; } @@ -346,8 +376,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 if (Method == Http.HttpMethod.None) { - // TODO - //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR); + Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http3ErrorCode.ProtocolError); return false; } @@ -355,7 +384,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { if (HttpCharacters.IndexOfInvalidTokenChar(_methodText) >= 0) { - //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http2ErrorCode.PROTOCOL_ERROR); + Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2ErrorMethodInvalid(_methodText)), Http3ErrorCode.ProtocolError); return false; } } @@ -395,7 +424,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 if (host.Count > 1 || !HttpUtilities.IsHostHeaderValid(hostText)) { // RST replaces 400 - //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(hostText)), Http2ErrorCode.PROTOCOL_ERROR); + Abort(new ConnectionAbortedException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(hostText)), Http3ErrorCode.ProtocolError); return false; } @@ -407,7 +436,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 // Must start with a leading slash if (pathSegment.Length == 0 || pathSegment[0] != '/') { - //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR); + Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http3ErrorCode.ProtocolError); return false; } @@ -437,7 +466,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 } catch (InvalidOperationException) { - //ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR); + // TODO change HTTP/2 specific messages to include HTTP/3 + Abort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http3ErrorCode.ProtocolError); return false; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs index 86952e6a3d..8edccac290 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamErrorException.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.Net.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DynamicTable.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DynamicTable.cs index bd48915bea..63388ffdfb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DynamicTable.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/DynamicTable.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.Net.Http.QPack; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/EncoderStreamReader.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/EncoderStreamReader.cs index e2d769507f..54ebad55db 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/EncoderStreamReader.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/EncoderStreamReader.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Net.Http.HPack; +using System.Net.Http.QPack; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack { @@ -317,11 +318,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack _state = State.Ready; } - private HeaderField GetHeader(int index) + private System.Net.Http.QPack.HeaderField GetHeader(int index) { try { - return _s ? StaticTable.Instance[index] : _dynamicTable[index]; + return _s ? H3StaticTable.Instance[index] : _dynamicTable[index]; } catch (IndexOutOfRangeException 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 deleted file mode 100644 index d929187de5..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/HeaderField.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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/QPackDecodingException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecodingException.cs deleted file mode 100644 index fb648d3045..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecodingException.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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 deleted file mode 100644 index 44a5e20ea5..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncoder.cs +++ /dev/null @@ -1,504 +0,0 @@ -// 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 deleted file mode 100644 index 306850ab4b..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackEncodingException.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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/KestrelServer.cs b/src/Servers/Kestrel/Core/src/KestrelServer.cs index 5d1c1b4cee..ca1af168f0 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServer.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServer.cs @@ -161,7 +161,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } else { - factory = _transportFactories.Last(); + foreach (var transportFactory in _transportFactories) + { + if (!(transportFactory is IMultiplexedConnectionListenerFactory)) + { + factory = transportFactory; + } + } } var transport = await factory.BindAsync(options.EndPoint).ConfigureAwait(false); diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index 37d8684e99..fc8c2f69d9 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Servers/Kestrel/Core/test/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs index ebd7bfc316..1b2646d16b 100644 --- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs @@ -523,6 +523,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Query = query.GetAsciiStringNonNullCharacters(); PathEncoded = pathEncoded; } + + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } } // Doesn't put empty blocks in between every byte diff --git a/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs b/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs index e35ad26b98..579d85a687 100644 --- a/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs +++ b/src/Servers/Kestrel/Core/test/VariableIntHelperTests.cs @@ -1,6 +1,6 @@ using System; using System.Buffers; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; +using System.Net.Http; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests diff --git a/src/Servers/Kestrel/Kestrel.sln b/src/Servers/Kestrel/Kestrel.sln index 37e7dc8ff8..e6a1ee0477 100644 --- a/src/Servers/Kestrel/Kestrel.sln +++ b/src/Servers/Kestrel/Kestrel.sln @@ -90,7 +90,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server EndProject 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Http3SampleApp", "samples\Http3SampleApp\Http3SampleApp.csproj", "{B3CDC83A-A9C5-45DF-9828-6BC419C24308}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs index 3eb982991f..34a7e299bc 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs @@ -74,6 +74,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal public async ValueTask AcceptAsync() { var stream = await _connection.AcceptStreamAsync(); + try + { + // Because the stream is wrapped with a quic connection provider, + // we need to check a property to check if this is null + // Will be removed once the provider abstraction is removed. + _ = stream.CanRead; + } + catch (Exception) + { + return null; + } + return new QuicStreamContext(stream, this, _context); } } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs index 89d8d2e576..efbca9b1cb 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionListener.cs @@ -39,6 +39,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) { var quicConnection = await _listener.AcceptConnectionAsync(cancellationToken); + try + { + // Because the stream is wrapped with a quic connection provider, + // we need to check a property to check if this is null + // Will be removed once the provider abstraction is removed. + _ = quicConnection.LocalEndPoint; + } + catch (Exception) + { + return null; + } + return new QuicConnectionContext(quicConnection, _context); } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs index 625259c473..6097e78e83 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs @@ -95,8 +95,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal try { // Spawn send and receive logic - var receiveTask = DoReceive(); - var sendTask = DoSend(); + // Streams may or may not have reading/writing, so only start tasks accordingly + var receiveTask = Task.CompletedTask; + var sendTask = Task.CompletedTask; + + if (_stream.CanRead) + { + receiveTask = DoReceive(); + } + + if (_stream.CanWrite) + { + sendTask = DoSend(); + } // Now wait for both to complete await receiveTask; diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs index c165c141ce..d54f4a879f 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs @@ -114,6 +114,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) => RequestHandler.Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded); + + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } } } } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs index 2ed922386f..3960ebe388 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs @@ -79,6 +79,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { } + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } + private struct Adapter : IHttpRequestLineHandler, IHttpHeadersHandler { public HttpParserBenchmark RequestHandler; @@ -96,6 +106,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) => RequestHandler.OnStartLine(method, version, target, path, query, customMethod, pathEncoded); + + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } } } } diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Http3SampleApp.csproj b/src/Servers/Kestrel/samples/Http3SampleApp/Http3SampleApp.csproj index eb811e1dcb..07adeb2684 100644 --- a/src/Servers/Kestrel/samples/Http3SampleApp/Http3SampleApp.csproj +++ b/src/Servers/Kestrel/samples/Http3SampleApp/Http3SampleApp.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -11,4 +11,10 @@ + + + PreserveNewest + PreserveNewest + + diff --git a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs index db60249ab3..f989d62607 100644 --- a/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/Http3SampleApp/Program.cs @@ -16,6 +16,7 @@ namespace Http3SampleApp public static void Main(string[] args) { var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, true); + var hostBuilder = new HostBuilder() .ConfigureLogging((_, factory) => { @@ -30,17 +31,27 @@ namespace Http3SampleApp { options.Certificate = cert; options.RegistrationName = "Quic"; - options.Alpn = "h3-24"; + options.Alpn = "h3-25"; options.IdleTimeout = TimeSpan.FromHours(1); }) .ConfigureKestrel((context, options) => { - var basePort = 5555; - + var basePort = 443; + options.EnableAltSvc = true; options.Listen(IPAddress.Any, basePort, listenOptions => { - listenOptions.UseHttps(); - listenOptions.Protocols = HttpProtocols.Http3; + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificate = cert; + }); + }); + options.Listen(IPAddress.Any, basePort, listenOptions => + { + listenOptions.UseHttps(httpsOptions => + { + httpsOptions.ServerCertificate = cert; + }); + listenOptions.Protocols = HttpProtocols.Http3; }); }) .UseStartup(); diff --git a/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs b/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs index 4766bc8bfe..e2b6b7b5df 100644 --- a/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/QuicSampleApp/Program.cs @@ -25,7 +25,6 @@ namespace QuicSampleApp public static void Main(string[] args) { - //var cert = CertificateLoader.LoadFromStoreCert("localhost", StoreName.My.ToString(), StoreLocation.CurrentUser, true); var hostBuilder = new WebHostBuilder() .ConfigureLogging((_, factory) => { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 556e62a07f..d43657f545 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -1285,6 +1285,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests return bufferSize ?? 0; } + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } + internal class Http2FrameWithPayload : Http2Frame { public Http2FrameWithPayload() : base() diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index d56624e4f9..330f9a08b7 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -30,5 +30,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests var responseData = await requestStream.ExpectDataAsync(); Assert.Equal("Hello world", Encoding.ASCII.GetString(responseData.ToArray())); } + + [Fact] + public async Task RequestHeadersMaxRequestHeaderFieldSize_EndsStream() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + new KeyValuePair("test", new string('a', 10000)) + }; + + var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + + var doneWithHeaders = await requestStream.SendHeadersAsync(headers); + await requestStream.SendDataAsync(Encoding.ASCII.GetBytes("Hello world")); + + // TODO figure out how to test errors for request streams that would be set on the Quic Stream. + await requestStream.ExpectReceiveEndOfStream(); + } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs index aa0e7fc9ea..121c43881d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Pipelines; using System.Net.Http; +using System.Net.Http.QPack; using System.Reflection; using System.Threading.Channels; using System.Threading.Tasks; @@ -86,7 +87,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests internal async ValueTask InitializeConnectionAndStreamsAsync(RequestDelegate application) { await InitializeConnectionAsync(application); - + var controlStream1 = await CreateControlStream(0); var controlStream2 = await CreateControlStream(2); var controlStream3 = await CreateControlStream(3); @@ -177,7 +178,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests internal async ValueTask CreateControlStream(int id) { var stream = new Http3ControlStream(this, _connection); - _acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext); + _acceptConnectionQueue.Writer.TryWrite(stream.ConnectionContext); await stream.WriteStreamIdAsync(id); return stream; } @@ -226,7 +227,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests private readonly byte[] _headerEncodingBuffer = new byte[Http3PeerSettings.MinAllowedMaxFrameSize]; private QPackEncoder _qpackEncoder = new QPackEncoder(); - private QPackDecoder _qpackDecoder = new QPackDecoder(10000, 10000); + private QPackDecoder _qpackDecoder = new QPackDecoder(8192); private long _bytesReceived; protected readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -238,7 +239,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests 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); @@ -247,7 +248,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public async Task SendHeadersAsync(IEnumerable> headers) { var outputWriter = _pair.Application.Output; - var frame = new Http3Frame(); + var frame = new Http3RawFrame(); frame.PrepareHeaders(); var buffer = _headerEncodingBuffer.AsMemory(); var done = _qpackEncoder.BeginEncode(headers, buffer.Span, out var length); @@ -261,7 +262,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests internal async Task SendDataAsync(Memory data) { var outputWriter = _pair.Application.Output; - var frame = new Http3Frame(); + var frame = new Http3RawFrame(); frame.PrepareData(); frame.Length = data.Length; Http3FrameWriter.WriteHeader(frame, outputWriter); @@ -321,6 +322,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests } } + internal async Task ExpectReceiveEndOfStream() + { + var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); + Assert.True(result.IsCompleted); + } + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); @@ -329,9 +336,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public void OnHeadersComplete(bool endHeaders) { } + + public void OnStaticIndexedHeader(int index) + { + var knownHeader = H3StaticTable.Instance[index]; + _decodedHeaders[((Span)knownHeader.Name).GetAsciiStringNonNullCharacters()] = ((Span)knownHeader.Value).GetAsciiOrUTF8StringNonNullCharacters(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + _decodedHeaders[((Span)H3StaticTable.Instance[index].Name).GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); + } } - internal class Http3FrameWithPayload : Http3Frame + internal class Http3FrameWithPayload : Http3RawFrame { public Http3FrameWithPayload() : base() { diff --git a/src/Shared/Http2/Hpack/StaticTable.cs b/src/Shared/Http2/Hpack/H2StaticTable.cs similarity index 99% rename from src/Shared/Http2/Hpack/StaticTable.cs rename to src/Shared/Http2/Hpack/H2StaticTable.cs index 79090d8495..eeb1029119 100644 --- a/src/Shared/Http2/Hpack/StaticTable.cs +++ b/src/Shared/Http2/Hpack/H2StaticTable.cs @@ -7,7 +7,7 @@ using System.Text; namespace System.Net.Http.HPack { - internal static class StaticTable + internal static class H2StaticTable { // Index of status code into s_staticDecoderTable private static readonly Dictionary s_statusIndex = new Dictionary diff --git a/src/Shared/Http2/Hpack/HPackDecoder.cs b/src/Shared/Http2/Hpack/HPackDecoder.cs index 93c4bfeb9e..98fb416523 100644 --- a/src/Shared/Http2/Hpack/HPackDecoder.cs +++ b/src/Shared/Http2/Hpack/HPackDecoder.cs @@ -470,9 +470,9 @@ namespace System.Net.Http.HPack { try { - return index <= StaticTable.Count - ? StaticTable.Get(index - 1) - : _dynamicTable[index - StaticTable.Count - 1]; + return index <= H2StaticTable.Count + ? H2StaticTable.Get(index - 1) + : _dynamicTable[index - H2StaticTable.Count - 1]; } catch (IndexOutOfRangeException) { diff --git a/src/Shared/Http2/Hpack/HPackEncoder.cs b/src/Shared/Http2/Hpack/HPackEncoder.cs index 3aa94376be..52b21dcce3 100644 --- a/src/Shared/Http2/Hpack/HPackEncoder.cs +++ b/src/Shared/Http2/Hpack/HPackEncoder.cs @@ -73,7 +73,7 @@ namespace System.Net.Http.HPack case 400: case 404: case 500: - buffer[0] = (byte)(0x80 | StaticTable.StatusIndex[statusCode]); + buffer[0] = (byte)(0x80 | H2StaticTable.StatusIndex[statusCode]); return 1; default: // Send as Literal Header Field Without Indexing - Indexed Name diff --git a/src/Shared/Http2/Hpack/Huffman.cs b/src/Shared/Http2/Hpack/Huffman.cs index 1cfc8e7d5d..3cba01b1d2 100644 --- a/src/Shared/Http2/Hpack/Huffman.cs +++ b/src/Shared/Http2/Hpack/Huffman.cs @@ -319,7 +319,8 @@ namespace System.Net.Http.HPack // Note that if lastDecodeBits is 3 or more, then we will only get 5 bits (or less) // from src[i]. Thus we need to read 5 bytes here to ensure that we always have // at least 30 bits available for decoding. - // TODO ISSUE 31751: Rework this as part of Huffman perf improvements + // TODO https://github.com/dotnet/runtime/issues/1506: + // Rework this as part of Huffman perf improvements uint next = (uint)(src[i] << 24 + lastDecodedBits); next |= (i + 1 < src.Length ? (uint)(src[i + 1] << 16 + lastDecodedBits) : 0); next |= (i + 2 < src.Length ? (uint)(src[i + 2] << 8 + lastDecodedBits) : 0); diff --git a/src/Shared/Http2/Hpack/IntegerEncoder.cs b/src/Shared/Http2/Hpack/IntegerEncoder.cs index b98bfd3c75..f56ad0a85e 100644 --- a/src/Shared/Http2/Hpack/IntegerEncoder.cs +++ b/src/Shared/Http2/Hpack/IntegerEncoder.cs @@ -8,6 +8,11 @@ namespace System.Net.Http.HPack { internal static class IntegerEncoder { + /// + /// The maximum bytes required to encode a 32-bit int, regardless of prefix length. + /// + public const int MaxInt32EncodedLength = 6; + /// /// Encodes an integer into one or more bytes. /// diff --git a/src/Shared/Http2/IHttpHeadersHandler.cs b/src/Shared/Http2/IHttpHeadersHandler.cs index 256dde1797..311afa8c39 100644 --- a/src/Shared/Http2/IHttpHeadersHandler.cs +++ b/src/Shared/Http2/IHttpHeadersHandler.cs @@ -17,6 +17,8 @@ namespace System.Net.Http #endif interface IHttpHeadersHandler { + void OnStaticIndexedHeader(int index); + void OnStaticIndexedHeader(int index, ReadOnlySpan value); void OnHeader(ReadOnlySpan name, ReadOnlySpan value); void OnHeadersComplete(bool endStream); } diff --git a/src/Shared/Http2/ReadMe.SharedCode.md b/src/Shared/Http2/ReadMe.SharedCode.md index f5f420322d..20e88283f4 100644 --- a/src/Shared/Http2/ReadMe.SharedCode.md +++ b/src/Shared/Http2/ReadMe.SharedCode.md @@ -1,19 +1,19 @@ -The code in this directory is shared between dotnet/runtime and dotnet/aspnetcore. This contains HTTP/2 protocol infrastructure such as an HPACK implementation. Any changes to this dir need to be checked into both repositories. +The code in this directory is shared between dotnet/runtime and aspnet/AspNetCore. This contains HTTP/2 protocol infrastructure such as an HPACK implementation. Any changes to this dir need to be checked into both repositories. dotnet/runtime code paths: - runtime\src\libraries\Common\src\System\Net\Http\Http2 - runtime\src\libraries\Common\tests\Tests\System\Net\Http2 -dotnet/aspnetcore code paths: + +aspnet/AspNetCore code paths: - AspNetCore\src\Shared\Http2 - AspNetCore\src\Shared\test\Shared.Tests\Http2 ## Copying code -To copy code from dotnet/runtime to dotnet/aspnetcore, set ASPNETCORE_REPO to the AspNetCore repo root and then run CopyToAspNetCore.cmd. -To copy code from dotnet/aspnetcore to dotnet/runtime, set RUNTIME_REPO to the runtime repo root and then run CopyToRuntime.cmd. +- To copy code from dotnet/runtime to aspnet/AspNetCore, set ASPNETCORE_REPO to the AspNetCore repo root and then run CopyToAspNetCore.cmd. +- To copy code from aspnet/AspNetCore to dotnet/runtime, set RUNTIME_REPO to the runtime repo root and then run CopyToRuntime.cmd. ## Building dotnet/runtime code: -- https://github.com/dotnet/runtime/blob/master/docs/libraries/building/windows-instructions.md -- https://github.com/dotnet/runtime/blob/master/docs/libraries/project-docs/developer-guide.md +- https://github.com/dotnet/runtime/tree/master/docs/workflow - Run libraries.cmd from the root once: `PS D:\github\runtime> .\libraries.cmd` - Build the individual projects: - `PS D:\github\dotnet\src\libraries\Common\tests> dotnet msbuild /t:rebuild` @@ -23,14 +23,14 @@ To copy code from dotnet/aspnetcore to dotnet/runtime, set RUNTIME_REPO to the r - `PS D:\github\runtime\src\libraries\Common\tests> dotnet msbuild /t:rebuildandtest` - `PS D:\github\runtime\src\libraries\System.Net.Http\tests\UnitTests> dotnet msbuild /t:rebuildandtest` -## Building dotnet/aspnetcore code: -- https://github.com/dotnet/aspnetcore/blob/master/docs/BuildFromSource.md +## Building aspnet/AspNetCore code: +- https://github.com/aspnet/AspNetCore/blob/master/docs/BuildFromSource.md - Run restore in the root once: `PS D:\github\AspNetCore> .\restore.cmd` - Activate to use the repo local runtime: `PS D:\github\AspNetCore> . .\activate.ps1` - Build the individual projects: - `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet msbuild` - `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\src> dotnet msbuild` -### Running dotnet/aspnetcore tests: +### Running aspnet/AspNetCore tests: - `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet test` - `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\test> dotnet test` diff --git a/src/Shared/Http2cat/Http2Utilities.cs b/src/Shared/Http2cat/Http2Utilities.cs index f28933c9c0..fecc6ed3bb 100644 --- a/src/Shared/Http2cat/Http2Utilities.cs +++ b/src/Shared/Http2cat/Http2Utilities.cs @@ -1017,6 +1017,16 @@ namespace Microsoft.AspNetCore.Http2Cat Assert.Equal(expectedErrorCode, frame.RstStreamErrorCode); } + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } + internal class Http2FrameWithPayload : Http2Frame { public Http2FrameWithPayload() : base() diff --git a/src/Shared/Http3/CopyToAspNetCore.cmd b/src/Shared/Http3/CopyToAspNetCore.cmd new file mode 100644 index 0000000000..f94a7609fe --- /dev/null +++ b/src/Shared/Http3/CopyToAspNetCore.cmd @@ -0,0 +1,14 @@ +@ECHO OFF +SETLOCAL + +if not [%1] == [] (set remote_repo=%1) else (set remote_repo=%ASPNETCORE_REPO%) + +IF [%remote_repo%] == [] ( + echo The 'ASPNETCORE_REPO' environment variable or command line parameter is not set, aborting. + exit /b 1 +) + +echo ASPNETCORE_REPO: %remote_repo% + +robocopy . %remote_repo%\src\Shared\Http3 /MIR +robocopy .\..\..\..\..\..\tests\Tests\System\Net\Http3\ %remote_repo%\src\Shared\test\Shared.Tests\Http3 /MIR diff --git a/src/Shared/Http3/CopyToAspNetCore.sh b/src/Shared/Http3/CopyToAspNetCore.sh new file mode 100644 index 0000000000..053a620a5e --- /dev/null +++ b/src/Shared/Http3/CopyToAspNetCore.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +if [[ -n "$1" ]]; then + remote_repo="$1" +else + remote_repo="$ASPNETCORE_REPO" +fi + +if [[ -z "$remote_repo" ]]; then + echo The 'ASPNETCORE_REPO' environment variable or command line parameter is not set, aborting. + exit 1 +fi + +cd "$(dirname "$0")" || exit 1 + +echo "ASPNETCORE_REPO: $remote_repo" + +rsync -av --delete ./ "$remote_repo"/src/Shared/Http3 +rsync -av --delete ./../../../../../tests/Tests/System/Net/Http3/ "$remote_repo"/src/Shared/test/Shared.Tests/Http3 diff --git a/src/Shared/Http3/CopyToRuntime.cmd b/src/Shared/Http3/CopyToRuntime.cmd new file mode 100644 index 0000000000..b6edcf89e4 --- /dev/null +++ b/src/Shared/Http3/CopyToRuntime.cmd @@ -0,0 +1,14 @@ +@ECHO OFF +SETLOCAL + +if not [%1] == [] (set remote_repo=%1) else (set remote_repo=%RUNTIME_REPO%) + +IF [%remote_repo%] == [] ( + echo The 'RUNTIME_REPO' environment variable or command line parameter is not set, aborting. + exit /b 1 +) + +echo RUNTIME_REPO: %remote_repo% + +robocopy . %remote_repo%\src\libraries\Common\src\System\Net\Http\Http3 /MIR +robocopy .\..\test\Shared.Tests\Http3 %remote_repo%\src\libraries\Common\tests\Tests\System\Net\Http3 /MIR diff --git a/src/Shared/Http3/CopyToRuntime.sh b/src/Shared/Http3/CopyToRuntime.sh new file mode 100644 index 0000000000..b562659da6 --- /dev/null +++ b/src/Shared/Http3/CopyToRuntime.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +if [[ -n "$1" ]]; then + remote_repo="$1" +else + remote_repo="$RUNTIME_REPO" +fi + +if [[ -z "$remote_repo" ]]; then + echo The 'RUNTIME_REPO' environment variable or command line parameter is not set, aborting. + exit 1 +fi + +cd "$(dirname "$0")" || exit 1 + +echo "RUNTIME_REPO: $remote_repo" + +rsync -av --delete ./ "$remote_repo"/src/libraries/Common/src/System/Net/Http/Http3 +rsync -av --delete ./../test/Shared.Tests/Http3/ "$remote_repo"/src/libraries/Common/tests/Tests/System/Net/Http3 diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3ErrorCode.cs b/src/Shared/Http3/Frames/Http3ErrorCode.cs similarity index 62% rename from src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3ErrorCode.cs rename to src/Shared/Http3/Frames/Http3ErrorCode.cs index ac01a35477..785eab75bd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Frames/Http3ErrorCode.cs +++ b/src/Shared/Http3/Frames/Http3ErrorCode.cs @@ -1,101 +1,92 @@ -// 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. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 +namespace System.Net.Http { - internal enum Http3ErrorCode : uint + internal enum Http3ErrorCode : long { /// - /// HTTP_NO_ERROR (0x100): + /// H3_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): + /// H3_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): + /// H3_INTERNAL_ERROR (0x102): /// An internal error has occurred in the HTTP stack. /// InternalError = 0x102, /// - /// HTTP_STREAM_CREATION_ERROR (0x103): + /// H3_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): + /// H3_CLOSED_CRITICAL_STREAM (0x104): /// A stream required by the connection was closed or reset. /// ClosedCriticalStream = 0x104, /// - /// HTTP_UNEXPECTED_FRAME (0x105): + /// H3_FRAME_UNEXPECTED (0x105): /// A frame was received which was not permitted in the current state. /// UnexpectedFrame = 0x105, /// - /// HTTP_FRAME_ERROR (0x106): + /// H3_FRAME_ERROR (0x106): /// A frame that fails to satisfy layout requirements or with an invalid size was received. /// FrameError = 0x106, /// - /// HTTP_EXCESSIVE_LOAD (0x107): + /// H3_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): + /// H3_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, + IdError = 0x108, /// - /// HTTP_SETTINGS_ERROR (0x10A): + /// H3_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, + SettingsError = 0x109, /// - /// HTTP_MISSING_SETTINGS (0x10B): + /// H3_MISSING_SETTINGS (0x10B): /// No SETTINGS frame was received at the beginning of the control stream. /// - MissingSettings = 0x10b, + MissingSettings = 0x10a, /// - /// HTTP_REQUEST_REJECTED (0x10C): + /// H3_REQUEST_REJECTED (0x10C): /// A server rejected a request without performing any application processing. /// - RequestRejected = 0x10c, + RequestRejected = 0x10b, /// - /// HTTP_REQUEST_CANCELLED (0x10D): + /// H3_REQUEST_CANCELLED (0x10D): /// The request or its response (including pushed response) is cancelled. /// - RequestCancelled = 0x10d, + RequestCancelled = 0x10c, /// - /// HTTP_REQUEST_INCOMPLETE (0x10E): + /// H3_REQUEST_INCOMPLETE (0x10E): /// The client’s stream terminated without containing a fully-formed request. /// - RequestIncomplete = 0x10e, + RequestIncomplete = 0x10d, /// - /// 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): + /// H3_CONNECT_ERROR (0x110): /// The connection established in response to a CONNECT request was reset or abnormally closed. /// - ConnectError = 0x110, + ConnectError = 0x10f, /// - /// HTTP_VERSION_FALLBACK (0x111): + /// H3_VERSION_FALLBACK (0x111): /// The requested operation cannot be served over HTTP/3. The peer should retry over HTTP/1.1. /// - VersionFallback = 0x111, + VersionFallback = 0x110, } } diff --git a/src/Shared/Http3/Frames/Http3Frame.cs b/src/Shared/Http3/Frames/Http3Frame.cs new file mode 100644 index 0000000000..f7ed1b96a5 --- /dev/null +++ b/src/Shared/Http3/Frames/Http3Frame.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace System.Net.Http +{ + internal static partial class Http3Frame + { + public const int MaximumEncodedFrameEnvelopeLength = 1 + VariableLengthIntegerHelper.MaximumEncodedLength; // Frame type + payload length. + + /// + /// Reads two variable-length integers. + /// + public static bool TryReadIntegerPair(ReadOnlySpan buffer, out long a, out long b, out int bytesRead) + { + if (VariableLengthIntegerHelper.TryRead(buffer, out a, out int aLength)) + { + buffer = buffer.Slice(aLength); + if (VariableLengthIntegerHelper.TryRead(buffer, out b, out int bLength)) + { + bytesRead = aLength + bLength; + return true; + } + } + + b = 0; + bytesRead = 0; + return false; + } + + // 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 (*) ... + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + public static bool TryWriteFrameEnvelope(Http3FrameType frameType, long payloadLength, Span buffer, out int bytesWritten) + { + Debug.Assert(VariableLengthIntegerHelper.GetByteCount((long)frameType) == 1, $"{nameof(TryWriteFrameEnvelope)} assumes {nameof(frameType)} will fit within a single byte varint."); + + if (buffer.Length != 0) + { + buffer[0] = (byte)frameType; + buffer = buffer.Slice(1); + + if (VariableLengthIntegerHelper.TryWrite(buffer, payloadLength, out int payloadLengthEncodedLength)) + { + bytesWritten = payloadLengthEncodedLength + 1; + return true; + } + } + + bytesWritten = 0; + return false; + } + } +} diff --git a/src/Shared/Http3/Frames/Http3FrameType.cs b/src/Shared/Http3/Frames/Http3FrameType.cs new file mode 100644 index 0000000000..252d6b76b6 --- /dev/null +++ b/src/Shared/Http3/Frames/Http3FrameType.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Net.Http +{ + internal enum Http3FrameType : long + { + Data = 0x0, + Headers = 0x1, + CancelPush = 0x3, + Settings = 0x4, + PushPromise = 0x5, + GoAway = 0x7, + MaxPushId = 0xD, + DuplicatePush = 0xE + } +} diff --git a/src/Shared/Http3/Helpers/VariableLengthIntegerHelper.cs b/src/Shared/Http3/Helpers/VariableLengthIntegerHelper.cs new file mode 100644 index 0000000000..010d2372fa --- /dev/null +++ b/src/Shared/Http3/Helpers/VariableLengthIntegerHelper.cs @@ -0,0 +1,209 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; + +namespace System.Net.Http +{ + /// + /// Variable length integer encoding and decoding methods. Based on https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-16. + /// A variable-length integer can use 1, 2, 4, or 8 bytes. + /// + internal static class VariableLengthIntegerHelper + { + public const int MaximumEncodedLength = 8; + + // The high 4 bits indicate the length of the integer. + // 00 = length 1 + // 01 = length 2 + // 10 = length 4 + // 11 = length 8 + private const byte LengthMask = 0xC0; + private const byte InitialOneByteLengthMask = 0x00; + private const byte InitialTwoByteLengthMask = 0x40; + private const byte InitialFourByteLengthMask = 0x80; + private const byte InitialEightByteLengthMask = 0xC0; + + // Bits to subtract to remove the length. + private const uint TwoByteLengthMask = 0x4000; + private const uint FourByteLengthMask = 0x80000000; + private const ulong EightByteLengthMask = 0xC000000000000000; + + public const uint OneByteLimit = (1U << 6) - 1; + private const uint TwoByteLimit = (1U << 16) - 1; + private const uint FourByteLimit = (1U << 30) - 1; + private const long EightByteLimit = (1L << 62) - 1; + + public static bool TryRead(ReadOnlySpan buffer, out long value, out int bytesRead) + { + if (buffer.Length != 0) + { + byte firstByte = buffer[0]; + + switch (firstByte & LengthMask) + { + case InitialOneByteLengthMask: + value = firstByte; + bytesRead = 1; + return true; + case InitialTwoByteLengthMask: + if (BinaryPrimitives.TryReadUInt16BigEndian(buffer, out ushort serializedShort)) + { + value = serializedShort - TwoByteLengthMask; + bytesRead = 2; + return true; + } + break; + case InitialFourByteLengthMask: + if (BinaryPrimitives.TryReadUInt32BigEndian(buffer, out uint serializedInt)) + { + value = serializedInt - FourByteLengthMask; + bytesRead = 4; + return true; + } + break; + default: // InitialEightByteLengthMask + Debug.Assert((firstByte & LengthMask) == InitialEightByteLengthMask); + if (BinaryPrimitives.TryReadUInt64BigEndian(buffer, out ulong serializedLong)) + { + value = (long)(serializedLong - EightByteLengthMask); + Debug.Assert(value >= 0 && value <= EightByteLimit, "Serialized values are within [0, 2^62)."); + + bytesRead = 8; + return true; + } + break; + } + } + + value = 0; + bytesRead = 0; + return false; + } + + public static bool TryRead(ref SequenceReader reader, out long value) + { + // Hot path: we probably have the entire integer in one unbroken span. + if (TryRead(reader.UnreadSpan, out value, out int bytesRead)) + { + reader.Advance(bytesRead); + return true; + } + + // Cold path: copy to a temporary buffer before calling span-based read. + return TryReadSlow(ref reader, out value); + + static bool TryReadSlow(ref SequenceReader reader, out long value) + { + ReadOnlySpan span = reader.CurrentSpan; + + if (reader.TryPeek(out byte firstByte)) + { + int length = + (firstByte & LengthMask) switch + { + InitialOneByteLengthMask => 1, + InitialTwoByteLengthMask => 2, + InitialFourByteLengthMask => 4, + _ => 8 // LengthEightByte + }; + + Span temp = (stackalloc byte[8])[..length]; + if (reader.TryCopyTo(temp)) + { + bool result = TryRead(temp, out value, out int bytesRead); + Debug.Assert(result == true); + Debug.Assert(bytesRead == length); + + reader.Advance(bytesRead); + return true; + } + } + + value = 0; + return false; + } + } + + public static long GetInteger(in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined) + { + var reader = new SequenceReader(buffer); + if (TryRead(ref reader, out long value)) + { + consumed = examined = buffer.GetPosition(reader.Consumed); + return value; + } + else + { + consumed = default; + examined = buffer.End; + return -1; + } + } + + public static bool TryWrite(Span buffer, long longToEncode, out int bytesWritten) + { + Debug.Assert(longToEncode >= 0); + Debug.Assert(longToEncode <= EightByteLimit); + + if (longToEncode < OneByteLimit) + { + if (buffer.Length != 0) + { + buffer[0] = (byte)longToEncode; + bytesWritten = 1; + return true; + } + } + else if (longToEncode < TwoByteLimit) + { + if (BinaryPrimitives.TryWriteUInt16BigEndian(buffer, (ushort)((uint)longToEncode | TwoByteLengthMask))) + { + bytesWritten = 2; + return true; + } + } + else if (longToEncode < FourByteLimit) + { + if (BinaryPrimitives.TryWriteUInt32BigEndian(buffer, (uint)longToEncode | FourByteLengthMask)) + { + bytesWritten = 4; + return true; + } + } + else // EightByteLimit + { + if (BinaryPrimitives.TryWriteUInt64BigEndian(buffer, (ulong)longToEncode | EightByteLengthMask)) + { + bytesWritten = 8; + return true; + } + } + + bytesWritten = 0; + return false; + } + + public static int WriteInteger(Span buffer, long longToEncode) + { + bool res = TryWrite(buffer, longToEncode, out int bytesWritten); + Debug.Assert(res == true); + return bytesWritten; + } + + public static int GetByteCount(long value) + { + Debug.Assert(value >= 0); + Debug.Assert(value <= EightByteLimit); + + return + value < OneByteLimit ? 1 : + value < TwoByteLimit ? 2 : + value < FourByteLimit ? 4 : + 8; // EightByteLimit + } + } +} diff --git a/src/Shared/Http3/Http3SettingType.cs b/src/Shared/Http3/Http3SettingType.cs new file mode 100644 index 0000000000..760446fa5c --- /dev/null +++ b/src/Shared/Http3/Http3SettingType.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Net.Http +{ + internal enum Http3SettingType : long + { + /// + /// SETTINGS_QPACK_MAX_TABLE_CAPACITY + /// The maximum dynamic table size. The default is 0. + /// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-5 + /// + QPackMaxTableCapacity = 0x1, + + /// + /// SETTINGS_MAX_HEADER_LIST_SIZE + /// The maximum size of headers. The default is unlimited. + /// https://tools.ietf.org/html/draft-ietf-quic-http-24#section-7.2.4.1 + /// + MaxHeaderListSize = 0x6, + + /// + /// SETTINGS_QPACK_BLOCKED_STREAMS + /// The maximum number of request streams that can be blocked waiting for QPack instructions. The default is 0. + /// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-5 + /// + QPackBlockedStreams = 0x7 + } +} diff --git a/src/Shared/Http3/Http3StreamType.cs b/src/Shared/Http3/Http3StreamType.cs new file mode 100644 index 0000000000..c3402f2550 --- /dev/null +++ b/src/Shared/Http3/Http3StreamType.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Net.Http +{ + /// + /// Unidirectional stream types. + /// + /// + /// Bidirectional streams are always a request stream. + /// + internal enum Http3StreamType : long + { + /// + /// https://tools.ietf.org/html/draft-ietf-quic-http-24#section-6.2.1 + /// + Control = 0x00, + /// + /// https://tools.ietf.org/html/draft-ietf-quic-http-24#section-6.2.2 + /// + Push = 0x01, + /// + /// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.2 + /// + QPackEncoder = 0x02, + /// + /// https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.2 + /// + QPackDecoder = 0x03 + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/StaticTable.cs b/src/Shared/Http3/QPack/H3StaticTable.cs similarity index 68% rename from src/Servers/Kestrel/Core/src/Internal/Http3/QPack/StaticTable.cs rename to src/Shared/Http3/QPack/H3StaticTable.cs index 4bf15a9b45..13fc509cc6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/StaticTable.cs +++ b/src/Shared/Http3/QPack/H3StaticTable.cs @@ -1,16 +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. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Net.Http; using System.Text; -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +namespace System.Net.Http.QPack { - internal class StaticTable + // TODO: make class static. + internal class H3StaticTable { - private static readonly StaticTable _instance = new StaticTable(); - private readonly Dictionary _statusIndex = new Dictionary { [103] = 24, @@ -31,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack private readonly Dictionary _methodIndex = new Dictionary { - // TODO connect is intenral to system.net.http + // TODO connect is internal to system.net.http [HttpMethod.Delete] = 16, [HttpMethod.Get] = 17, [HttpMethod.Head] = 18, @@ -40,16 +39,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack [HttpMethod.Put] = 21, }; - private StaticTable() + private H3StaticTable() { } - public static StaticTable Instance => _instance; + public static H3StaticTable Instance { get; } = new H3StaticTable(); public int Count => _staticTable.Length; public HeaderField this[int index] => _staticTable[index]; + // TODO: just use Dictionary directly to avoid interface dispatch. public IReadOnlyDictionary StatusIndex => _statusIndex; public IReadOnlyDictionary MethodIndex => _methodIndex; @@ -158,5 +158,68 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack private static HeaderField CreateHeaderField(string name, string value) => new HeaderField(Encoding.ASCII.GetBytes(name), Encoding.ASCII.GetBytes(value)); + + public const int Authority = 0; + public const int PathSlash = 1; + public const int Age0 = 2; + public const int ContentDisposition = 3; + public const int ContentLength0 = 4; + public const int Cookie = 5; + public const int Date = 6; + public const int ETag = 7; + public const int IfModifiedSince = 8; + public const int IfNoneMatch = 9; + public const int LastModified = 10; + public const int Link = 11; + public const int Location = 12; + public const int Referer = 13; + public const int SetCookie = 14; + public const int MethodConnect = 15; + public const int MethodDelete = 16; + public const int MethodGet = 17; + public const int MethodHead = 18; + public const int MethodOptions = 19; + public const int MethodPost = 20; + public const int MethodPut = 21; + public const int SchemeHttps = 23; + public const int Status103 = 24; + public const int Status200 = 25; + public const int Status304 = 26; + public const int Status404 = 27; + public const int Status503 = 28; + public const int AcceptAny = 29; + public const int AcceptEncodingGzipDeflateBr = 31; + public const int AcceptRangesBytes = 32; + public const int AccessControlAllowHeadersCacheControl = 33; + public const int AccessControlAllowOriginAny = 35; + public const int CacheControlMaxAge0 = 36; + public const int ContentEncodingBr = 42; + public const int ContentTypeApplicationDnsMessage = 44; + public const int RangeBytes0ToAll = 55; + public const int StrictTransportSecurityMaxAge31536000 = 56; + public const int VaryAcceptEncoding = 59; + public const int XContentTypeOptionsNoSniff = 61; + public const int Status100 = 63; + public const int Status204 = 64; + public const int Status206 = 65; + public const int Status302 = 66; + public const int Status400 = 67; + public const int Status403 = 68; + public const int Status421 = 69; + public const int Status425 = 70; + public const int Status500 = 71; + public const int AcceptLanguage = 72; + public const int AccessControlAllowCredentials = 73; + public const int AccessControlAllowMethodsGet = 76; + public const int AccessControlExposeHeadersContentLength = 79; + public const int AltSvcClear = 83; + public const int Authorization = 84; + public const int ContentSecurityPolicyAllNone = 85; + public const int IfRange = 89; + public const int Origin = 90; + public const int Server = 92; + public const int UpgradeInsecureRequests1 = 94; + public const int UserAgent = 95; + public const int XFrameOptionsDeny = 97; } } diff --git a/src/Shared/Http3/QPack/HeaderField.cs b/src/Shared/Http3/QPack/HeaderField.cs new file mode 100644 index 0000000000..12594381bd --- /dev/null +++ b/src/Shared/Http3/QPack/HeaderField.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Net.Http.QPack +{ + internal readonly struct HeaderField + { + public HeaderField(byte[] name, byte[] value) + { + Name = name; + Value = value; + } + + public byte[] Name { get; } + + public byte[] Value { get; } + + public int Length => Name.Length + Value.Length; + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecoder.cs b/src/Shared/Http3/QPack/QPackDecoder.cs similarity index 54% rename from src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecoder.cs rename to src/Shared/Http3/QPack/QPackDecoder.cs index 538a9ff2fc..1893f9db5e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPack/QPackDecoder.cs +++ b/src/Shared/Http3/QPack/QPackDecoder.cs @@ -1,21 +1,23 @@ -// 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. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. -using System; using System.Buffers; +using System.Diagnostics; using System.Net.Http.HPack; +using System.Numerics; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack +namespace System.Net.Http.QPack { - internal class QPackDecoder + internal class QPackDecoder : IDisposable { private enum State { - Ready, RequiredInsertCount, - RequiredInsertCountDone, + RequiredInsertCountContinue, Base, + BaseContinue, CompressedHeaders, HeaderFieldIndex, HeaderNameIndex, @@ -48,8 +50,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack //+---+---+---+---+---+---+---+---+ //| 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; @@ -60,7 +60,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack //| 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 @@ -71,9 +70,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack //+---+---------------------------+ //| 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; @@ -86,9 +82,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack //+---+---------------------------+ //| 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; @@ -102,9 +95,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack //+---+---------------------------+ //| 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; @@ -112,24 +102,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack 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; + private const int DefaultStringBufferSize = 64; + + private readonly int _maxHeadersLength; + private State _state = State.RequiredInsertCount; - // 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 int? _index; private byte[] _headerName; private int _headerNameLength; @@ -138,37 +122,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack 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)) { } + private static ArrayPool Pool => ArrayPool.Shared; - - // For testing. - internal QPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize, DynamicTable dynamicTable) + private static void ReturnAndGetNewPooledArray(ref byte[] buffer, int newSize) { - _maxDynamicTableSize = maxDynamicTableSize; - _dynamicTable = dynamicTable; + byte[] old = buffer; + buffer = null; - _stringOctets = new byte[maxRequestHeaderFieldSize]; - _headerNameOctets = new byte[maxRequestHeaderFieldSize]; - _headerValueOctets = new byte[maxRequestHeaderFieldSize]; + Pool.Return(old, clearArray: true); + buffer = Pool.Rent(newSize); + } + + public QPackDecoder(int maxHeadersLength) + { + _maxHeadersLength = maxHeadersLength; + + // TODO: make allocation lazy? with static entries it's possible no buffers will be needed. + _stringOctets = Pool.Rent(DefaultStringBufferSize); + _headerNameOctets = Pool.Rent(DefaultStringBufferSize); + _headerValueOctets = Pool.Rent(DefaultStringBufferSize); + } + + public void Dispose() + { + if (_stringOctets != null) + { + Pool.Return(_stringOctets, true); + _stringOctets = null; + } + + if (_headerNameOctets != null) + { + Pool.Return(_headerNameOctets, true); + _headerNameOctets = null; + } + + if (_headerValueOctets != null) + { + Pool.Return(_headerValueOctets, true); + _headerValueOctets = null; + } } - // 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) + foreach (ReadOnlyMemory segment in headerBlock) { - var span = segment.Span; - for (var i = 0; i < span.Length; i++) - { - OnByte(span[i], handler); - } + Decode(segment.Span, handler); + } + } + + public void Decode(ReadOnlySpan headerBlock, IHttpHeadersHandler handler) + { + foreach (byte b in headerBlock) + { + OnByte(b, handler); } } @@ -178,120 +186,118 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack int prefixInt; switch (_state) { - case State.Ready: + case State.RequiredInsertCount: if (_integerDecoder.BeginTryDecode(b, RequiredInsertCountPrefix, out intResult)) { OnRequiredInsertCount(intResult); } else { - _state = State.RequiredInsertCount; + _state = State.RequiredInsertCountContinue; } break; - case State.RequiredInsertCount: + case State.RequiredInsertCountContinue: if (_integerDecoder.TryDecode(b, out intResult)) { OnRequiredInsertCount(intResult); } break; - case State.RequiredInsertCountDone: + case State.Base: prefixInt = ~BaseMask & b; - _s = (b & BaseMask) == BaseMask; - if (_integerDecoder.BeginTryDecode(b, BasePrefix, out intResult)) { OnBase(intResult); } else { - _state = State.Base; + _state = State.BaseContinue; } break; - case State.Base: + case State.BaseContinue: if (_integerDecoder.TryDecode(b, out intResult)) { OnBase(intResult); } break; case State.CompressedHeaders: - if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation) + switch (BitOperations.LeadingZeroCount(b)) { - 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 + case 24: // Indexed Header Field + prefixInt = IndexedHeaderFieldPrefixMask & b; - _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; + bool useStaticTable = (b & IndexedHeaderStaticMask) == IndexedHeaderStaticRepresentation; - if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldWithoutNameReferencePrefix, out intResult)) - { - OnStringLength(intResult, State.HeaderName); - } - else - { - _state = State.HeaderNameLength; - } + if (!useStaticTable) + { + ThrowDynamicTableNotSupported(); + } + + if (_integerDecoder.BeginTryDecode((byte)prefixInt, IndexedHeaderFieldPrefix, out intResult)) + { + OnIndexedHeaderField(intResult, handler); + } + else + { + _state = State.HeaderFieldIndex; + } + break; + case 25: // Literal Header Field With Name Reference + useStaticTable = (LiteralHeaderFieldStaticMask & b) == LiteralHeaderFieldStaticMask; + + if (!useStaticTable) + { + ThrowDynamicTableNotSupported(); + } + + prefixInt = b & LiteralHeaderFieldPrefixMask; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPrefix, out intResult)) + { + OnIndexedHeaderName(intResult); + } + else + { + _state = State.HeaderNameIndex; + } + break; + case 26: // Literal Header Field Without Name Reference + _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 27: // Indexed Header Field With Post-Base Index + prefixInt = ~PostBaseIndexMask & b; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, PostBaseIndexPrefix, out intResult)) + { + OnPostBaseIndex(intResult, handler); + } + else + { + _state = State.PostBaseIndex; + } + break; + default: // Literal Header Field With Post-Base Name Reference (at least 4 zeroes, maybe more) + prefixInt = b & LiteralHeaderFieldPostBasePrefixMask; + if (_integerDecoder.BeginTryDecode((byte)prefixInt, LiteralHeaderFieldPostBasePrefix, out intResult)) + { + OnIndexedHeaderNamePostBase(intResult); + } + else + { + _state = State.HeaderNameIndexPostBase; + } + break; } break; case State.HeaderNameLength: - // huffman has already been processed. if (_integerDecoder.TryDecode(b, out intResult)) { OnStringLength(intResult, nextState: State.HeaderName); @@ -373,7 +379,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack { if (length > _stringOctets.Length) { - throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.FormatQPackStringLengthTooLarge(length, _stringOctets.Length)*/); + if (length > _maxHeadersLength) + { + throw new QPackDecodingException(SR.Format(SR.net_http_headers_exceeded_length, _maxHeadersLength)); + } + + ReturnAndGetNewPooledArray(ref _stringOctets, length); } _stringLength = length; @@ -385,20 +396,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack { OnString(nextState: State.CompressedHeaders); - var headerNameSpan = new Span(_headerName, 0, _headerNameLength); - var headerValueSpan = new Span(_headerValueOctets, 0, _headerValueLength); + Span headerNameSpan; + Span headerValueSpan = _headerValueOctets.AsSpan(0, _headerValueLength); + + if (_index is int index) + { + Debug.Assert(index >= 0 && index <= H3StaticTable.Instance.Count, $"The index should be a valid static index here. {nameof(QPackDecoder)} should have previously thrown if it read a dynamic index."); + handler.OnStaticIndexedHeader(index, headerValueSpan); + _index = null; + + return; + } + else + { + headerNameSpan = _headerNameOctets.AsSpan(0, _headerNameLength); + } handler.OnHeader(headerNameSpan, headerValueSpan); - - if (_index) - { - _dynamicTable.Insert(headerNameSpan, headerValueSpan); - } } private void OnString(State nextState) { - int Decode(byte[] dst) + int Decode(ref byte[] dst) { if (_huffman) { @@ -406,6 +425,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack } else { + if (dst.Length < _stringLength) + { + ReturnAndGetNewPooledArray(ref dst, _stringLength); + } + Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength); return _stringLength; } @@ -415,17 +439,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack { if (_state == State.HeaderName) { + _headerNameLength = Decode(ref _headerNameOctets); _headerName = _headerNameOctets; - _headerNameLength = Decode(_headerNameOctets); } else { - _headerValueLength = Decode(_headerValueOctets); + _headerValueLength = Decode(ref _headerValueOctets); } } catch (HuffmanDecodingException ex) { - throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.QPackHuffmanError, */, ex); + throw new QPackDecodingException(SR.net_http_hpack_huffman_decode_failed, ex); } _state = nextState; @@ -434,80 +458,52 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.QPack private void OnIndexedHeaderName(int index) { - var header = GetHeader(index); - _headerName = header.Name; - _headerNameLength = header.Name.Length; + _index = index; _state = State.HeaderValueLength; } private void OnIndexedHeaderNamePostBase(int index) { + ThrowDynamicTableNotSupported(); // TODO update with postbase index - var header = GetHeader(index); - _headerName = header.Name; - _headerNameLength = header.Name.Length; - _state = State.HeaderValueLength; + // _index = index; + // _state = State.HeaderValueLength; } private void OnPostBaseIndex(int intResult, IHttpHeadersHandler handler) { + ThrowDynamicTableNotSupported(); // TODO - _state = State.CompressedHeaders; + // _state = State.CompressedHeaders; } private void OnBase(int deltaBase) { + if (deltaBase != 0) + { + ThrowDynamicTableNotSupported(); + } _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) - // { - - // } + if (requiredInsertCount != 0) + { + ThrowDynamicTableNotSupported(); + } + _state = State.Base; } 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)); + handler.OnStaticIndexedHeader(index); _state = State.CompressedHeaders; } - private HeaderField GetHeader(int index) + private static void ThrowDynamicTableNotSupported() { - try - { - return _s ? StaticTable.Instance[index] : _dynamicTable[index]; - } - catch (IndexOutOfRangeException ex) - { - throw new QPackDecodingException("TODO sync with corefx" /*CoreStrings.FormatQPackErrorIndexOutOfRange(index), */, ex); - } + throw new QPackDecodingException("No dynamic table support"); } } } diff --git a/src/Shared/Http3/QPack/QPackDecodingException.cs b/src/Shared/Http3/QPack/QPackDecodingException.cs new file mode 100644 index 0000000000..bbdba5d773 --- /dev/null +++ b/src/Shared/Http3/QPack/QPackDecodingException.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.Serialization; + +namespace System.Net.Http.QPack +{ + [Serializable] + internal sealed class QPackDecodingException : Exception + { + public QPackDecodingException() + { + } + + public QPackDecodingException(string message) : base(message) + { + } + + public QPackDecodingException(string message, Exception innerException) : base(message, innerException) + { + } + + private QPackDecodingException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Shared/Http3/QPack/QPackEncoder.cs b/src/Shared/Http3/QPack/QPackEncoder.cs new file mode 100644 index 0000000000..348106aa15 --- /dev/null +++ b/src/Shared/Http3/QPack/QPackEncoder.cs @@ -0,0 +1,428 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http.HPack; + +namespace System.Net.Http.QPack +{ + internal class QPackEncoder + { + private IEnumerator> _enumerator; + + // https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.5.2 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | T | Index (6+) | + // +---+---+-----------------------+ + // + // Note for this method's implementation of above: + // - T is constant 1 here, indicating a static table reference. + public static bool EncodeStaticIndexedHeaderField(int index, Span destination, out int bytesWritten) + { + if (!destination.IsEmpty) + { + destination[0] = 0b11000000; + return IntegerEncoder.Encode(index, 6, destination, out bytesWritten); + } + else + { + bytesWritten = 0; + return false; + } + } + + public static byte[] EncodeStaticIndexedHeaderFieldToArray(int index) + { + Span buffer = stackalloc byte[IntegerEncoder.MaxInt32EncodedLength]; + + bool res = EncodeStaticIndexedHeaderField(index, buffer, out int bytesWritten); + Debug.Assert(res == true); + + return buffer.Slice(0, bytesWritten).ToArray(); + } + + // https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.5.4 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | N | T |Name Index (4+)| + // +---+---+---+---+---------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length bytes) | + // +-------------------------------+ + // + // Note for this method's implementation of above: + // - N is constant 0 here, indicating intermediates (proxies) can compress the header when fordwarding. + // - T is constant 1 here, indicating a static table reference. + // - H is constant 0 here, as we do not yet perform Huffman coding. + public static bool EncodeLiteralHeaderFieldWithStaticNameReference(int index, string value, Span destination, out int bytesWritten) + { + // Requires at least two bytes (one for name reference header, one for value length) + if (destination.Length >= 2) + { + destination[0] = 0b01010000; + if (IntegerEncoder.Encode(index, 4, destination, out int headerBytesWritten)) + { + destination = destination.Slice(headerBytesWritten); + + if (EncodeValueString(value, destination, out int valueBytesWritten)) + { + bytesWritten = headerBytesWritten + valueBytesWritten; + return true; + } + } + } + + bytesWritten = 0; + return false; + } + + /// + /// Encodes just the name part of a Literal Header Field With Static Name Reference. Must call after to encode the header's value. + /// + public static byte[] EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(int index) + { + Span temp = stackalloc byte[IntegerEncoder.MaxInt32EncodedLength]; + + temp[0] = 0b01110000; + bool res = IntegerEncoder.Encode(index, 4, temp, out int headerBytesWritten); + Debug.Assert(res == true); + + return temp.Slice(0, headerBytesWritten).ToArray(); + } + + public static byte[] EncodeLiteralHeaderFieldWithStaticNameReferenceToArray(int index, string value) + { + Span temp = value.Length < 256 ? stackalloc byte[256 + IntegerEncoder.MaxInt32EncodedLength * 2] : new byte[value.Length + IntegerEncoder.MaxInt32EncodedLength * 2]; + bool res = EncodeLiteralHeaderFieldWithStaticNameReference(index, value, temp, out int bytesWritten); + Debug.Assert(res == true); + return temp.Slice(0, bytesWritten).ToArray(); + } + + // https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-4.5.6 + // 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) | + // +-------------------------------+ + // + // Note for this method's implementation of above: + // - N is constant 0 here, indicating intermediates (proxies) can compress the header when fordwarding. + // - H is constant 0 here, as we do not yet perform Huffman coding. + public static bool EncodeLiteralHeaderFieldWithoutNameReference(string name, string value, Span destination, out int bytesWritten) + { + if (EncodeNameString(name, destination, out int nameLength) && EncodeValueString(value, destination.Slice(nameLength), out int valueLength)) + { + bytesWritten = nameLength + valueLength; + return true; + } + else + { + bytesWritten = 0; + return false; + } + } + + /// + /// Encodes a Literal Header Field Without Name Reference, building the value by concatenating a collection of strings with separators. + /// + public static bool EncodeLiteralHeaderFieldWithoutNameReference(string name, ReadOnlySpan values, string valueSeparator, Span destination, out int bytesWritten) + { + if (EncodeNameString(name, destination, out int nameLength) && EncodeValueString(values, valueSeparator, destination.Slice(nameLength), out int valueLength)) + { + bytesWritten = nameLength + valueLength; + return true; + } + + bytesWritten = 0; + return false; + } + + /// + /// Encodes just the value part of a Literawl Header Field Without Static Name Reference. Must call after to encode the header's value. + /// + public static byte[] EncodeLiteralHeaderFieldWithoutNameReferenceToArray(string name) + { + Span temp = name.Length < 256 ? stackalloc byte[256 + IntegerEncoder.MaxInt32EncodedLength] : new byte[name.Length + IntegerEncoder.MaxInt32EncodedLength]; + + bool res = EncodeNameString(name, temp, out int nameLength); + Debug.Assert(res == true); + + return temp.Slice(0, nameLength).ToArray(); + } + + public static byte[] EncodeLiteralHeaderFieldWithoutNameReferenceToArray(string name, string value) + { + Span temp = (name.Length + value.Length) < 256 ? stackalloc byte[256 + IntegerEncoder.MaxInt32EncodedLength * 2] : new byte[name.Length + value.Length + IntegerEncoder.MaxInt32EncodedLength * 2]; + + bool res = EncodeLiteralHeaderFieldWithoutNameReference(name, value, temp, out int bytesWritten); + Debug.Assert(res == true); + + return temp.Slice(0, bytesWritten).ToArray(); + } + + private static bool EncodeValueString(string s, Span buffer, out int length) + { + if (buffer.Length != 0) + { + buffer[0] = 0; + if (IntegerEncoder.Encode(s.Length, 7, buffer, out int nameLength)) + { + buffer = buffer.Slice(nameLength); + if (buffer.Length >= s.Length) + { + EncodeValueStringPart(s, buffer); + + length = nameLength + s.Length; + return true; + } + } + } + + length = 0; + return false; + } + + /// + /// Encodes a value by concatenating a collection of strings, separated by a separator string. + /// + public static bool EncodeValueString(ReadOnlySpan values, string separator, Span buffer, out int length) + { + if (values.Length == 1) + { + return EncodeValueString(values[0], buffer, out length); + } + + if (values.Length == 0) + { + // TODO: this will be called with a string array from HttpHeaderCollection. Can we ever get a 0-length array from that? Assert if not. + return EncodeValueString(string.Empty, buffer, out length); + } + + if (buffer.Length > 0) + { + int valueLength = separator.Length * (values.Length - 1); + for (int i = 0; i < values.Length; ++i) + { + valueLength += values[i].Length; + } + + buffer[0] = 0; + if (IntegerEncoder.Encode(valueLength, 7, buffer, out int nameLength)) + { + buffer = buffer.Slice(nameLength); + if (buffer.Length >= valueLength) + { + string value = values[0]; + EncodeValueStringPart(value, buffer); + buffer = buffer.Slice(value.Length); + + for (int i = 1; i < values.Length; ++i) + { + EncodeValueStringPart(separator, buffer); + buffer = buffer.Slice(separator.Length); + + value = values[i]; + EncodeValueStringPart(value, buffer); + buffer = buffer.Slice(value.Length); + } + + length = nameLength + valueLength; + return true; + } + } + } + + length = 0; + return false; + } + + private static void EncodeValueStringPart(string s, Span buffer) + { + Debug.Assert(buffer.Length >= s.Length); + + for (int i = 0; i < s.Length; ++i) + { + char ch = s[i]; + + if (ch > 127) + { + throw new QPackEncodingException("ASCII header value."); + } + + buffer[i] = (byte)ch; + } + } + + private static bool EncodeNameString(string s, Span buffer, out int length) + { + const int toLowerMask = 0x20; + + if (buffer.Length != 0) + { + buffer[0] = 0x30; + + if (IntegerEncoder.Encode(s.Length, 3, buffer, out int nameLength)) + { + buffer = buffer.Slice(nameLength); + + if (buffer.Length >= s.Length) + { + for (int i = 0; i < s.Length; ++i) + { + int ch = s[i]; + Debug.Assert(ch <= 127, "HttpHeaders prevents adding non-ASCII header names."); + + if ((uint)(ch - 'A') <= 'Z' - 'A') + { + ch |= toLowerMask; + } + + buffer[i] = (byte)ch; + } + + length = nameLength + s.Length; + return true; + } + } + } + + length = 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; + } + + // 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(); + + bool hasValue = _enumerator.MoveNext(); + Debug.Assert(hasValue == true); + + 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(); + + bool hasValue = _enumerator.MoveNext(); + Debug.Assert(hasValue == true); + + // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#header-prefix + buffer[0] = 0; + buffer[1] = 0; + + int statusCodeLength = EncodeStatusCode(statusCode, buffer.Slice(2)); + bool done = Encode(buffer.Slice(statusCodeLength + 2), throwIfNoneEncoded: false, out int 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 (!EncodeLiteralHeaderFieldWithoutNameReference(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(length), out int headerLength)) + { + if (length == 0 && throwIfNoneEncoded) + { + throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */); + } + return false; + } + + length += headerLength; + } while (_enumerator.MoveNext()); + + return true; + } + + // TODO: use H3StaticTable? + 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 | H3StaticTable.Instance.StatusIndex[statusCode]); + return 1; + default: + // Send as Literal Header Field Without Indexing - Indexed Name + buffer[0] = 0x08; + + ReadOnlySpan statusBytes = StatusCodes.ToStatusBytes(statusCode); + buffer[1] = (byte)statusBytes.Length; + statusBytes.CopyTo(buffer.Slice(2)); + + return 2 + statusBytes.Length; + } + } + } +} diff --git a/src/Shared/Http3/QPack/QPackEncodingException.cs b/src/Shared/Http3/QPack/QPackEncodingException.cs new file mode 100644 index 0000000000..9c5907a3a9 --- /dev/null +++ b/src/Shared/Http3/QPack/QPackEncodingException.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.Serialization; + +namespace System.Net.Http.QPack +{ + [Serializable] + internal sealed class QPackEncodingException : Exception + { + public QPackEncodingException(string message) + : base(message) + { + } + public QPackEncodingException(string message, Exception innerException) + : base(message, innerException) + { + } + + private QPackEncodingException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Shared/Http3/ReadMe.SharedCode.md b/src/Shared/Http3/ReadMe.SharedCode.md new file mode 100644 index 0000000000..bbb04284ad --- /dev/null +++ b/src/Shared/Http3/ReadMe.SharedCode.md @@ -0,0 +1,36 @@ +The code in this directory is shared between dotnet/runtime and aspnet/AspNetCore. This contains HTTP/3 protocol infrastructure such as a QPACK implementation. Any changes to this dir need to be checked into both repositories. + +dotnet/runtime code paths: +- runtime\src\libraries\Common\src\System\Net\Http\Http3 +- runtime\src\libraries\Common\tests\Tests\System\Net\Http3 +aspnet/AspNetCore code paths: +- AspNetCore\src\Shared\Http3 +- AspNetCore\src\Shared\test\Shared.Tests\Http3 + +## Copying code +To copy code from dotnet/runtime to aspnet/AspNetCore, set ASPNETCORE_REPO to the AspNetCore repo root and then run CopyToAspNetCore.cmd. +To copy code from aspnet/AspNetCore to dotnet/runtime, set RUNTIME_REPO to the runtime repo root and then run CopyToRuntime.cmd. + +## Building dotnet/runtime code: +- https://github.com/dotnet/runtime/blob/master/docs/libraries/building/windows-instructions.md +- https://github.com/dotnet/runtime/blob/master/docs/libraries/project-docs/developer-guide.md +- Run libraries.cmd from the root once: `PS D:\github\runtime> .\libraries.cmd` +- Build the individual projects: +- `PS D:\github\dotnet\src\libraries\Common\tests> dotnet msbuild /t:rebuild` +- `PS D:\github\dotnet\src\libraries\System.Net.Http\src> dotnet msbuild /t:rebuild` + +### Running dotnet/runtime tests: +- `PS D:\github\runtime\src\libraries\Common\tests> dotnet msbuild /t:rebuildandtest` +- `PS D:\github\runtime\src\libraries\System.Net.Http\tests\UnitTests> dotnet msbuild /t:rebuildandtest` + +## Building aspnet/AspNetCore code: +- https://github.com/aspnet/AspNetCore/blob/master/docs/BuildFromSource.md +- Run restore in the root once: `PS D:\github\AspNetCore> .\restore.cmd` +- Activate to use the repo local runtime: `PS D:\github\AspNetCore> . .\activate.ps1` +- Build the individual projects: +- `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet msbuild` +- `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\src> dotnet msbuild` + +### Running aspnet/AspNetCore tests: +- `(AspNetCore) PS D:\github\AspNetCore\src\Shared\test\Shared.Tests> dotnet test` +- `(AspNetCore) PS D:\github\AspNetCore\src\servers\Kestrel\core\test> dotnet test` diff --git a/src/Shared/test/Shared.Tests/Http2/HPackDecoderTest.cs b/src/Shared/test/Shared.Tests/Http2/HPackDecoderTest.cs index 9e80a509b7..d7bd47845c 100644 --- a/src/Shared/test/Shared.Tests/Http2/HPackDecoderTest.cs +++ b/src/Shared/test/Shared.Tests/Http2/HPackDecoderTest.cs @@ -106,6 +106,18 @@ namespace System.Net.Http.Unit.Tests.HPack _decodedHeaders[headerName] = headerValue; } + void IHttpHeadersHandler.OnStaticIndexedHeader(int index) + { + // Not yet implemented for HPACK. + throw new NotImplementedException(); + } + + void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + // Not yet implemented for HPACK. + throw new NotImplementedException(); + } + void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } [Fact]