From 3ecdc403189a28e2f36723663f50e58c4637e3aa Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Fri, 1 Nov 2019 17:32:21 -0700 Subject: [PATCH] Synchronize Http/2 HPack implementation between CoreFx and ASP.NET Core (#13931) --- .github/CODEOWNERS | 6 +- ...pNetCore.Server.Kestrel.Core.netcoreapp.cs | 18 - src/Servers/Kestrel/Core/src/CoreStrings.resx | 60 -- .../src/Internal/Http/Http1ParsingHandler.cs | 5 +- .../Internal/Http/HttpHeaders.Generated.cs | 2 +- .../Core/src/Internal/Http/HttpParser.cs | 5 +- .../Core/src/Internal/Http/HttpProtocol.cs | 4 +- .../src/Internal/Http/HttpRequestHeaders.cs | 4 +- .../src/Internal/Http/IHttpHeadersHandler.cs | 13 - .../Core/src/Internal/Http/IHttpParser.cs | 3 +- .../src/Internal/Http2/HPack/HPackDecoder.cs | 433 -------------- .../Http2/HPack/HPackDecodingException.cs | 19 - .../src/Internal/Http2/HPack/HPackEncoder.cs | 160 ------ .../src/Internal/Http2/HPack/HeaderField.cs | 30 - .../Http2/HPack/HuffmanDecodingException.cs | 15 - .../Internal/Http2/HPack/IntegerDecoder.cs | 67 --- .../Internal/Http2/HPack/IntegerEncoder.cs | 64 --- .../src/Internal/Http2/HPack/StaticTable.cs | 105 ---- .../src/Internal/Http2/HPack/StatusCodes.cs | 158 ------ .../src/Internal/Http2/Http2Connection.cs | 13 +- .../src/Internal/Http2/Http2FrameWriter.cs | 2 +- .../Internal/Infrastructure/HttpUtilities.cs | 10 +- .../Internal/Infrastructure/IKestrelTrace.cs | 2 +- .../Internal/Infrastructure/KestrelTrace.cs | 2 +- .../Infrastructure/StringUtilities.cs | 2 +- ...soft.AspNetCore.Server.Kestrel.Core.csproj | 7 +- .../Kestrel/Core/test/DynamicTableTests.cs | 174 ------ .../Kestrel/Core/test/HPackEncoderTests.cs | 2 +- .../Kestrel/Core/test/HPackIntegerTests.cs | 91 --- .../Kestrel/Core/test/HttpParserTests.cs | 6 +- .../Kestrel/Core/test/IntegerDecoderTests.cs | 86 --- .../Kestrel/Core/test/IntegerEncoderTests.cs | 36 -- ...spNetCore.Server.Kestrel.Core.Tests.csproj | 1 + .../Http1ConnectionBenchmark.cs | 6 +- .../HttpParserBenchmark.cs | 12 +- .../IntegerDecoderBenchmark.cs | 4 +- .../Kestrel.Performance/Mocks/MockTrace.cs | 2 +- .../Kestrel.Performance/Mocks/NullParser.cs | 4 +- .../samples/http2cat/Http2Utilities.cs | 7 +- src/Servers/Kestrel/shared/KnownHeaders.cs | 2 +- .../shared/test/CompositeKestrelTrace.cs | 2 +- .../Http2/Http2ConnectionTests.cs | 9 +- .../Http2/Http2StreamTests.cs | 9 +- .../Http2/Http2TestBase.cs | 7 +- src/Shared/Http2/CopyToAspNetCore.cmd | 14 + src/Shared/Http2/CopyToCoreFx.cmd | 14 + .../Http2/Hpack}/DynamicTable.cs | 39 +- src/Shared/Http2/Hpack/HPackDecoder.cs | 491 ++++++++++++++++ .../Http2/Hpack/HPackDecodingException.cs | 29 + src/Shared/Http2/Hpack/HPackEncoder.cs | 530 ++++++++++++++++++ .../Http2/Hpack}/HPackEncodingException.cs | 14 +- src/Shared/Http2/Hpack/HeaderField.cs | 51 ++ .../HPack => Shared/Http2/Hpack}/Huffman.cs | 46 +- .../Http2/Hpack/HuffmanDecodingException.cs | 37 ++ src/Shared/Http2/Hpack/IntegerDecoder.cs | 103 ++++ src/Shared/Http2/Hpack/IntegerEncoder.cs | 73 +++ src/Shared/Http2/Hpack/StaticTable.cs | 159 ++++++ src/Shared/Http2/Hpack/StatusCodes.cs | 218 +++++++ src/Shared/Http2/IHttpHeadersHandler.cs | 12 + src/Shared/Http2/ReadMe.SharedCode.md | 36 ++ src/Shared/Http2/SR.cs | 20 + src/Shared/Http2/SR.resx | 156 ++++++ src/Shared/Shared.sln | 25 + .../Shared.Tests/Http2/DynamicTableTest.cs | 255 +++++++++ .../Shared.Tests/Http2/HPackDecoderTest.cs} | 142 ++--- .../Shared.Tests/Http2/HPackIntegerTest.cs | 129 +++++ .../Http2/HuffmanDecodingTests.cs} | 242 +++++++- .../Shared.Tests/Http2/ReadMe.SharedCode.md | 3 + .../Microsoft.AspNetCore.Shared.Tests.csproj | 25 +- 69 files changed, 2796 insertions(+), 1736 deletions(-) delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http/IHttpHeadersHandler.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackDecoder.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackDecodingException.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackEncoder.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HeaderField.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HuffmanDecodingException.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/IntegerDecoder.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/IntegerEncoder.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StaticTable.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs delete mode 100644 src/Servers/Kestrel/Core/test/DynamicTableTests.cs delete mode 100644 src/Servers/Kestrel/Core/test/HPackIntegerTests.cs delete mode 100644 src/Servers/Kestrel/Core/test/IntegerDecoderTests.cs delete mode 100644 src/Servers/Kestrel/Core/test/IntegerEncoderTests.cs create mode 100644 src/Shared/Http2/CopyToAspNetCore.cmd create mode 100644 src/Shared/Http2/CopyToCoreFx.cmd rename src/{Servers/Kestrel/Core/src/Internal/Http2/HPack => Shared/Http2/Hpack}/DynamicTable.cs (68%) create mode 100644 src/Shared/Http2/Hpack/HPackDecoder.cs create mode 100644 src/Shared/Http2/Hpack/HPackDecodingException.cs create mode 100644 src/Shared/Http2/Hpack/HPackEncoder.cs rename src/{Servers/Kestrel/Core/src/Internal/Http2/HPack => Shared/Http2/Hpack}/HPackEncodingException.cs (53%) create mode 100644 src/Shared/Http2/Hpack/HeaderField.cs rename src/{Servers/Kestrel/Core/src/Internal/Http2/HPack => Shared/Http2/Hpack}/Huffman.cs (93%) create mode 100644 src/Shared/Http2/Hpack/HuffmanDecodingException.cs create mode 100644 src/Shared/Http2/Hpack/IntegerDecoder.cs create mode 100644 src/Shared/Http2/Hpack/IntegerEncoder.cs create mode 100644 src/Shared/Http2/Hpack/StaticTable.cs create mode 100644 src/Shared/Http2/Hpack/StatusCodes.cs create mode 100644 src/Shared/Http2/IHttpHeadersHandler.cs create mode 100644 src/Shared/Http2/ReadMe.SharedCode.md create mode 100644 src/Shared/Http2/SR.cs create mode 100644 src/Shared/Http2/SR.resx create mode 100644 src/Shared/Shared.sln create mode 100644 src/Shared/test/Shared.Tests/Http2/DynamicTableTest.cs rename src/{Servers/Kestrel/Core/test/HPackDecoderTests.cs => Shared/test/Shared.Tests/Http2/HPackDecoderTest.cs} (78%) create mode 100644 src/Shared/test/Shared.Tests/Http2/HPackIntegerTest.cs rename src/{Servers/Kestrel/Core/test/HuffmanTests.cs => Shared/test/Shared.Tests/Http2/HuffmanDecodingTests.cs} (72%) create mode 100644 src/Shared/test/Shared.Tests/Http2/ReadMe.SharedCode.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index cb9cb3cdff..30ce38000c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,9 +14,11 @@ /src/Hosting/ @tratcher @anurse /src/Http/ @tratcher @jkotalik @anurse /src/Middleware/ @tratcher @anurse +/src/Middleware/HttpsPolicy @jkotalik @anurse +/src/Middleware/Rewrite @jkotalik @anurse # /src/ProjectTemplates/ @ryanbrandenburg /src/Security/ @tratcher @anurse /src/Servers/ @tratcher @jkotalik @anurse @halter73 -/src/Middleware/Rewrite @jkotalik @anurse -/src/Middleware/HttpsPolicy @jkotalik @anurse +/src/Shared/Http2 @aspnet/http2 +/src/Shared/test/Shared.Tests/Http2 @aspnet/http2 /src/SignalR/ @BrennanConroy @halter73 @anurse 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 d05a081d24..7e77770b7a 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 @@ -204,14 +204,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http Custom = (byte)9, None = (byte)255, } - public partial class HttpParser : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpParser where TRequestHandler : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpHeadersHandler, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpRequestLineHandler - { - public HttpParser() { } - public HttpParser(bool showErrorDetails) { } - bool Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpParser.ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence buffer, out System.SequencePosition consumed, out System.SequencePosition examined) { throw null; } - public bool ParseHeaders(TRequestHandler handler, ref System.Buffers.SequenceReader reader) { throw null; } - public bool ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence buffer, out System.SequencePosition consumed, out System.SequencePosition examined) { throw null; } - } public enum HttpScheme { Unknown = -1, @@ -225,16 +217,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http Http11 = 1, Http2 = 2, } - public partial interface IHttpHeadersHandler - { - void OnHeader(System.Span name, System.Span value); - void OnHeadersComplete(); - } - public partial interface IHttpParser where TRequestHandler : Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpHeadersHandler, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.IHttpRequestLineHandler - { - bool ParseHeaders(TRequestHandler handler, ref System.Buffers.SequenceReader reader); - bool ParseRequestLine(TRequestHandler handler, in System.Buffers.ReadOnlySequence buffer, out System.SequencePosition consumed, out System.SequencePosition examined); - } public partial interface IHttpRequestLineHandler { void OnStartLine(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod method, Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpVersion version, System.Span target, System.Span path, System.Span query, System.Span customMethod, bool pathEncoded); diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index f0211469e1..a5b2b5ff5a 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -249,9 +249,6 @@ No listening endpoints were configured. Binding to {address} by default. - - HTTPS endpoints can only be configured using {methodName}. - A path base can only be configured using {methodName}. @@ -261,9 +258,6 @@ Failed to bind to address {endpoint}: address already in use. - - Invalid URL: '{url}'. - Unable to bind to {address} on the {interfaceName} interface: '{error}'. @@ -288,9 +282,6 @@ {name} cannot be set because the response has already started. - - Request processing didn't complete within the shutdown timeout. - Response Content-Length mismatch: too few bytes written ({written} of {expected}). @@ -330,9 +321,6 @@ Value must be a positive TimeSpan. - - Value must be a non-negative TimeSpan. - The request body rate enforcement grace period must be greater than {heartbeatInterval} second. @@ -357,30 +345,6 @@ HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint. - - A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets. - - - Index {index} is outside the bounds of the header field table. - - - Input data could not be fully decoded. - - - Input data contains the EOS symbol. - - - The destination buffer is not large enough to store the decoded data. - - - Huffman decoding error. - - - Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets. - - - The header block was incomplete and could not be fully decoded. - The client sent a {frameType} frame with even stream ID {streamId}. @@ -459,9 +423,6 @@ Request headers contain connection-specific header field. - - Unable to configure default https bindings because no IDefaultHttpsProvider service was provided. - Failed to authenticate HTTPS connection. @@ -471,9 +432,6 @@ Certificate {thumbprint} cannot be used as an SSL server certificate. It has an Extended Key Usage extension but the usages do not include Server Authentication (OID 1.3.6.1.5.5.7.3.1). - - Value must be a positive TimeSpan. - The server certificate parameter is required. @@ -575,30 +533,12 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l A value between {min} and {max} is required. - - Dynamic tables size update did not occur at the beginning of the first header block. - - - The given buffer was too small to encode any headers. - - - The decoded integer exceeds the maximum value of Int32.MaxValue. - The client closed the connection. A frame of type {frameType} was received after stream {streamId} was reset or aborted. - - HTTP protocol selection failed. - - - Server shutdown started during connection initialization. - - - Cannot call GetMemory() until response has started. Call HttpResponse.StartAsync() before calling GetMemory(). - This feature is not supported for HTTP/2 requests except to disable it entirely by setting the rate to null. diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs index b4c67c13a3..f76ab52a8f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ParsingHandler.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.Http { @@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http Trailers = trailers; } - public void OnHeader(Span name, Span value) + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { if (Trailers) { @@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - public void OnHeadersComplete() + public void OnHeadersComplete(bool endStream) { if (Trailers) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs index e626a19d9c..b21c4914a8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs @@ -5768,7 +5768,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public unsafe void Append(Span name, Span value) + public unsafe void Append(ReadOnlySpan name, ReadOnlySpan value) { ref byte nameStart = ref MemoryMarshal.GetReference(name); ref StringValues values = ref Unsafe.AsRef(null); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index ce63ec989f..db62037f74 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -4,13 +4,14 @@ using System; using System.Buffers; using System.Diagnostics; +using System.Net.Http; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { - public class HttpParser : IHttpParser where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler + internal class HttpParser : IHttpParser where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler { private readonly bool _showErrorDetails; @@ -212,7 +213,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } // Double CRLF found, so end of headers. - handler.OnHeadersComplete(); + handler.OnHeadersComplete(endStream: false); return true; } else if (readAhead == 1) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 7f10c3af64..9e181fd7cf 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -492,7 +492,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - public void OnHeader(Span name, Span value) + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { _requestHeadersParsed++; if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) @@ -503,7 +503,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http HttpRequestHeaders.Append(name, value); } - public void OnTrailer(Span name, Span value) + public void OnTrailer(ReadOnlySpan name, ReadOnlySpan value) { // Trailers still count towards the limit. _requestHeadersParsed++; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index 32e9e41d84..66ae0aad54 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } [MethodImpl(MethodImplOptions.NoInlining)] - private void AppendContentLength(Span value) + private void AppendContentLength(ReadOnlySpan value) { if (_contentLength.HasValue) { @@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } [MethodImpl(MethodImplOptions.NoInlining)] - private unsafe void AppendUnknownHeaders(Span name, string valueString) + private unsafe void AppendUnknownHeaders(ReadOnlySpan name, string valueString) { string key = name.GetHeaderName(); Unknown.TryGetValue(key, out var existing); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpHeadersHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpHeadersHandler.cs deleted file mode 100644 index 0ed16148b8..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpHeadersHandler.cs +++ /dev/null @@ -1,13 +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.Http -{ - public interface IHttpHeadersHandler - { - void OnHeader(Span name, Span value); - void OnHeadersComplete(); - } -} \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpParser.cs index 18837ccd0a..20688fe291 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpParser.cs @@ -3,10 +3,11 @@ using System; using System.Buffers; +using System.Net.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { - public interface IHttpParser where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler + internal interface IHttpParser where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler { bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence buffer, out SequencePosition consumed, out SequencePosition examined); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackDecoder.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackDecoder.cs deleted file mode 100644 index 543ce8afc1..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackDecoder.cs +++ /dev/null @@ -1,433 +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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack -{ - internal class HPackDecoder - { - private enum State - { - Ready, - HeaderFieldIndex, - HeaderNameIndex, - HeaderNameLength, - HeaderNameLengthContinue, - HeaderName, - HeaderValueLength, - HeaderValueLengthContinue, - HeaderValue, - DynamicTableSizeUpdate - } - - // http://httpwg.org/specs/rfc7541.html#rfc.section.6.1 - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 1 | Index (7+) | - // +---+---------------------------+ - private const byte IndexedHeaderFieldMask = 0x80; - private const byte IndexedHeaderFieldRepresentation = 0x80; - - // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1 - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 1 | Index (6+) | - // +---+---+-----------------------+ - private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0; - private const byte LiteralHeaderFieldWithIncrementalIndexingRepresentation = 0x40; - - // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2 - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 0 | 0 | 0 | Index (4+) | - // +---+---+-----------------------+ - private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0; - private const byte LiteralHeaderFieldWithoutIndexingRepresentation = 0x00; - - // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3 - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 0 | 0 | 1 | Index (4+) | - // +---+---+-----------------------+ - private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0; - private const byte LiteralHeaderFieldNeverIndexedRepresentation = 0x10; - - // http://httpwg.org/specs/rfc7541.html#rfc.section.6.3 - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | 0 | 0 | 1 | Max size (5+) | - // +---+---------------------------+ - private const byte DynamicTableSizeUpdateMask = 0xe0; - private const byte DynamicTableSizeUpdateRepresentation = 0x20; - - // http://httpwg.org/specs/rfc7541.html#rfc.section.5.2 - // 0 1 2 3 4 5 6 7 - // +---+---+---+---+---+---+---+---+ - // | H | String Length (7+) | - // +---+---------------------------+ - private const byte HuffmanMask = 0x80; - - private const int IndexedHeaderFieldPrefix = 7; - private const int LiteralHeaderFieldWithIncrementalIndexingPrefix = 6; - private const int LiteralHeaderFieldWithoutIndexingPrefix = 4; - private const int LiteralHeaderFieldNeverIndexedPrefix = 4; - private const int DynamicTableSizeUpdatePrefix = 5; - private const int StringLengthPrefix = 7; - - private readonly int _maxDynamicTableSize; - private readonly DynamicTable _dynamicTable; - private readonly IntegerDecoder _integerDecoder = new IntegerDecoder(); - private readonly byte[] _stringOctets; - private readonly byte[] _headerNameOctets; - private readonly byte[] _headerValueOctets; - - private State _state = State.Ready; - private byte[] _headerName; - private int _stringIndex; - private int _stringLength; - private int _headerNameLength; - private int _headerValueLength; - private bool _index; - private bool _huffman; - private bool _headersObserved; - - public HPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize) - : this(maxDynamicTableSize, maxRequestHeaderFieldSize, new DynamicTable(maxDynamicTableSize)) { } - - // For testing. - internal HPackDecoder(int maxDynamicTableSize, int maxRequestHeaderFieldSize, DynamicTable dynamicTable) - { - _maxDynamicTableSize = maxDynamicTableSize; - _dynamicTable = dynamicTable; - - _stringOctets = new byte[maxRequestHeaderFieldSize]; - _headerNameOctets = new byte[maxRequestHeaderFieldSize]; - _headerValueOctets = new byte[maxRequestHeaderFieldSize]; - } - - public void Decode(in ReadOnlySequence data, bool endHeaders, IHttpHeadersHandler handler) - { - foreach (var segment in data) - { - var span = segment.Span; - for (var i = 0; i < span.Length; i++) - { - OnByte(span[i], handler); - } - } - - if (endHeaders) - { - if (_state != State.Ready) - { - throw new HPackDecodingException(CoreStrings.HPackErrorIncompleteHeaderBlock); - } - - _headersObserved = false; - } - } - - private void OnByte(byte b, IHttpHeadersHandler handler) - { - int intResult; - switch (_state) - { - case State.Ready: - if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation) - { - _headersObserved = true; - var val = b & ~IndexedHeaderFieldMask; - - if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out intResult)) - { - OnIndexedHeaderField(intResult, handler); - } - else - { - _state = State.HeaderFieldIndex; - } - } - else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation) - { - _headersObserved = true; - _index = true; - var val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask; - - if (val == 0) - { - _state = State.HeaderNameLength; - } - else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix, out intResult)) - { - OnIndexedHeaderName(intResult); - } - else - { - _state = State.HeaderNameIndex; - } - } - else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation) - { - _headersObserved = true; - _index = false; - var val = b & ~LiteralHeaderFieldWithoutIndexingMask; - - if (val == 0) - { - _state = State.HeaderNameLength; - } - else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix, out intResult)) - { - OnIndexedHeaderName(intResult); - } - else - { - _state = State.HeaderNameIndex; - } - } - else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation) - { - _headersObserved = true; - _index = false; - var val = b & ~LiteralHeaderFieldNeverIndexedMask; - - if (val == 0) - { - _state = State.HeaderNameLength; - } - else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix, out intResult)) - { - OnIndexedHeaderName(intResult); - } - else - { - _state = State.HeaderNameIndex; - } - } - else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation) - { - // https://tools.ietf.org/html/rfc7541#section-4.2 - // This dynamic table size - // update MUST occur at the beginning of the first header block - // following the change to the dynamic table size. - if (_headersObserved) - { - throw new HPackDecodingException(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock); - } - - if (_integerDecoder.BeginTryDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix, out intResult)) - { - SetDynamicHeaderTableSize(intResult); - } - else - { - _state = State.DynamicTableSizeUpdate; - } - } - else - { - // Can't happen - throw new HPackDecodingException($"Byte value {b} does not encode a valid header field representation."); - } - - break; - case State.HeaderFieldIndex: - if (_integerDecoder.TryDecode(b, out intResult)) - { - OnIndexedHeaderField(intResult, handler); - } - - break; - case State.HeaderNameIndex: - if (_integerDecoder.TryDecode(b, out intResult)) - { - OnIndexedHeaderName(intResult); - } - - break; - case State.HeaderNameLength: - _huffman = (b & HuffmanMask) != 0; - - if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) - { - OnStringLength(intResult, nextState: State.HeaderName); - } - else - { - _state = State.HeaderNameLengthContinue; - } - - break; - case State.HeaderNameLengthContinue: - if (_integerDecoder.TryDecode(b, out intResult)) - { - OnStringLength(intResult, nextState: State.HeaderName); - } - - break; - case State.HeaderName: - _stringOctets[_stringIndex++] = b; - - if (_stringIndex == _stringLength) - { - OnString(nextState: State.HeaderValueLength); - } - - break; - case State.HeaderValueLength: - _huffman = (b & HuffmanMask) != 0; - - if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) - { - OnStringLength(intResult, nextState: State.HeaderValue); - if (intResult == 0) - { - ProcessHeaderValue(handler); - } - } - else - { - _state = State.HeaderValueLengthContinue; - } - - break; - case State.HeaderValueLengthContinue: - if (_integerDecoder.TryDecode(b, out intResult)) - { - OnStringLength(intResult, nextState: State.HeaderValue); - if (intResult == 0) - { - ProcessHeaderValue(handler); - } - } - - break; - case State.HeaderValue: - _stringOctets[_stringIndex++] = b; - - if (_stringIndex == _stringLength) - { - ProcessHeaderValue(handler); - } - - break; - case State.DynamicTableSizeUpdate: - if (_integerDecoder.TryDecode(b, out intResult)) - { - SetDynamicHeaderTableSize(intResult); - _state = State.Ready; - } - - break; - default: - // Can't happen - throw new HPackDecodingException("The HPACK decoder reached an invalid state."); - } - } - - private void ProcessHeaderValue(IHttpHeadersHandler handler) - { - OnString(nextState: State.Ready); - - var headerNameSpan = new Span(_headerName, 0, _headerNameLength); - var headerValueSpan = new Span(_headerValueOctets, 0, _headerValueLength); - - handler.OnHeader(headerNameSpan, headerValueSpan); - - if (_index) - { - _dynamicTable.Insert(headerNameSpan, headerValueSpan); - } - } - - private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler) - { - var header = GetHeader(index); - handler.OnHeader(new Span(header.Name), new Span(header.Value)); - _state = State.Ready; - } - - private void OnIndexedHeaderName(int index) - { - var header = GetHeader(index); - _headerName = header.Name; - _headerNameLength = header.Name.Length; - _state = State.HeaderValueLength; - } - - private void OnStringLength(int length, State nextState) - { - if (length > _stringOctets.Length) - { - throw new HPackDecodingException(CoreStrings.FormatHPackStringLengthTooLarge(length, _stringOctets.Length)); - } - - _stringLength = length; - _stringIndex = 0; - _state = nextState; - } - - private void OnString(State nextState) - { - int Decode(byte[] dst) - { - if (_huffman) - { - return Huffman.Decode(new ReadOnlySpan(_stringOctets, 0, _stringLength), dst); - } - else - { - Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength); - return _stringLength; - } - } - - try - { - if (_state == State.HeaderName) - { - _headerName = _headerNameOctets; - _headerNameLength = Decode(_headerNameOctets); - } - else - { - _headerValueLength = Decode(_headerValueOctets); - } - } - catch (HuffmanDecodingException ex) - { - throw new HPackDecodingException(CoreStrings.HPackHuffmanError, ex); - } - - _state = nextState; - } - - private HeaderField GetHeader(int index) - { - try - { - return index <= StaticTable.Instance.Count - ? StaticTable.Instance[index - 1] - : _dynamicTable[index - StaticTable.Instance.Count - 1]; - } - catch (IndexOutOfRangeException ex) - { - throw new HPackDecodingException(CoreStrings.FormatHPackErrorIndexOutOfRange(index), ex); - } - } - - private void SetDynamicHeaderTableSize(int size) - { - if (size > _maxDynamicTableSize) - { - throw new HPackDecodingException( - CoreStrings.FormatHPackErrorDynamicTableSizeUpdateTooLarge(size, _maxDynamicTableSize)); - } - - _dynamicTable.Resize(size); - } - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackDecodingException.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackDecodingException.cs deleted file mode 100644 index d549554ab6..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackDecodingException.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.Http2.HPack -{ - internal sealed class HPackDecodingException : Exception - { - public HPackDecodingException(string message) - : base(message) - { - } - public HPackDecodingException(string message, Exception innerException) - : base(message, innerException) - { - } - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackEncoder.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackEncoder.cs deleted file mode 100644 index 9268061b28..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackEncoder.cs +++ /dev/null @@ -1,160 +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; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack -{ - internal class HPackEncoder - { - private IEnumerator> _enumerator; - - public bool BeginEncode(IEnumerable> headers, Span buffer, out int length) - { - _enumerator = headers.GetEnumerator(); - _enumerator.MoveNext(); - - return Encode(buffer, out length); - } - - public bool BeginEncode(int statusCode, IEnumerable> headers, Span buffer, out int length) - { - _enumerator = headers.GetEnumerator(); - _enumerator.MoveNext(); - - var statusCodeLength = EncodeStatusCode(statusCode, buffer); - var done = Encode(buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out var headersLength); - length = statusCodeLength + headersLength; - - 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 HPackEncodingException(CoreStrings.HPackErrorNotEnoughBuffer); - } - return false; - } - - length += headerLength; - } while (_enumerator.MoveNext()); - - 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: - buffer[0] = (byte)(0x80 | 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; - ((Span)statusBytes).CopyTo(buffer.Slice(2)); - - return 2 + statusBytes.Length; - } - } - - private bool EncodeHeader(string name, string value, Span buffer, out int length) - { - var i = 0; - length = 0; - - if (buffer.Length == 0) - { - return false; - } - - buffer[i++] = 0; - - if (i == buffer.Length) - { - return false; - } - - if (!EncodeString(name, buffer.Slice(i), out var nameLength, lowercase: true)) - { - return false; - } - - i += nameLength; - - if (i >= buffer.Length) - { - return false; - } - - if (!EncodeString(value, buffer.Slice(i), out var valueLength, lowercase: false)) - { - return false; - } - - i += valueLength; - - length = i; - return true; - } - - private bool EncodeString(string s, Span buffer, out int length, bool lowercase) - { - const int toLowerMask = 0x20; - - var i = 0; - length = 0; - - if (buffer.Length == 0) - { - 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; - } - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HeaderField.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HeaderField.cs deleted file mode 100644 index d6a07e7b35..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HeaderField.cs +++ /dev/null @@ -1,30 +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.Http2.HPack -{ - internal readonly struct HeaderField - { - // http://httpwg.org/specs/rfc7541.html#rfc.section.4.1 - public const int RfcOverhead = 32; - - 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 + 32; - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HuffmanDecodingException.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HuffmanDecodingException.cs deleted file mode 100644 index f20769f0b6..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HuffmanDecodingException.cs +++ /dev/null @@ -1,15 +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.Http2.HPack -{ - internal sealed class HuffmanDecodingException : Exception - { - public HuffmanDecodingException(string message) - : base(message) - { - } - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/IntegerDecoder.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/IntegerDecoder.cs deleted file mode 100644 index 081fd30f6c..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/IntegerDecoder.cs +++ /dev/null @@ -1,67 +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.Http2.HPack -{ - /// - /// The maximum we will decode is Int32.MaxValue, which is also the maximum request header field size. - /// - internal class IntegerDecoder - { - private int _i; - private int _m; - - /// - /// Callers must ensure higher bits above the prefix are cleared before calling this method. - /// - /// - /// - /// - /// - public bool BeginTryDecode(byte b, int prefixLength, out int result) - { - if (b < ((1 << prefixLength) - 1)) - { - result = b; - return true; - } - - _i = b; - _m = 0; - result = 0; - return false; - } - - public bool TryDecode(byte b, out int result) - { - var m = _m; // Enregister - var i = _i + ((b & 0x7f) << m); // Enregister - - if ((b & 0x80) == 0) - { - // Int32.MaxValue only needs a maximum of 5 bytes to represent and the last byte cannot have any value set larger than 0x7 - if ((m > 21 && b > 0x7) || i < 0) - { - ThrowIntegerTooBigException(); - } - - result = i; - return true; - } - else if (m > 21) - { - // Int32.MaxValue only needs a maximum of 5 bytes to represent - ThrowIntegerTooBigException(); - } - - _m = m + 7; - _i = i; - - result = 0; - return false; - } - - public static void ThrowIntegerTooBigException() - => throw new HPackDecodingException(CoreStrings.HPackErrorIntegerTooBig); - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/IntegerEncoder.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/IntegerEncoder.cs deleted file mode 100644 index 600d032176..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/IntegerEncoder.cs +++ /dev/null @@ -1,64 +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.Diagnostics; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack -{ - internal static class IntegerEncoder - { - public static bool Encode(int i, int n, Span buffer, out int length) - { - Debug.Assert(i >= 0); - Debug.Assert(n >= 1 && n <= 8); - - var j = 0; - length = 0; - - if (buffer.Length == 0) - { - return false; - } - - if (i < (1 << n) - 1) - { - buffer[j] &= MaskHigh(8 - n); - buffer[j++] |= (byte)i; - } - else - { - buffer[j] &= MaskHigh(8 - n); - buffer[j++] |= (byte)((1 << n) - 1); - - if (j == buffer.Length) - { - return false; - } - - i -= ((1 << n) - 1); - while (i >= 128) - { - var ui = (uint)i; // Use unsigned for optimizations - buffer[j++] = (byte)((ui % 128) + 128); - - if (j >= buffer.Length) - { - return false; - } - - i = (int)(ui / 128); // Jit converts unsigned divide by power-of-2 constant to clean shift - } - buffer[j++] = (byte)i; - } - - length = j; - return true; - } - - private static byte MaskHigh(int n) - { - return (byte)(sbyte.MinValue >> (n - 1)); - } - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StaticTable.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StaticTable.cs deleted file mode 100644 index 5c0ece5c9f..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StaticTable.cs +++ /dev/null @@ -1,105 +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.Collections.Generic; -using System.Text; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack -{ - internal class StaticTable - { - private static readonly StaticTable _instance = new StaticTable(); - - private readonly Dictionary _statusIndex = new Dictionary - { - [200] = 8, - [204] = 9, - [206] = 10, - [304] = 11, - [400] = 12, - [404] = 13, - [500] = 14, - }; - - private StaticTable() - { - } - - public static StaticTable Instance => _instance; - - public int Count => _staticTable.Length; - - public HeaderField this[int index] => _staticTable[index]; - - public IReadOnlyDictionary StatusIndex => _statusIndex; - - private readonly HeaderField[] _staticTable = new HeaderField[] - { - CreateHeaderField(HeaderNames.Authority, ""), - CreateHeaderField(HeaderNames.Method, "GET"), - CreateHeaderField(HeaderNames.Method, "POST"), - CreateHeaderField(HeaderNames.Path, "/"), - CreateHeaderField(HeaderNames.Path, "/index.html"), - CreateHeaderField(HeaderNames.Scheme, "http"), - CreateHeaderField(HeaderNames.Scheme, "https"), - CreateHeaderField(HeaderNames.Status, "200"), - CreateHeaderField(HeaderNames.Status, "204"), - CreateHeaderField(HeaderNames.Status, "206"), - CreateHeaderField(HeaderNames.Status, "304"), - CreateHeaderField(HeaderNames.Status, "400"), - CreateHeaderField(HeaderNames.Status, "404"), - CreateHeaderField(HeaderNames.Status, "500"), - CreateHeaderField("accept-charset", ""), - CreateHeaderField("accept-encoding", "gzip, deflate"), - CreateHeaderField("accept-language", ""), - CreateHeaderField("accept-ranges", ""), - CreateHeaderField("accept", ""), - CreateHeaderField("access-control-allow-origin", ""), - CreateHeaderField("age", ""), - CreateHeaderField("allow", ""), - CreateHeaderField("authorization", ""), - CreateHeaderField("cache-control", ""), - CreateHeaderField("content-disposition", ""), - CreateHeaderField("content-encoding", ""), - CreateHeaderField("content-language", ""), - CreateHeaderField("content-length", ""), - CreateHeaderField("content-location", ""), - CreateHeaderField("content-range", ""), - CreateHeaderField("content-type", ""), - CreateHeaderField("cookie", ""), - CreateHeaderField("date", ""), - CreateHeaderField("etag", ""), - CreateHeaderField("expect", ""), - CreateHeaderField("expires", ""), - CreateHeaderField("from", ""), - CreateHeaderField("host", ""), - CreateHeaderField("if-match", ""), - CreateHeaderField("if-modified-since", ""), - CreateHeaderField("if-none-match", ""), - CreateHeaderField("if-range", ""), - CreateHeaderField("if-unmodifiedsince", ""), - CreateHeaderField("last-modified", ""), - CreateHeaderField("link", ""), - CreateHeaderField("location", ""), - CreateHeaderField("max-forwards", ""), - CreateHeaderField("proxy-authenticate", ""), - CreateHeaderField("proxy-authorization", ""), - CreateHeaderField("range", ""), - CreateHeaderField("referer", ""), - CreateHeaderField("refresh", ""), - CreateHeaderField("retry-after", ""), - CreateHeaderField("server", ""), - CreateHeaderField("set-cookie", ""), - CreateHeaderField("strict-transport-security", ""), - CreateHeaderField("transfer-encoding", ""), - CreateHeaderField("user-agent", ""), - CreateHeaderField("vary", ""), - CreateHeaderField("via", ""), - CreateHeaderField("www-authenticate", "") - }; - - private static HeaderField CreateHeaderField(string name, string value) - => new HeaderField(Encoding.ASCII.GetBytes(name), Encoding.ASCII.GetBytes(value)); - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs deleted file mode 100644 index e00afa1d28..0000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs +++ /dev/null @@ -1,158 +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.Globalization; -using System.Text; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack -{ - internal static class StatusCodes - { - private static readonly byte[] _bytesStatus100 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status100Continue); - private static readonly byte[] _bytesStatus101 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status101SwitchingProtocols); - private static readonly byte[] _bytesStatus102 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status102Processing); - - private static readonly byte[] _bytesStatus200 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status200OK); - private static readonly byte[] _bytesStatus201 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status201Created); - private static readonly byte[] _bytesStatus202 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status202Accepted); - private static readonly byte[] _bytesStatus203 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status203NonAuthoritative); - private static readonly byte[] _bytesStatus204 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status204NoContent); - private static readonly byte[] _bytesStatus205 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status205ResetContent); - private static readonly byte[] _bytesStatus206 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status206PartialContent); - private static readonly byte[] _bytesStatus207 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status207MultiStatus); - private static readonly byte[] _bytesStatus208 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status208AlreadyReported); - private static readonly byte[] _bytesStatus226 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status226IMUsed); - - private static readonly byte[] _bytesStatus300 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status300MultipleChoices); - private static readonly byte[] _bytesStatus301 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status301MovedPermanently); - private static readonly byte[] _bytesStatus302 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status302Found); - private static readonly byte[] _bytesStatus303 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status303SeeOther); - private static readonly byte[] _bytesStatus304 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status304NotModified); - private static readonly byte[] _bytesStatus305 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status305UseProxy); - private static readonly byte[] _bytesStatus306 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status306SwitchProxy); - private static readonly byte[] _bytesStatus307 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status307TemporaryRedirect); - private static readonly byte[] _bytesStatus308 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status308PermanentRedirect); - - private static readonly byte[] _bytesStatus400 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest); - private static readonly byte[] _bytesStatus401 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status401Unauthorized); - private static readonly byte[] _bytesStatus402 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status402PaymentRequired); - private static readonly byte[] _bytesStatus403 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status403Forbidden); - private static readonly byte[] _bytesStatus404 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status404NotFound); - private static readonly byte[] _bytesStatus405 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status405MethodNotAllowed); - private static readonly byte[] _bytesStatus406 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status406NotAcceptable); - private static readonly byte[] _bytesStatus407 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status407ProxyAuthenticationRequired); - private static readonly byte[] _bytesStatus408 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status408RequestTimeout); - private static readonly byte[] _bytesStatus409 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status409Conflict); - private static readonly byte[] _bytesStatus410 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status410Gone); - private static readonly byte[] _bytesStatus411 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status411LengthRequired); - private static readonly byte[] _bytesStatus412 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status412PreconditionFailed); - private static readonly byte[] _bytesStatus413 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status413PayloadTooLarge); - private static readonly byte[] _bytesStatus414 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status414UriTooLong); - private static readonly byte[] _bytesStatus415 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status415UnsupportedMediaType); - private static readonly byte[] _bytesStatus416 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status416RangeNotSatisfiable); - private static readonly byte[] _bytesStatus417 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status417ExpectationFailed); - private static readonly byte[] _bytesStatus418 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status418ImATeapot); - private static readonly byte[] _bytesStatus419 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status419AuthenticationTimeout); - private static readonly byte[] _bytesStatus421 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status421MisdirectedRequest); - private static readonly byte[] _bytesStatus422 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status422UnprocessableEntity); - private static readonly byte[] _bytesStatus423 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status423Locked); - private static readonly byte[] _bytesStatus424 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status424FailedDependency); - private static readonly byte[] _bytesStatus426 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status426UpgradeRequired); - private static readonly byte[] _bytesStatus428 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status428PreconditionRequired); - private static readonly byte[] _bytesStatus429 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status429TooManyRequests); - private static readonly byte[] _bytesStatus431 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status431RequestHeaderFieldsTooLarge); - private static readonly byte[] _bytesStatus451 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status451UnavailableForLegalReasons); - - private static readonly byte[] _bytesStatus500 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError); - private static readonly byte[] _bytesStatus501 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status501NotImplemented); - private static readonly byte[] _bytesStatus502 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status502BadGateway); - private static readonly byte[] _bytesStatus503 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status503ServiceUnavailable); - private static readonly byte[] _bytesStatus504 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status504GatewayTimeout); - private static readonly byte[] _bytesStatus505 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status505HttpVersionNotsupported); - private static readonly byte[] _bytesStatus506 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status506VariantAlsoNegotiates); - private static readonly byte[] _bytesStatus507 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status507InsufficientStorage); - private static readonly byte[] _bytesStatus508 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status508LoopDetected); - private static readonly byte[] _bytesStatus510 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status510NotExtended); - private static readonly byte[] _bytesStatus511 = CreateStatusBytes(Microsoft.AspNetCore.Http.StatusCodes.Status511NetworkAuthenticationRequired); - - private static byte[] CreateStatusBytes(int statusCode) - { - return Encoding.ASCII.GetBytes(statusCode.ToString(CultureInfo.InvariantCulture)); - } - - public static byte[] ToStatusBytes(int statusCode) - { - return statusCode switch - { - Microsoft.AspNetCore.Http.StatusCodes.Status100Continue => _bytesStatus100, - Microsoft.AspNetCore.Http.StatusCodes.Status101SwitchingProtocols => _bytesStatus101, - Microsoft.AspNetCore.Http.StatusCodes.Status102Processing => _bytesStatus102, - - Microsoft.AspNetCore.Http.StatusCodes.Status200OK => _bytesStatus200, - Microsoft.AspNetCore.Http.StatusCodes.Status201Created => _bytesStatus201, - Microsoft.AspNetCore.Http.StatusCodes.Status202Accepted => _bytesStatus202, - Microsoft.AspNetCore.Http.StatusCodes.Status203NonAuthoritative => _bytesStatus203, - Microsoft.AspNetCore.Http.StatusCodes.Status204NoContent => _bytesStatus204, - Microsoft.AspNetCore.Http.StatusCodes.Status205ResetContent => _bytesStatus205, - Microsoft.AspNetCore.Http.StatusCodes.Status206PartialContent => _bytesStatus206, - Microsoft.AspNetCore.Http.StatusCodes.Status207MultiStatus => _bytesStatus207, - Microsoft.AspNetCore.Http.StatusCodes.Status208AlreadyReported => _bytesStatus208, - Microsoft.AspNetCore.Http.StatusCodes.Status226IMUsed => _bytesStatus226, - - Microsoft.AspNetCore.Http.StatusCodes.Status300MultipleChoices => _bytesStatus300, - Microsoft.AspNetCore.Http.StatusCodes.Status301MovedPermanently => _bytesStatus301, - Microsoft.AspNetCore.Http.StatusCodes.Status302Found => _bytesStatus302, - Microsoft.AspNetCore.Http.StatusCodes.Status303SeeOther => _bytesStatus303, - Microsoft.AspNetCore.Http.StatusCodes.Status304NotModified => _bytesStatus304, - Microsoft.AspNetCore.Http.StatusCodes.Status305UseProxy => _bytesStatus305, - Microsoft.AspNetCore.Http.StatusCodes.Status306SwitchProxy => _bytesStatus306, - Microsoft.AspNetCore.Http.StatusCodes.Status307TemporaryRedirect => _bytesStatus307, - Microsoft.AspNetCore.Http.StatusCodes.Status308PermanentRedirect => _bytesStatus308, - - Microsoft.AspNetCore.Http.StatusCodes.Status400BadRequest => _bytesStatus400, - Microsoft.AspNetCore.Http.StatusCodes.Status401Unauthorized => _bytesStatus401, - Microsoft.AspNetCore.Http.StatusCodes.Status402PaymentRequired => _bytesStatus402, - Microsoft.AspNetCore.Http.StatusCodes.Status403Forbidden => _bytesStatus403, - Microsoft.AspNetCore.Http.StatusCodes.Status404NotFound => _bytesStatus404, - Microsoft.AspNetCore.Http.StatusCodes.Status405MethodNotAllowed => _bytesStatus405, - Microsoft.AspNetCore.Http.StatusCodes.Status406NotAcceptable => _bytesStatus406, - Microsoft.AspNetCore.Http.StatusCodes.Status407ProxyAuthenticationRequired => _bytesStatus407, - Microsoft.AspNetCore.Http.StatusCodes.Status408RequestTimeout => _bytesStatus408, - Microsoft.AspNetCore.Http.StatusCodes.Status409Conflict => _bytesStatus409, - Microsoft.AspNetCore.Http.StatusCodes.Status410Gone => _bytesStatus410, - Microsoft.AspNetCore.Http.StatusCodes.Status411LengthRequired => _bytesStatus411, - Microsoft.AspNetCore.Http.StatusCodes.Status412PreconditionFailed => _bytesStatus412, - Microsoft.AspNetCore.Http.StatusCodes.Status413PayloadTooLarge => _bytesStatus413, - Microsoft.AspNetCore.Http.StatusCodes.Status414UriTooLong => _bytesStatus414, - Microsoft.AspNetCore.Http.StatusCodes.Status415UnsupportedMediaType => _bytesStatus415, - Microsoft.AspNetCore.Http.StatusCodes.Status416RangeNotSatisfiable => _bytesStatus416, - Microsoft.AspNetCore.Http.StatusCodes.Status417ExpectationFailed => _bytesStatus417, - Microsoft.AspNetCore.Http.StatusCodes.Status418ImATeapot => _bytesStatus418, - Microsoft.AspNetCore.Http.StatusCodes.Status419AuthenticationTimeout => _bytesStatus419, - Microsoft.AspNetCore.Http.StatusCodes.Status421MisdirectedRequest => _bytesStatus421, - Microsoft.AspNetCore.Http.StatusCodes.Status422UnprocessableEntity => _bytesStatus422, - Microsoft.AspNetCore.Http.StatusCodes.Status423Locked => _bytesStatus423, - Microsoft.AspNetCore.Http.StatusCodes.Status424FailedDependency => _bytesStatus424, - Microsoft.AspNetCore.Http.StatusCodes.Status426UpgradeRequired => _bytesStatus426, - Microsoft.AspNetCore.Http.StatusCodes.Status428PreconditionRequired => _bytesStatus428, - Microsoft.AspNetCore.Http.StatusCodes.Status429TooManyRequests => _bytesStatus429, - Microsoft.AspNetCore.Http.StatusCodes.Status431RequestHeaderFieldsTooLarge => _bytesStatus431, - Microsoft.AspNetCore.Http.StatusCodes.Status451UnavailableForLegalReasons => _bytesStatus451, - - Microsoft.AspNetCore.Http.StatusCodes.Status500InternalServerError => _bytesStatus500, - Microsoft.AspNetCore.Http.StatusCodes.Status501NotImplemented => _bytesStatus501, - Microsoft.AspNetCore.Http.StatusCodes.Status502BadGateway => _bytesStatus502, - Microsoft.AspNetCore.Http.StatusCodes.Status503ServiceUnavailable => _bytesStatus503, - Microsoft.AspNetCore.Http.StatusCodes.Status504GatewayTimeout => _bytesStatus504, - Microsoft.AspNetCore.Http.StatusCodes.Status505HttpVersionNotsupported => _bytesStatus505, - Microsoft.AspNetCore.Http.StatusCodes.Status506VariantAlsoNegotiates => _bytesStatus506, - Microsoft.AspNetCore.Http.StatusCodes.Status507InsufficientStorage => _bytesStatus507, - Microsoft.AspNetCore.Http.StatusCodes.Status508LoopDetected => _bytesStatus508, - Microsoft.AspNetCore.Http.StatusCodes.Status510NotExtended => _bytesStatus510, - Microsoft.AspNetCore.Http.StatusCodes.Status511NetworkAuthenticationRequired => _bytesStatus511, - - _ => CreateStatusBytes(statusCode) - }; - } - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 10756c0e80..52349b04ca 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -8,6 +8,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Pipelines; +using System.Net.Http; +using System.Net.Http.HPack; using System.Runtime.CompilerServices; using System.Security.Authentication; using System.Text; @@ -19,7 +21,6 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -1080,7 +1081,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // We can't throw a Http2StreamErrorException here, it interrupts the header decompression state and may corrupt subsequent header frames on other streams. // For now these either need to be connection errors or BadRequests. If we want to downgrade any of them to stream errors later then we need to // rework the flow so that the remaining headers are drained and the decompression state is maintained. - public void OnHeader(Span name, Span value) + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { // https://tools.ietf.org/html/rfc7540#section-6.5.2 // "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field."; @@ -1114,10 +1115,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public void OnHeadersComplete() + public void OnHeadersComplete(bool endStream) => _currentHeadersStream.OnHeadersComplete(); - private void ValidateHeader(Span name, Span value) + private void ValidateHeader(ReadOnlySpan name, ReadOnlySpan value) { // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1 /* @@ -1207,7 +1208,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - private bool IsPseudoHeaderField(Span name, out PseudoHeaderFields headerField) + private bool IsPseudoHeaderField(ReadOnlySpan name, out PseudoHeaderFields headerField) { headerField = PseudoHeaderFields.None; @@ -1244,7 +1245,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 return true; } - private static bool IsConnectionSpecificHeaderField(Span name, Span value) + private static bool IsConnectionSpecificHeaderField(ReadOnlySpan name, ReadOnlySpan value) { return name.SequenceEqual(_connectionBytes) || (name.SequenceEqual(_teBytes) && !value.SequenceEqual(_trailersBytes)); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 1177504aa6..244bc2663c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -7,13 +7,13 @@ using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.IO.Pipelines; +using System.Net.Http.HPack; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeWriterHelpers; diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs index f57c296e1e..04069db3e8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/HttpUtilities.cs @@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } // The same as GetAsciiStringNonNullCharacters but throws BadRequest - public static unsafe string GetHeaderName(this Span span) + public static unsafe string GetHeaderName(this ReadOnlySpan span) { if (span.IsEmpty) { @@ -108,7 +108,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure return asciiString; } - public static unsafe string GetAsciiStringNonNullCharacters(this Span span) + public static string GetAsciiStringNonNullCharacters(this Span span) + => GetAsciiStringNonNullCharacters((ReadOnlySpan)span); + + public static unsafe string GetAsciiStringNonNullCharacters(this ReadOnlySpan span) { if (span.IsEmpty) { @@ -131,6 +134,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this Span span) + => GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan)span); + + public static unsafe string GetAsciiOrUTF8StringNonNullCharacters(this ReadOnlySpan span) { if (span.IsEmpty) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs index 1a5815300e..f19213a07d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IKestrelTrace.cs @@ -2,9 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net.Http.HPack; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs index d72aa3c707..4ac7cf8ecc 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.cs @@ -2,9 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net.Http.HPack; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/StringUtilities.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/StringUtilities.cs index aea8290b66..2a12b9893b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/StringUtilities.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/StringUtilities.cs @@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure } [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public unsafe static bool BytesOrdinalEqualsStringAndAscii(string previousValue, Span newValue) + public unsafe static bool BytesOrdinalEqualsStringAndAscii(string previousValue, ReadOnlySpan newValue) { // previousValue is a previously materialized string which *must* have already passed validation. Debug.Assert(IsValidHeaderString(previousValue)); 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 058df2190c..1c8ebd0712 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 @@ -1,4 +1,4 @@ - + Core components of ASP.NET Core Kestrel cross-platform web server. @@ -15,6 +15,7 @@ + @@ -34,6 +35,10 @@ + + System.Net.Http.SR + + diff --git a/src/Servers/Kestrel/Core/test/DynamicTableTests.cs b/src/Servers/Kestrel/Core/test/DynamicTableTests.cs deleted file mode 100644 index a7fb8520c6..0000000000 --- a/src/Servers/Kestrel/Core/test/DynamicTableTests.cs +++ /dev/null @@ -1,174 +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.Linq; -using System.Text; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests -{ - public class DynamicTableTests - { - private readonly HeaderField _header1 = new HeaderField(Encoding.ASCII.GetBytes("header-1"), Encoding.ASCII.GetBytes("value1")); - private readonly HeaderField _header2 = new HeaderField(Encoding.ASCII.GetBytes("header-02"), Encoding.ASCII.GetBytes("value_2")); - - [Fact] - public void DynamicTableIsInitiallyEmpty() - { - var dynamicTable = new DynamicTable(4096); - Assert.Equal(0, dynamicTable.Count); - Assert.Equal(0, dynamicTable.Size); - Assert.Equal(4096, dynamicTable.MaxSize); - } - - [Fact] - public void CountIsNumberOfEntriesInDynamicTable() - { - var dynamicTable = new DynamicTable(4096); - - dynamicTable.Insert(_header1.Name, _header1.Value); - Assert.Equal(1, dynamicTable.Count); - - dynamicTable.Insert(_header2.Name, _header2.Value); - Assert.Equal(2, dynamicTable.Count); - } - - [Fact] - public void SizeIsCurrentDynamicTableSize() - { - var dynamicTable = new DynamicTable(4096); - Assert.Equal(0, dynamicTable.Size); - - dynamicTable.Insert(_header1.Name, _header1.Value); - Assert.Equal(_header1.Length, dynamicTable.Size); - - dynamicTable.Insert(_header2.Name, _header2.Value); - Assert.Equal(_header1.Length + _header2.Length, dynamicTable.Size); - } - - [Fact] - public void FirstEntryIsMostRecentEntry() - { - var dynamicTable = new DynamicTable(4096); - dynamicTable.Insert(_header1.Name, _header1.Value); - dynamicTable.Insert(_header2.Name, _header2.Value); - - VerifyTableEntries(dynamicTable, _header2, _header1); - } - - [Fact] - public void WrapsAroundBuffer() - { - var header3 = new HeaderField(Encoding.ASCII.GetBytes("header-3"), Encoding.ASCII.GetBytes("value3")); - var header4 = new HeaderField(Encoding.ASCII.GetBytes("header-4"), Encoding.ASCII.GetBytes("value4")); - - // Make the table small enough that the circular buffer kicks in. - var dynamicTable = new DynamicTable(HeaderField.RfcOverhead * 3); - dynamicTable.Insert(header4.Name, header4.Value); - dynamicTable.Insert(header3.Name, header3.Value); - dynamicTable.Insert(_header2.Name, _header2.Value); - dynamicTable.Insert(_header1.Name, _header1.Value); - - VerifyTableEntries(dynamicTable, _header1, _header2); - } - - [Fact] - public void ThrowsIndexOutOfRangeException() - { - var dynamicTable = new DynamicTable(4096); - Assert.Throws(() => dynamicTable[0]); - - dynamicTable.Insert(_header1.Name, _header1.Value); - Assert.Throws(() => dynamicTable[1]); - } - - [Fact] - public void NoOpWhenInsertingEntryLargerThanMaxSize() - { - var dynamicTable = new DynamicTable(_header1.Length - 1); - dynamicTable.Insert(_header1.Name, _header1.Value); - - Assert.Equal(0, dynamicTable.Count); - Assert.Equal(0, dynamicTable.Size); - } - - [Fact] - public void NoOpWhenInsertingEntryLargerThanRemainingSpace() - { - var dynamicTable = new DynamicTable(_header1.Length); - dynamicTable.Insert(_header1.Name, _header1.Value); - - VerifyTableEntries(dynamicTable, _header1); - - dynamicTable.Insert(_header2.Name, _header2.Value); - - Assert.Equal(0, dynamicTable.Count); - Assert.Equal(0, dynamicTable.Size); - } - - [Fact] - public void ResizingEvictsOldestEntries() - { - var dynamicTable = new DynamicTable(4096); - dynamicTable.Insert(_header1.Name, _header1.Value); - dynamicTable.Insert(_header2.Name, _header2.Value); - - VerifyTableEntries(dynamicTable, _header2, _header1); - - dynamicTable.Resize(_header2.Length); - - VerifyTableEntries(dynamicTable, _header2); - } - - [Fact] - public void ResizingToZeroEvictsAllEntries() - { - var dynamicTable = new DynamicTable(4096); - dynamicTable.Insert(_header1.Name, _header1.Value); - dynamicTable.Insert(_header2.Name, _header2.Value); - - dynamicTable.Resize(0); - - Assert.Equal(0, dynamicTable.Count); - Assert.Equal(0, dynamicTable.Size); - } - - [Fact] - public void CanBeResizedToLargerMaxSize() - { - var dynamicTable = new DynamicTable(_header1.Length); - dynamicTable.Insert(_header1.Name, _header1.Value); - dynamicTable.Insert(_header2.Name, _header2.Value); - - // _header2 is larger than _header1, so an attempt at inserting it - // would first clear the table then return without actually inserting it, - // given it is larger than the current max size. - Assert.Equal(0, dynamicTable.Count); - Assert.Equal(0, dynamicTable.Size); - - dynamicTable.Resize(dynamicTable.MaxSize + _header2.Length); - dynamicTable.Insert(_header2.Name, _header2.Value); - - VerifyTableEntries(dynamicTable, _header2); - } - - private void VerifyTableEntries(DynamicTable dynamicTable, params HeaderField[] entries) - { - Assert.Equal(entries.Length, dynamicTable.Count); - Assert.Equal(entries.Sum(e => e.Length), dynamicTable.Size); - - for (var i = 0; i < entries.Length; i++) - { - var headerField = dynamicTable[i]; - - Assert.NotSame(entries[i].Name, headerField.Name); - Assert.Equal(entries[i].Name, headerField.Name); - - Assert.NotSame(entries[i].Value, headerField.Value); - Assert.Equal(entries[i].Value, headerField.Value); - } - } - } -} diff --git a/src/Servers/Kestrel/Core/test/HPackEncoderTests.cs b/src/Servers/Kestrel/Core/test/HPackEncoderTests.cs index 57ee0ba9a4..b41eb2a910 100644 --- a/src/Servers/Kestrel/Core/test/HPackEncoderTests.cs +++ b/src/Servers/Kestrel/Core/test/HPackEncoderTests.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; +using System.Net.Http.HPack; using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests diff --git a/src/Servers/Kestrel/Core/test/HPackIntegerTests.cs b/src/Servers/Kestrel/Core/test/HPackIntegerTests.cs deleted file mode 100644 index 4448463dd4..0000000000 --- a/src/Servers/Kestrel/Core/test/HPackIntegerTests.cs +++ /dev/null @@ -1,91 +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.Linq; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests -{ - public class HPackIntegerTests - { - [Fact] - public void IntegerEncoderDecoderRoundtrips() - { - var decoder = new IntegerDecoder(); - var range = 1 << 8; - - foreach (var i in Enumerable.Range(0, range).Concat(Enumerable.Range(int.MaxValue - range + 1, range))) - { - for (int n = 1; n <= 8; n++) - { - var integerBytes = new byte[6]; - Assert.True(IntegerEncoder.Encode(i, n, integerBytes, out var length)); - - var decodeResult = decoder.BeginTryDecode(integerBytes[0], n, out var intResult); - - for (int j = 1; j < length; j++) - { - Assert.False(decodeResult); - decodeResult = decoder.TryDecode(integerBytes[j], out intResult); - } - - Assert.True(decodeResult); - Assert.Equal(i, intResult); - } - } - } - - [Theory] - [MemberData(nameof(IntegerCodecSamples))] - public void EncodeSamples(int value, int bits, byte[] expectedResult) - { - Span actualResult = new byte[64]; - bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten); - - Assert.True(success); - Assert.Equal(expectedResult.Length, bytesWritten); - Assert.True(actualResult.Slice(0, bytesWritten).SequenceEqual(expectedResult)); - } - - [Theory] - [MemberData(nameof(IntegerCodecSamples))] - public void EncodeSamplesWithShortBuffer(int value, int bits, byte[] expectedResult) - { - Span actualResult = new byte[expectedResult.Length - 1]; - bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten); - - Assert.False(success); - } - - [Theory] - [MemberData(nameof(IntegerCodecSamples))] - public void DecodeSamples(int expectedResult, int bits, byte[] encoded) - { - var integerDecoder = new IntegerDecoder(); - - bool finished = integerDecoder.BeginTryDecode(encoded[0], bits, out int actualResult); - - int i = 1; - for (; !finished && i < encoded.Length; ++i) - { - finished = integerDecoder.TryDecode(encoded[i], out actualResult); - } - - Assert.True(finished); - Assert.Equal(encoded.Length, i); - - Assert.Equal(expectedResult, actualResult); - } - - // integer, prefix length, encoded - public static IEnumerable IntegerCodecSamples() - { - yield return new object[] { 10, 5, new byte[] { 0x0A } }; - yield return new object[] { 1337, 5, new byte[] { 0x1F, 0x9A, 0x0A } }; - yield return new object[] { 42, 8, new byte[] { 0x2A } }; - } - } -} diff --git a/src/Servers/Kestrel/Core/test/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs index 82d69d8b4d..ebd7bfc316 100644 --- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs @@ -5,6 +5,7 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; @@ -13,6 +14,7 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Moq; using Xunit; +using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { @@ -505,12 +507,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public Dictionary Headers { get; } = new Dictionary(); - public void OnHeader(Span name, Span value) + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { Headers[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters(); } - void IHttpHeadersHandler.OnHeadersComplete() { } + void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) { diff --git a/src/Servers/Kestrel/Core/test/IntegerDecoderTests.cs b/src/Servers/Kestrel/Core/test/IntegerDecoderTests.cs deleted file mode 100644 index fdebb56922..0000000000 --- a/src/Servers/Kestrel/Core/test/IntegerDecoderTests.cs +++ /dev/null @@ -1,86 +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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests -{ - public class IntegerDecoderTests - { - [Theory] - [MemberData(nameof(IntegerData))] - public void IntegerDecode(int i, int prefixLength, byte[] octets) - { - var decoder = new IntegerDecoder(); - var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult); - - if (octets.Length == 1) - { - Assert.True(result); - } - else - { - var j = 1; - - for (; j < octets.Length - 1; j++) - { - Assert.False(decoder.TryDecode(octets[j], out intResult)); - } - - Assert.True(decoder.TryDecode(octets[j], out intResult)); - } - - Assert.Equal(i, intResult); - } - - [Theory] - [MemberData(nameof(IntegerData_OverMax))] - public void IntegerDecode_Throws_IfMaxExceeded(int prefixLength, byte[] octets) - { - var decoder = new IntegerDecoder(); - var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult); - - for (var j = 1; j < octets.Length - 1; j++) - { - Assert.False(decoder.TryDecode(octets[j], out intResult)); - } - - Assert.Throws(() => decoder.TryDecode(octets[octets.Length - 1], out intResult)); - } - - public static TheoryData IntegerData - { - get - { - var data = new TheoryData(); - - data.Add(10, 5, new byte[] { 10 }); - data.Add(1337, 5, new byte[] { 0x1f, 0x9a, 0x0a }); - data.Add(42, 8, new byte[] { 42 }); - data.Add(7, 3, new byte[] { 0x7, 0x0 }); - data.Add(int.MaxValue, 1, new byte[] { 0x01, 0xfe, 0xff, 0xff, 0xff, 0x07 }); - data.Add(int.MaxValue, 8, new byte[] { 0xff, 0x80, 0xfe, 0xff, 0xff, 0x07 }); - - return data; - } - } - - public static TheoryData IntegerData_OverMax - { - get - { - var data = new TheoryData(); - - data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1 - data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x08 }); // MSB exceeds maximum - data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set - data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1 - data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x08 }); // MSB exceeds maximum - data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set - - return data; - } - } - } -} diff --git a/src/Servers/Kestrel/Core/test/IntegerEncoderTests.cs b/src/Servers/Kestrel/Core/test/IntegerEncoderTests.cs deleted file mode 100644 index c667cc6cee..0000000000 --- a/src/Servers/Kestrel/Core/test/IntegerEncoderTests.cs +++ /dev/null @@ -1,36 +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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests -{ - public class IntegerEncoderTests - { - [Theory] - [MemberData(nameof(IntegerData))] - public void IntegerEncode(int i, int prefixLength, byte[] expectedOctets) - { - var buffer = new byte[expectedOctets.Length]; - - Assert.True(IntegerEncoder.Encode(i, prefixLength, buffer, out var octets)); - Assert.Equal(expectedOctets.Length, octets); - Assert.Equal(expectedOctets, buffer); - } - - public static TheoryData IntegerData - { - get - { - var data = new TheoryData(); - - data.Add(10, 5, new byte[] { 10 }); - data.Add(1337, 5, new byte[] { 0x1f, 0x9a, 0x0a }); - data.Add(42, 8, new byte[] { 42 }); - - return data; - } - } - } -} diff --git a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj index 0497c91342..f64bf87991 100644 --- a/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj +++ b/src/Servers/Kestrel/Core/test/Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs index 887aad3939..c165c141ce 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http1ConnectionBenchmark.cs @@ -4,12 +4,14 @@ using System; using System.Buffers; using System.IO.Pipelines; +using System.Net.Http; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -104,10 +106,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance RequestHandler = requestHandler; } - public void OnHeader(Span name, Span value) + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) => RequestHandler.Connection.OnHeader(name, value); - public void OnHeadersComplete() + public void OnHeadersComplete(bool endStream) => RequestHandler.Connection.OnHeadersComplete(); public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod, bool pathEncoded) diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs index c5eb24baf8..2ed922386f 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HttpParserBenchmark.cs @@ -3,8 +3,10 @@ using System; using System.Buffers; +using System.Net.Http; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -69,11 +71,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { } - public void OnHeader(Span name, Span value) + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { } - public void OnHeadersComplete() + public void OnHeadersComplete(bool endStream) { } @@ -86,11 +88,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance RequestHandler = requestHandler; } - public void OnHeader(Span name, Span value) + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) => RequestHandler.OnHeader(name, value); - public void OnHeadersComplete() - => RequestHandler.OnHeadersComplete(); + public void OnHeadersComplete(bool endStream) + => RequestHandler.OnHeadersComplete(endStream); 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); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/IntegerDecoderBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/IntegerDecoderBenchmark.cs index 6fc22390c0..e41d076ec6 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/IntegerDecoderBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/IntegerDecoderBenchmark.cs @@ -1,8 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Net.Http.HPack; using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockTrace.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockTrace.cs index d2514f998f..aeab28894b 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockTrace.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockTrace.cs @@ -2,10 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net.Http.HPack; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/NullParser.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/NullParser.cs index 288588f3b1..53bae7b1b5 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/NullParser.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/NullParser.cs @@ -3,8 +3,10 @@ using System; using System.Buffers; +using System.Net.Http; using System.Text; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using HttpMethod = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -26,7 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance handler.OnHeader(new Span(_hostHeaderName), new Span(_hostHeaderValue)); handler.OnHeader(new Span(_acceptHeaderName), new Span(_acceptHeaderValue)); handler.OnHeader(new Span(_connectionHeaderName), new Span(_connectionHeaderValue)); - handler.OnHeadersComplete(); + handler.OnHeadersComplete(endStream: false); return true; } diff --git a/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs b/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs index 6ca7d17f3c..b51e3a5b04 100644 --- a/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs +++ b/src/Servers/Kestrel/samples/http2cat/Http2Utilities.cs @@ -9,13 +9,14 @@ using System.Collections.Generic; using System.IO; using System.IO.Pipelines; using System.Linq; +using System.Net.Http; +using System.Net.Http.HPack; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Net.Http.Headers; @@ -119,12 +120,12 @@ namespace http2cat _pair = new DuplexPipe.DuplexPipePair(transport: null, application: clientConnectionContext.Transport); } - void IHttpHeadersHandler.OnHeader(Span name, Span value) + void IHttpHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan value) { _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); } - void IHttpHeadersHandler.OnHeadersComplete() { } + void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } public async Task InitializeConnectionAsync(int expectedSettingsCount = 3) { diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index e38dfb19a9..69058b75c5 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -951,7 +951,7 @@ $@" private void Clear(long bitsToClear) }} while (tempBits != 0); }}" : "")}{(loop.ClassName == "HttpRequestHeaders" ? $@" [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public unsafe void Append(Span name, Span value) + public unsafe void Append(ReadOnlySpan name, ReadOnlySpan value) {{ ref byte nameStart = ref MemoryMarshal.GetReference(name); ref StringValues values = ref Unsafe.AsRef(null); diff --git a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs index f4b55a18a6..9188ef8f22 100644 --- a/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs +++ b/src/Servers/Kestrel/shared/test/CompositeKestrelTrace.cs @@ -2,10 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net.Http.HPack; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index a294d65225..beaa39740e 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -6,13 +6,14 @@ using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; +using System.Net.Http.HPack; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -1564,7 +1565,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, - expectedErrorMessage: CoreStrings.HPackErrorIncompleteHeaderBlock); + expectedErrorMessage: SR.net_http_hpack_incomplete_header_block); } [Fact] @@ -1592,7 +1593,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, - expectedErrorMessage: CoreStrings.HPackErrorIntegerTooBig); + expectedErrorMessage: SR.net_http_hpack_bad_integer); } [Theory] @@ -3431,7 +3432,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, - expectedErrorMessage: CoreStrings.HPackErrorIncompleteHeaderBlock); + expectedErrorMessage: SR.net_http_hpack_incomplete_header_block); } [Theory] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 00f8f3b829..2acf67530b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -6,6 +6,8 @@ using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; +using System.Net.Http.HPack; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; @@ -15,7 +17,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -2041,7 +2042,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await _connectionTask; var message = Assert.Single(TestApplicationErrorLogger.Messages, m => m.Exception is HPackEncodingException); - Assert.Contains(CoreStrings.HPackErrorNotEnoughBuffer, message.Exception.Message); + Assert.Contains(SR.net_http_hpack_encode_failure, message.Exception.Message); } [Fact] @@ -2614,7 +2615,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var message = await appFinished.Task.DefaultTimeout(); - Assert.Equal(CoreStrings.HPackErrorNotEnoughBuffer, message); + Assert.Equal(SR.net_http_hpack_encode_failure, message); // Just the StatusCode gets written before aborting in the continuation frame await ExpectAsync(Http2FrameType.HEADERS, @@ -2625,7 +2626,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _pair.Application.Output.Complete(); await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, - CoreStrings.HPackErrorNotEnoughBuffer); + SR.net_http_hpack_encode_failure); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 3a73eb0213..c6af966746 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -9,6 +9,8 @@ using System.Collections.Generic; using System.IO; using System.IO.Pipelines; using System.Linq; +using System.Net.Http; +using System.Net.Http.HPack; using System.Reflection; using System.Text; using System.Threading; @@ -21,7 +23,6 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; @@ -400,12 +401,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests base.Dispose(); } - void IHttpHeadersHandler.OnHeader(Span name, Span value) + void IHttpHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan value) { _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); } - void IHttpHeadersHandler.OnHeadersComplete() { } + void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } protected void CreateConnection() { diff --git a/src/Shared/Http2/CopyToAspNetCore.cmd b/src/Shared/Http2/CopyToAspNetCore.cmd new file mode 100644 index 0000000000..1df4fb7e74 --- /dev/null +++ b/src/Shared/Http2/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 paramter is not set, aborting. + exit /b 1 +) + +echo ASPNETCORE_REPO: %remote_repo% + +robocopy . %remote_repo%\src\Shared\Http2 /MIR +robocopy .\..\..\..\..\..\tests\Tests\System\Net\Http2\ %remote_repo%\src\Shared\test\Shared.Tests\Http2 /MIR \ No newline at end of file diff --git a/src/Shared/Http2/CopyToCoreFx.cmd b/src/Shared/Http2/CopyToCoreFx.cmd new file mode 100644 index 0000000000..5945d65257 --- /dev/null +++ b/src/Shared/Http2/CopyToCoreFx.cmd @@ -0,0 +1,14 @@ +@ECHO OFF +SETLOCAL + +if not [%1] == [] (set remote_repo=%1) else (set remote_repo=%COREFX_REPO%) + +IF [%remote_repo%] == [] ( + echo The 'COREFX_REPO' environment variable or command line paramter is not set, aborting. + exit /b 1 +) + +echo COREFX_REPO: %remote_repo% + +robocopy . %remote_repo%\src\Common\src\System\Net\Http\Http2 /MIR +robocopy .\..\test\Shared.Tests\Http2 %remote_repo%\src\Common\tests\Tests\System\Net\Http2 /MIR diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/DynamicTable.cs b/src/Shared/Http2/Hpack/DynamicTable.cs similarity index 68% rename from src/Servers/Kestrel/Core/src/Internal/Http2/HPack/DynamicTable.cs rename to src/Shared/Http2/Hpack/DynamicTable.cs index 7183589021..22178acae7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/DynamicTable.cs +++ b/src/Shared/Http2/Hpack/DynamicTable.cs @@ -1,12 +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. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. -using System; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack +namespace System.Net.Http.HPack { - // The dynamic table is defined as a queue where items are inserted at the front and removed from the back. - // It's implemented as a circular buffer that appends to the end and trims from the front. Thus index are reversed. internal class DynamicTable { private HeaderField[] _buffer; @@ -37,19 +34,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack throw new IndexOutOfRangeException(); } - var modIndex = _insertIndex - index - 1; - if (modIndex < 0) + index = _insertIndex - index - 1; + + if (index < 0) { - modIndex += _buffer.Length; + // _buffer is circular; wrap the index back around. + index += _buffer.Length; } - return _buffer[modIndex]; + return _buffer[index]; } } - public void Insert(Span name, Span value) + public void Insert(ReadOnlySpan name, ReadOnlySpan value) { - var entryLength = HeaderField.GetLength(name.Length, value.Length); + int entryLength = HeaderField.GetLength(name.Length, value.Length); EnsureAvailable(entryLength); if (entryLength > _maxSize) @@ -74,12 +73,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack { var newBuffer = new HeaderField[maxSize / HeaderField.RfcOverhead]; - for (var i = 0; i < Count; i++) - { - newBuffer[i] = _buffer[i]; - } + int headCount = Math.Min(_buffer.Length - _removeIndex, _count); + int tailCount = _count - headCount; + + Array.Copy(_buffer, _removeIndex, newBuffer, 0, headCount); + Array.Copy(_buffer, 0, newBuffer, headCount, tailCount); _buffer = newBuffer; + _removeIndex = 0; + _insertIndex = _count; _maxSize = maxSize; } else @@ -93,7 +95,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack { while (_count > 0 && _maxSize - _size < available) { - _size -= _buffer[_removeIndex].Length; + ref HeaderField field = ref _buffer[_removeIndex]; + _size -= field.Length; + field = default; + _count--; _removeIndex = (_removeIndex + 1) % _buffer.Length; } diff --git a/src/Shared/Http2/Hpack/HPackDecoder.cs b/src/Shared/Http2/Hpack/HPackDecoder.cs new file mode 100644 index 0000000000..19e6f34898 --- /dev/null +++ b/src/Shared/Http2/Hpack/HPackDecoder.cs @@ -0,0 +1,491 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Buffers; +using System.Diagnostics; + +namespace System.Net.Http.HPack +{ + internal class HPackDecoder + { + private enum State + { + Ready, + HeaderFieldIndex, + HeaderNameIndex, + HeaderNameLength, + HeaderNameLengthContinue, + HeaderName, + HeaderValueLength, + HeaderValueLengthContinue, + HeaderValue, + DynamicTableSizeUpdate + } + + public const int DefaultHeaderTableSize = 4096; + public const int DefaultStringOctetsSize = 4096; + public const int DefaultMaxHeadersLength = 64 * 1024; + + // http://httpwg.org/specs/rfc7541.html#rfc.section.6.1 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | Index (7+) | + // +---+---------------------------+ + private const byte IndexedHeaderFieldMask = 0x80; + private const byte IndexedHeaderFieldRepresentation = 0x80; + + // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 1 | Index (6+) | + // +---+---+-----------------------+ + private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0; + private const byte LiteralHeaderFieldWithIncrementalIndexingRepresentation = 0x40; + + // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | Index (4+) | + // +---+---+-----------------------+ + private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0; + private const byte LiteralHeaderFieldWithoutIndexingRepresentation = 0x00; + + // http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+-----------------------+ + private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0; + private const byte LiteralHeaderFieldNeverIndexedRepresentation = 0x10; + + // http://httpwg.org/specs/rfc7541.html#rfc.section.6.3 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Max size (5+) | + // +---+---------------------------+ + private const byte DynamicTableSizeUpdateMask = 0xe0; + private const byte DynamicTableSizeUpdateRepresentation = 0x20; + + // http://httpwg.org/specs/rfc7541.html#rfc.section.5.2 + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | H | String Length (7+) | + // +---+---------------------------+ + private const byte HuffmanMask = 0x80; + + private const int IndexedHeaderFieldPrefix = 7; + private const int LiteralHeaderFieldWithIncrementalIndexingPrefix = 6; + private const int LiteralHeaderFieldWithoutIndexingPrefix = 4; + private const int LiteralHeaderFieldNeverIndexedPrefix = 4; + private const int DynamicTableSizeUpdatePrefix = 5; + private const int StringLengthPrefix = 7; + + private readonly int _maxDynamicTableSize; + private readonly int _maxHeadersLength; + private readonly DynamicTable _dynamicTable; + private readonly IntegerDecoder _integerDecoder = new IntegerDecoder(); + private byte[] _stringOctets; + private byte[] _headerNameOctets; + private byte[] _headerValueOctets; + + private State _state = State.Ready; + private byte[] _headerName; + private int _stringIndex; + private int _stringLength; + private int _headerNameLength; + private int _headerValueLength; + private bool _index; + private bool _huffman; + private bool _headersObserved; + + public HPackDecoder(int maxDynamicTableSize = DefaultHeaderTableSize, int maxHeadersLength = DefaultMaxHeadersLength) + : this(maxDynamicTableSize, maxHeadersLength, new DynamicTable(maxDynamicTableSize)) + { + } + + // For testing. + internal HPackDecoder(int maxDynamicTableSize, int maxHeadersLength, DynamicTable dynamicTable) + { + _maxDynamicTableSize = maxDynamicTableSize; + _maxHeadersLength = maxHeadersLength; + _dynamicTable = dynamicTable; + + _stringOctets = new byte[DefaultStringOctetsSize]; + _headerNameOctets = new byte[DefaultStringOctetsSize]; + _headerValueOctets = new byte[DefaultStringOctetsSize]; + } + + public void Decode(in ReadOnlySequence data, bool endHeaders, IHttpHeadersHandler handler) + { + foreach (ReadOnlyMemory segment in data) + { + DecodeInternal(segment.Span, endHeaders, handler); + } + + CheckIncompleteHeaderBlock(endHeaders); + } + + public void Decode(ReadOnlySpan data, bool endHeaders, IHttpHeadersHandler handler) + { + DecodeInternal(data, endHeaders, handler); + CheckIncompleteHeaderBlock(endHeaders); + } + + private void DecodeInternal(ReadOnlySpan data, bool endHeaders, IHttpHeadersHandler handler) + { + int intResult; + + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; + switch (_state) + { + case State.Ready: + // TODO: Instead of masking and comparing each prefix value, + // consider doing a 16-way switch on the first four bits (which is the max prefix size). + // Look at this once we have more concrete perf data. + if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation) + { + _headersObserved = true; + + int val = b & ~IndexedHeaderFieldMask; + + if (_integerDecoder.BeginTryDecode((byte)val, IndexedHeaderFieldPrefix, out intResult)) + { + OnIndexedHeaderField(intResult, handler); + } + else + { + _state = State.HeaderFieldIndex; + } + } + else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation) + { + _headersObserved = true; + + _index = true; + int val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask; + + if (val == 0) + { + _state = State.HeaderNameLength; + } + else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix, out intResult)) + { + OnIndexedHeaderName(intResult); + } + else + { + _state = State.HeaderNameIndex; + } + } + else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation) + { + _headersObserved = true; + + _index = false; + int val = b & ~LiteralHeaderFieldWithoutIndexingMask; + + if (val == 0) + { + _state = State.HeaderNameLength; + } + else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix, out intResult)) + { + OnIndexedHeaderName(intResult); + } + else + { + _state = State.HeaderNameIndex; + } + } + else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation) + { + _headersObserved = true; + + _index = false; + int val = b & ~LiteralHeaderFieldNeverIndexedMask; + + if (val == 0) + { + _state = State.HeaderNameLength; + } + else if (_integerDecoder.BeginTryDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix, out intResult)) + { + OnIndexedHeaderName(intResult); + } + else + { + _state = State.HeaderNameIndex; + } + } + else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation) + { + // https://tools.ietf.org/html/rfc7541#section-4.2 + // This dynamic table size + // update MUST occur at the beginning of the first header block + // following the change to the dynamic table size. + if (_headersObserved) + { + throw new HPackDecodingException(SR.net_http_hpack_late_dynamic_table_size_update); + } + + if (_integerDecoder.BeginTryDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix, out intResult)) + { + SetDynamicHeaderTableSize(intResult); + } + else + { + _state = State.DynamicTableSizeUpdate; + } + } + else + { + // Can't happen + Debug.Fail("Unreachable code"); + throw new InvalidOperationException("Unreachable code."); + } + + break; + case State.HeaderFieldIndex: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnIndexedHeaderField(intResult, handler); + } + + break; + case State.HeaderNameIndex: + if (_integerDecoder.TryDecode(b, out intResult)) + { + OnIndexedHeaderName(intResult); + } + + break; + case State.HeaderNameLength: + _huffman = (b & HuffmanMask) != 0; + + if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) + { + if (intResult == 0) + { + throw new HPackDecodingException(SR.Format(SR.net_http_invalid_header_name, "")); + } + + OnStringLength(intResult, nextState: State.HeaderName); + } + else + { + _state = State.HeaderNameLengthContinue; + } + + break; + case State.HeaderNameLengthContinue: + if (_integerDecoder.TryDecode(b, out intResult)) + { + // IntegerDecoder disallows overlong encodings, where an integer is encoded with more bytes than is strictly required. + // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case. + Debug.Assert(intResult != 0, "A header name length of 0 should never be encoded with a continuation byte."); + + OnStringLength(intResult, nextState: State.HeaderName); + } + + break; + case State.HeaderName: + _stringOctets[_stringIndex++] = b; + + if (_stringIndex == _stringLength) + { + OnString(nextState: State.HeaderValueLength); + } + + break; + case State.HeaderValueLength: + _huffman = (b & HuffmanMask) != 0; + + if (_integerDecoder.BeginTryDecode((byte)(b & ~HuffmanMask), StringLengthPrefix, out intResult)) + { + OnStringLength(intResult, nextState: State.HeaderValue); + + if (intResult == 0) + { + ProcessHeaderValue(handler); + } + } + else + { + _state = State.HeaderValueLengthContinue; + } + + break; + case State.HeaderValueLengthContinue: + if (_integerDecoder.TryDecode(b, out intResult)) + { + // IntegerDecoder disallows overlong encodings where an integer is encoded with more bytes than is strictly required. + // 0 should always be represented by a single byte, so we shouldn't need to check for it in the continuation case. + Debug.Assert(intResult != 0, "A header value length of 0 should never be encoded with a continuation byte."); + + OnStringLength(intResult, nextState: State.HeaderValue); + } + + break; + case State.HeaderValue: + _stringOctets[_stringIndex++] = b; + + if (_stringIndex == _stringLength) + { + ProcessHeaderValue(handler); + } + + break; + case State.DynamicTableSizeUpdate: + if (_integerDecoder.TryDecode(b, out intResult)) + { + SetDynamicHeaderTableSize(intResult); + _state = State.Ready; + } + + break; + default: + // Can't happen + Debug.Fail("HPACK decoder reach an invalid state"); + throw new NotImplementedException(_state.ToString()); + } + } + } + + private void CheckIncompleteHeaderBlock(bool endHeaders) + { + if (endHeaders) + { + if (_state != State.Ready) + { + throw new HPackDecodingException(SR.net_http_hpack_incomplete_header_block); + } + + _headersObserved = false; + } + } + + private void ProcessHeaderValue(IHttpHeadersHandler handler) + { + OnString(nextState: State.Ready); + + var headerNameSpan = new Span(_headerName, 0, _headerNameLength); + var headerValueSpan = new Span(_headerValueOctets, 0, _headerValueLength); + + handler?.OnHeader(headerNameSpan, headerValueSpan); + + if (_index) + { + _dynamicTable.Insert(headerNameSpan, headerValueSpan); + } + } + + public void CompleteDecode() + { + if (_state != State.Ready) + { + // Incomplete header block + throw new HPackDecodingException(SR.net_http_hpack_unexpected_end); + } + } + + private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler) + { + HeaderField header = GetHeader(index); + handler?.OnHeader(header.Name, header.Value); + _state = State.Ready; + } + + private void OnIndexedHeaderName(int index) + { + HeaderField header = GetHeader(index); + _headerName = header.Name; + _headerNameLength = header.Name.Length; + _state = State.HeaderValueLength; + } + + private void OnStringLength(int length, State nextState) + { + if (length > _stringOctets.Length) + { + if (length > _maxHeadersLength) + { + throw new HPackDecodingException(SR.Format(SR.net_http_headers_exceeded_length, _maxHeadersLength)); + } + + _stringOctets = new byte[Math.Max(length, _stringOctets.Length * 2)]; + } + + _stringLength = length; + _stringIndex = 0; + _state = nextState; + } + + private void OnString(State nextState) + { + int Decode(ref byte[] dst) + { + if (_huffman) + { + return Huffman.Decode(new ReadOnlySpan(_stringOctets, 0, _stringLength), ref dst); + } + else + { + if (dst.Length < _stringLength) + { + dst = new byte[Math.Max(_stringLength, dst.Length * 2)]; + } + + Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength); + return _stringLength; + } + } + + try + { + if (_state == State.HeaderName) + { + _headerNameLength = Decode(ref _headerNameOctets); + _headerName = _headerNameOctets; + } + else + { + _headerValueLength = Decode(ref _headerValueOctets); + } + } + catch (HuffmanDecodingException ex) + { + throw new HPackDecodingException(SR.net_http_hpack_huffman_decode_failed, ex); + } + + _state = nextState; + } + + private HeaderField GetHeader(int index) + { + try + { + return index <= StaticTable.Count + ? StaticTable.Get(index - 1) + : _dynamicTable[index - StaticTable.Count - 1]; + } + catch (IndexOutOfRangeException) + { + // Header index out of range. + throw new HPackDecodingException(SR.Format(SR.net_http_hpack_invalid_index, index)); + } + } + + private void SetDynamicHeaderTableSize(int size) + { + if (size > _maxDynamicTableSize) + { + throw new HPackDecodingException(SR.Format(SR.net_http_hpack_large_table_size_update, size, _maxDynamicTableSize)); + } + + _dynamicTable.Resize(size); + } + } +} diff --git a/src/Shared/Http2/Hpack/HPackDecodingException.cs b/src/Shared/Http2/Hpack/HPackDecodingException.cs new file mode 100644 index 0000000000..63d80cbb9f --- /dev/null +++ b/src/Shared/Http2/Hpack/HPackDecodingException.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Runtime.Serialization; + +namespace System.Net.Http.HPack +{ + // TODO: Should this be public? + [Serializable] + internal class HPackDecodingException : Exception + { + public HPackDecodingException() + { + } + + public HPackDecodingException(string message) : base(message) + { + } + + public HPackDecodingException(string message, Exception innerException) : base(message, innerException) + { + } + + public HPackDecodingException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Shared/Http2/Hpack/HPackEncoder.cs b/src/Shared/Http2/Hpack/HPackEncoder.cs new file mode 100644 index 0000000000..3aa94376be --- /dev/null +++ b/src/Shared/Http2/Hpack/HPackEncoder.cs @@ -0,0 +1,530 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Net.Http.HPack +{ + internal class HPackEncoder + { + private IEnumerator> _enumerator; + + public bool BeginEncode(IEnumerable> headers, Span buffer, out int length) + { + _enumerator = headers.GetEnumerator(); + _enumerator.MoveNext(); + + return Encode(buffer, out length); + } + + public bool BeginEncode(int statusCode, IEnumerable> headers, Span buffer, out int length) + { + _enumerator = headers.GetEnumerator(); + _enumerator.MoveNext(); + + int statusCodeLength = EncodeStatusCode(statusCode, buffer); + bool done = Encode(buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out int headersLength); + length = statusCodeLength + headersLength; + + 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) + { + int currentLength = 0; + do + { + if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(currentLength), out int headerLength)) + { + if (currentLength == 0 && throwIfNoneEncoded) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + + length = currentLength; + return false; + } + + currentLength += headerLength; + } + while (_enumerator.MoveNext()); + + length = currentLength; + + return true; + } + + private int EncodeStatusCode(int statusCode, Span buffer) + { + switch (statusCode) + { + // Status codes which exist in the HTTP/2 StaticTable. + case 200: + case 204: + case 206: + case 304: + case 400: + case 404: + case 500: + buffer[0] = (byte)(0x80 | StaticTable.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; + } + } + + private bool EncodeHeader(string name, string value, Span buffer, out int length) + { + int i = 0; + length = 0; + + if (buffer.Length == 0) + { + return false; + } + + buffer[i++] = 0; + + if (i == buffer.Length) + { + return false; + } + + if (!EncodeString(name, buffer.Slice(i), out int nameLength, lowercase: true)) + { + return false; + } + + i += nameLength; + + if (i >= buffer.Length) + { + return false; + } + + if (!EncodeString(value, buffer.Slice(i), out int valueLength, lowercase: false)) + { + return false; + } + + i += valueLength; + + length = i; + return true; + } + + private bool EncodeString(string value, Span destination, out int bytesWritten, bool lowercase) + { + // 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) | + // +-------------------------------+ + const int toLowerMask = 0x20; + + if (destination.Length != 0) + { + 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)(lowercase && (uint)(c - 'A') <= ('Z' - 'A') ? c | toLowerMask : c); + } + + bytesWritten = integerLength + value.Length; + return true; + } + } + } + + bytesWritten = 0; + return false; + } + + // Things we should add: + // * Huffman encoding + // + // Things we should consider adding: + // * Dynamic table encoding: + // This would make the encoder stateful, which complicates things significantly. + // Additionally, it's not clear exactly what strings we would add to the dynamic table + // without some additional guidance from the user about this. + // So for now, don't do dynamic encoding. + + /// Encodes an "Indexed Header Field". + public static bool EncodeIndexedHeaderField(int index, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.1 + // ---------------------------------------------------- + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 1 | Index (7+) | + // +---+---------------------------+ + + if (destination.Length != 0) + { + destination[0] = 0x80; + return IntegerEncoder.Encode(index, 7, destination, out bytesWritten); + } + + bytesWritten = 0; + return false; + } + + /// Encodes a "Literal Header Field without Indexing". + public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | Index (4+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + if ((uint)destination.Length >= 2) + { + destination[0] = 0; + if (IntegerEncoder.Encode(index, 4, destination, out int indexLength)) + { + Debug.Assert(indexLength >= 1); + if (EncodeStringLiteral(value, destination.Slice(indexLength), out int nameLength)) + { + bytesWritten = indexLength + nameLength; + return true; + } + } + } + + bytesWritten = 0; + return false; + } + + /// + /// Encodes a "Literal Header Field without Indexing", but only the index portion; + /// a subsequent call to must be used to encode the associated value. + /// + public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | Index (4+) | + // +---+---+-----------------------+ + // + // ... expected after this: + // + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + if ((uint)destination.Length != 0) + { + destination[0] = 0; + if (IntegerEncoder.Encode(index, 4, destination, out int indexLength)) + { + Debug.Assert(indexLength >= 1); + bytesWritten = indexLength; + return true; + } + } + + bytesWritten = 0; + return false; + } + + /// Encodes a "Literal Header Field without Indexing - New Name". + public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, ReadOnlySpan values, string separator, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + if ((uint)destination.Length >= 3) + { + destination[0] = 0; + if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) && + EncodeStringLiterals(values, separator, destination.Slice(1 + nameLength), out int valueLength)) + { + bytesWritten = 1 + nameLength + valueLength; + return true; + } + } + + bytesWritten = 0; + return false; + } + + /// + /// Encodes a "Literal Header Field without Indexing - New Name", but only the name portion; + /// a subsequent call to must be used to encode the associated value. + /// + public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.2 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 0 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // + // ... expected after this: + // + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + if ((uint)destination.Length >= 2) + { + destination[0] = 0; + if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength)) + { + bytesWritten = 1 + nameLength; + return true; + } + } + + bytesWritten = 0; + return false; + } + + 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.Length != 0) + { + 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(SR.net_http_request_invalid_char_encoding); + } + + 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.Length != 0) + { + 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(ReadOnlySpan 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.Length != 0) + { + 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)); + + destination[0] = 0; + 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 = EncodeLiteralHeaderFieldWithoutIndexing(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(); + } + + /// 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 (EncodeLiteralHeaderFieldWithoutIndexing(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]; + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackEncodingException.cs b/src/Shared/Http2/Hpack/HPackEncodingException.cs similarity index 53% rename from src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackEncodingException.cs rename to src/Shared/Http2/Hpack/HPackEncodingException.cs index 6911754476..397b313191 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackEncodingException.cs +++ b/src/Shared/Http2/Hpack/HPackEncodingException.cs @@ -1,16 +1,20 @@ -// 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. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. -using System; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack +namespace System.Net.Http.HPack { internal sealed class HPackEncodingException : Exception { + public HPackEncodingException() + { + } + public HPackEncodingException(string message) : base(message) { } + public HPackEncodingException(string message, Exception innerException) : base(message, innerException) { diff --git a/src/Shared/Http2/Hpack/HeaderField.cs b/src/Shared/Http2/Hpack/HeaderField.cs new file mode 100644 index 0000000000..1eba82412d --- /dev/null +++ b/src/Shared/Http2/Hpack/HeaderField.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Diagnostics; +using System.Text; + +namespace System.Net.Http.HPack +{ + internal readonly struct HeaderField + { + // http://httpwg.org/specs/rfc7541.html#rfc.section.4.1 + public const int RfcOverhead = 32; + + public HeaderField(ReadOnlySpan name, ReadOnlySpan value) + { + Debug.Assert(name.Length > 0); + + // TODO: We're allocating here on every new table entry. + // That means a poorly-behaved server could cause us to allocate repeatedly. + // We should revisit our allocation strategy here so we don't need to allocate per entry + // and we have a cap to how much allocation can happen per dynamic table + // (without limiting the number of table entries a server can provide within the table size limit). + 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 + RfcOverhead; + + public override string ToString() + { + if (Name != null) + { + return Encoding.ASCII.GetString(Name) + ": " + Encoding.ASCII.GetString(Value); + } + else + { + return ""; + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Huffman.cs b/src/Shared/Http2/Hpack/Huffman.cs similarity index 93% rename from src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Huffman.cs rename to src/Shared/Http2/Hpack/Huffman.cs index fed5481d30..1cfc8e7d5d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Huffman.cs +++ b/src/Shared/Http2/Hpack/Huffman.cs @@ -1,9 +1,10 @@ // 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 under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. -using System; +using System.Diagnostics; -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack +namespace System.Net.Http.HPack { internal class Huffman { @@ -303,25 +304,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack /// Decodes a Huffman encoded string from a byte array. /// /// The source byte array containing the encoded data. - /// The destination byte array to store the decoded data. + /// The destination byte array to store the decoded data. This may grow if its size is insufficient. /// The number of decoded symbols. - public static int Decode(ReadOnlySpan src, Span dst) + public static int Decode(ReadOnlySpan src, ref byte[] dstArray) { - var i = 0; - var j = 0; - var lastDecodedBits = 0; + Span dst = dstArray; + Debug.Assert(dst != null && dst.Length > 0); + + int i = 0; + int j = 0; + int lastDecodedBits = 0; while (i < src.Length) { // 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. - var next = (uint)(src[i] << 24 + lastDecodedBits); + // TODO ISSUE 31751: 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); next |= (i + 3 < src.Length ? (uint)(src[i + 3] << lastDecodedBits) : 0); next |= (i + 4 < src.Length ? (uint)(src[i + 4] >> (8 - lastDecodedBits)) : 0); - var ones = (uint)(int.MinValue >> (8 - lastDecodedBits - 1)); + uint ones = (uint)(int.MinValue >> (8 - lastDecodedBits - 1)); if (i == src.Length - 1 && lastDecodedBits > 0 && (next & ones) == ones) { // The remaining 7 or less bits are all 1, which is padding. @@ -334,24 +339,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack // The longest possible symbol size is 30 bits. If we're at the last 4 bytes // of the input, we need to make sure we pass the correct number of valid bits // left, otherwise the trailing 0s in next may form a valid symbol. - var validBits = Math.Min(30, (8 - lastDecodedBits) + (src.Length - i - 1) * 8); - var ch = DecodeValue(next, validBits, out var decodedBits); + int validBits = Math.Min(30, (8 - lastDecodedBits) + (src.Length - i - 1) * 8); + int ch = DecodeValue(next, validBits, out int decodedBits); if (ch == -1) { // No valid symbol could be decoded with the bits in next - throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorIncomplete); + throw new HuffmanDecodingException(SR.net_http_hpack_huffman_decode_failed); } else if (ch == 256) { // A Huffman-encoded string literal containing the EOS symbol MUST be treated as a decoding error. // http://httpwg.org/specs/rfc7541.html#rfc.section.5.2 - throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorEOS); + throw new HuffmanDecodingException(SR.net_http_hpack_huffman_decode_failed); } if (j == dst.Length) { - throw new HuffmanDecodingException(CoreStrings.HPackHuffmanErrorDestinationTooSmall); + Array.Resize(ref dstArray, dst.Length * 2); + dst = dstArray; } dst[j++] = (byte)ch; @@ -398,11 +404,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack // symbol in the list of values associated with bit length b in the decoding table by indexing it // with codeMax - v. - var codeMax = 0; + int codeMax = 0; - for (var i = 0; i < _decodingTable.Length && _decodingTable[i].codeLength <= validBits; i++) + for (int i = 0; i < _decodingTable.Length && _decodingTable[i].codeLength <= validBits; i++) { - var (codeLength, codes) = _decodingTable[i]; + (int codeLength, int[] codes) = _decodingTable[i]; if (i > 0) { @@ -411,8 +417,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack codeMax += codes.Length; - var mask = int.MinValue >> (codeLength - 1); - var masked = (data & mask) >> (32 - codeLength); + int mask = int.MinValue >> (codeLength - 1); + long masked = (data & mask) >> (32 - codeLength); if (masked < codeMax) { diff --git a/src/Shared/Http2/Hpack/HuffmanDecodingException.cs b/src/Shared/Http2/Hpack/HuffmanDecodingException.cs new file mode 100644 index 0000000000..64352e9535 --- /dev/null +++ b/src/Shared/Http2/Hpack/HuffmanDecodingException.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Runtime.Serialization; + +namespace System.Net.Http.HPack +{ + // TODO: Should this be public? + [Serializable] + internal class HuffmanDecodingException : Exception, ISerializable + { + public HuffmanDecodingException() + { + } + + public HuffmanDecodingException(string message) + : base(message) + { + } + + protected HuffmanDecodingException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + void ISerializable.GetObjectData(SerializationInfo serializationInfo, StreamingContext streamingContext) + { + base.GetObjectData(serializationInfo, streamingContext); + } + + public override void GetObjectData(SerializationInfo serializationInfo, StreamingContext streamingContext) + { + base.GetObjectData(serializationInfo, streamingContext); + } + } +} diff --git a/src/Shared/Http2/Hpack/IntegerDecoder.cs b/src/Shared/Http2/Hpack/IntegerDecoder.cs new file mode 100644 index 0000000000..dafa6c08b4 --- /dev/null +++ b/src/Shared/Http2/Hpack/IntegerDecoder.cs @@ -0,0 +1,103 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Diagnostics; +using System.Numerics; + +namespace System.Net.Http.HPack +{ + internal class IntegerDecoder + { + private int _i; + private int _m; + + /// + /// Decodes the first byte of the integer. + /// + /// + /// The first byte of the variable-length encoded integer. + /// + /// + /// The number of lower bits in this prefix byte that the + /// integer has been encoded into. Must be between 1 and 8. + /// Upper bits must be zero. + /// + /// + /// If decoded successfully, contains the decoded integer. + /// + /// + /// If the integer has been fully decoded, true. + /// Otherwise, false -- must be called on subsequent bytes. + /// + /// + /// The term "prefix" can be confusing. From the HPACK spec: + /// An integer is represented in two parts: a prefix that fills the current octet and an + /// optional list of octets that are used if the integer value does not fit within the prefix. + /// + public bool BeginTryDecode(byte b, int prefixLength, out int result) + { + Debug.Assert(prefixLength >= 1 && prefixLength <= 8); + Debug.Assert((b & ~((1 << prefixLength) - 1)) == 0, "bits other than prefix data must be set to 0."); + + if (b < ((1 << prefixLength) - 1)) + { + result = b; + return true; + } + + _i = b; + _m = 0; + result = 0; + return false; + } + + /// + /// Decodes subsequent bytes of an integer. + /// + /// The next byte. + /// + /// If decoded successfully, contains the decoded integer. + /// + /// If the integer has been fully decoded, true. Otherwise, false -- must be called on subsequent bytes. + public bool TryDecode(byte b, out int result) + { + // Check if shifting b by _m would result in > 31 bits. + // No masking is required: if the 8th bit is set, it indicates there is a + // bit set in a future byte, so it is fine to check that here as if it were + // bit 0 on the next byte. + // This is a simplified form of: + // int additionalBitsRequired = 32 - BitOperations.LeadingZeroCount((uint)b); + // if (_m + additionalBitsRequired > 31) + if (BitOperations.LeadingZeroCount((uint)b) <= _m) + { + throw new HPackDecodingException(SR.net_http_hpack_bad_integer); + } + + _i = _i + ((b & 0x7f) << _m); + + // If the addition overflowed, the result will be negative. + if (_i < 0) + { + throw new HPackDecodingException(SR.net_http_hpack_bad_integer); + } + + _m = _m + 7; + + if ((b & 128) == 0) + { + if (b == 0 && _m / 7 > 1) + { + // Do not accept overlong encodings. + throw new HPackDecodingException(SR.net_http_hpack_bad_integer); + } + + result = _i; + return true; + } + + result = 0; + return false; + } + } +} diff --git a/src/Shared/Http2/Hpack/IntegerEncoder.cs b/src/Shared/Http2/Hpack/IntegerEncoder.cs new file mode 100644 index 0000000000..b98bfd3c75 --- /dev/null +++ b/src/Shared/Http2/Hpack/IntegerEncoder.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Diagnostics; + +namespace System.Net.Http.HPack +{ + internal static class IntegerEncoder + { + /// + /// Encodes an integer into one or more bytes. + /// + /// The value to encode. Must not be negative. + /// The length of the prefix, in bits, to encode within. Must be between 1 and 8. + /// The destination span to encode to. + /// The number of bytes used to encode . + /// If had enough storage to encode , true. Otherwise, false. + public static bool Encode(int value, int numBits, Span destination, out int bytesWritten) + { + Debug.Assert(value >= 0); + Debug.Assert(numBits >= 1 && numBits <= 8); + + if (destination.Length == 0) + { + bytesWritten = 0; + return false; + } + + destination[0] &= MaskHigh(8 - numBits); + + if (value < (1 << numBits) - 1) + { + destination[0] |= (byte)value; + + bytesWritten = 1; + return true; + } + else + { + destination[0] |= (byte)((1 << numBits) - 1); + + if (1 == destination.Length) + { + bytesWritten = 0; + return false; + } + + value = value - ((1 << numBits) - 1); + int i = 1; + + while (value >= 128) + { + destination[i++] = (byte)(value % 128 + 128); + + if (i >= destination.Length) + { + bytesWritten = 0; + return false; + } + + value = value / 128; + } + destination[i++] = (byte)value; + + bytesWritten = i; + return true; + } + } + + private static byte MaskHigh(int n) => (byte)(sbyte.MinValue >> (n - 1)); + } +} diff --git a/src/Shared/Http2/Hpack/StaticTable.cs b/src/Shared/Http2/Hpack/StaticTable.cs new file mode 100644 index 0000000000..79090d8495 --- /dev/null +++ b/src/Shared/Http2/Hpack/StaticTable.cs @@ -0,0 +1,159 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Collections.Generic; +using System.Text; + +namespace System.Net.Http.HPack +{ + internal static class StaticTable + { + // Index of status code into s_staticDecoderTable + private static readonly Dictionary s_statusIndex = new Dictionary + { + [200] = 8, + [204] = 9, + [206] = 10, + [304] = 11, + [400] = 12, + [404] = 13, + [500] = 14, + }; + + public static int Count => s_staticDecoderTable.Length; + + public static HeaderField Get(int index) => s_staticDecoderTable[index]; + + public static IReadOnlyDictionary StatusIndex => s_statusIndex; + + private static readonly HeaderField[] s_staticDecoderTable = new HeaderField[] + { + CreateHeaderField(":authority", ""), + CreateHeaderField(":method", "GET"), + CreateHeaderField(":method", "POST"), + CreateHeaderField(":path", "/"), + CreateHeaderField(":path", "/index.html"), + CreateHeaderField(":scheme", "http"), + CreateHeaderField(":scheme", "https"), + CreateHeaderField(":status", "200"), + CreateHeaderField(":status", "204"), + CreateHeaderField(":status", "206"), + CreateHeaderField(":status", "304"), + CreateHeaderField(":status", "400"), + CreateHeaderField(":status", "404"), + CreateHeaderField(":status", "500"), + CreateHeaderField("accept-charset", ""), + CreateHeaderField("accept-encoding", "gzip, deflate"), + CreateHeaderField("accept-language", ""), + CreateHeaderField("accept-ranges", ""), + CreateHeaderField("accept", ""), + CreateHeaderField("access-control-allow-origin", ""), + CreateHeaderField("age", ""), + CreateHeaderField("allow", ""), + CreateHeaderField("authorization", ""), + CreateHeaderField("cache-control", ""), + CreateHeaderField("content-disposition", ""), + CreateHeaderField("content-encoding", ""), + CreateHeaderField("content-language", ""), + CreateHeaderField("content-length", ""), + CreateHeaderField("content-location", ""), + CreateHeaderField("content-range", ""), + CreateHeaderField("content-type", ""), + CreateHeaderField("cookie", ""), + CreateHeaderField("date", ""), + CreateHeaderField("etag", ""), + CreateHeaderField("expect", ""), + CreateHeaderField("expires", ""), + CreateHeaderField("from", ""), + CreateHeaderField("host", ""), + CreateHeaderField("if-match", ""), + CreateHeaderField("if-modified-since", ""), + CreateHeaderField("if-none-match", ""), + CreateHeaderField("if-range", ""), + CreateHeaderField("if-unmodified-since", ""), + CreateHeaderField("last-modified", ""), + CreateHeaderField("link", ""), + CreateHeaderField("location", ""), + CreateHeaderField("max-forwards", ""), + CreateHeaderField("proxy-authenticate", ""), + CreateHeaderField("proxy-authorization", ""), + CreateHeaderField("range", ""), + CreateHeaderField("referer", ""), + CreateHeaderField("refresh", ""), + CreateHeaderField("retry-after", ""), + CreateHeaderField("server", ""), + CreateHeaderField("set-cookie", ""), + CreateHeaderField("strict-transport-security", ""), + CreateHeaderField("transfer-encoding", ""), + CreateHeaderField("user-agent", ""), + CreateHeaderField("vary", ""), + CreateHeaderField("via", ""), + CreateHeaderField("www-authenticate", "") + }; + + // TODO: The HeaderField constructor will allocate and copy again. We should avoid this. + // Tackle as part of header table allocation strategy in general (see note in HeaderField constructor). + + private static HeaderField CreateHeaderField(string name, string value) => + new HeaderField( + Encoding.ASCII.GetBytes(name), + value.Length != 0 ? Encoding.ASCII.GetBytes(value) : Array.Empty()); + + // Values for encoding. + // Unused values are omitted. + public const int Authority = 1; + public const int MethodGet = 2; + public const int MethodPost = 3; + public const int PathSlash = 4; + public const int SchemeHttp = 6; + public const int SchemeHttps = 7; + public const int AcceptCharset = 15; + public const int AcceptEncoding = 16; + public const int AcceptLanguage = 17; + public const int AcceptRanges = 18; + public const int Accept = 19; + public const int AccessControlAllowOrigin = 20; + public const int Age = 21; + public const int Allow = 22; + public const int Authorization = 23; + public const int CacheControl = 24; + public const int ContentDisposition = 25; + public const int ContentEncoding = 26; + public const int ContentLanguage = 27; + public const int ContentLength = 28; + public const int ContentLocation = 29; + public const int ContentRange = 30; + public const int ContentType = 31; + public const int Cookie = 32; + public const int Date = 33; + public const int ETag = 34; + public const int Expect = 35; + public const int Expires = 36; + public const int From = 37; + public const int Host = 38; + public const int IfMatch = 39; + public const int IfModifiedSince = 40; + public const int IfNoneMatch = 41; + public const int IfRange = 42; + public const int IfUnmodifiedSince = 43; + public const int LastModified = 44; + public const int Link = 45; + public const int Location = 46; + public const int MaxForwards = 47; + public const int ProxyAuthenticate = 48; + public const int ProxyAuthorization = 49; + public const int Range = 50; + public const int Referer = 51; + public const int Refresh = 52; + public const int RetryAfter = 53; + public const int Server = 54; + public const int SetCookie = 55; + public const int StrictTransportSecurity = 56; + public const int TransferEncoding = 57; + public const int UserAgent = 58; + public const int Vary = 59; + public const int Via = 60; + public const int WwwAuthenticate = 61; + } +} diff --git a/src/Shared/Http2/Hpack/StatusCodes.cs b/src/Shared/Http2/Hpack/StatusCodes.cs new file mode 100644 index 0000000000..602a3c02bc --- /dev/null +++ b/src/Shared/Http2/Hpack/StatusCodes.cs @@ -0,0 +1,218 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Globalization; +using System.Text; + +namespace System.Net.Http.HPack +{ + internal static class StatusCodes + { + private static ReadOnlySpan BytesStatus100 => new byte[] { (byte)'1', (byte)'0', (byte)'0' }; + private static ReadOnlySpan BytesStatus101 => new byte[] { (byte)'1', (byte)'0', (byte)'1' }; + private static ReadOnlySpan BytesStatus102 => new byte[] { (byte)'1', (byte)'0', (byte)'2' }; + + private static ReadOnlySpan BytesStatus200 => new byte[] { (byte)'2', (byte)'0', (byte)'0' }; + private static ReadOnlySpan BytesStatus201 => new byte[] { (byte)'2', (byte)'0', (byte)'1' }; + private static ReadOnlySpan BytesStatus202 => new byte[] { (byte)'2', (byte)'0', (byte)'2' }; + private static ReadOnlySpan BytesStatus203 => new byte[] { (byte)'2', (byte)'0', (byte)'3' }; + private static ReadOnlySpan BytesStatus204 => new byte[] { (byte)'2', (byte)'0', (byte)'4' }; + private static ReadOnlySpan BytesStatus205 => new byte[] { (byte)'2', (byte)'0', (byte)'5' }; + private static ReadOnlySpan BytesStatus206 => new byte[] { (byte)'2', (byte)'0', (byte)'6' }; + private static ReadOnlySpan BytesStatus207 => new byte[] { (byte)'2', (byte)'0', (byte)'7' }; + private static ReadOnlySpan BytesStatus208 => new byte[] { (byte)'2', (byte)'0', (byte)'8' }; + private static ReadOnlySpan BytesStatus226 => new byte[] { (byte)'2', (byte)'2', (byte)'6' }; + + private static ReadOnlySpan BytesStatus300 => new byte[] { (byte)'3', (byte)'0', (byte)'0' }; + private static ReadOnlySpan BytesStatus301 => new byte[] { (byte)'3', (byte)'0', (byte)'1' }; + private static ReadOnlySpan BytesStatus302 => new byte[] { (byte)'3', (byte)'0', (byte)'2' }; + private static ReadOnlySpan BytesStatus303 => new byte[] { (byte)'3', (byte)'0', (byte)'3' }; + private static ReadOnlySpan BytesStatus304 => new byte[] { (byte)'3', (byte)'0', (byte)'4' }; + private static ReadOnlySpan BytesStatus305 => new byte[] { (byte)'3', (byte)'0', (byte)'5' }; + private static ReadOnlySpan BytesStatus306 => new byte[] { (byte)'3', (byte)'0', (byte)'6' }; + private static ReadOnlySpan BytesStatus307 => new byte[] { (byte)'3', (byte)'0', (byte)'7' }; + private static ReadOnlySpan BytesStatus308 => new byte[] { (byte)'3', (byte)'0', (byte)'8' }; + + private static ReadOnlySpan BytesStatus400 => new byte[] { (byte)'4', (byte)'0', (byte)'0' }; + private static ReadOnlySpan BytesStatus401 => new byte[] { (byte)'4', (byte)'0', (byte)'1' }; + private static ReadOnlySpan BytesStatus402 => new byte[] { (byte)'4', (byte)'0', (byte)'2' }; + private static ReadOnlySpan BytesStatus403 => new byte[] { (byte)'4', (byte)'0', (byte)'3' }; + private static ReadOnlySpan BytesStatus404 => new byte[] { (byte)'4', (byte)'0', (byte)'4' }; + private static ReadOnlySpan BytesStatus405 => new byte[] { (byte)'4', (byte)'0', (byte)'5' }; + private static ReadOnlySpan BytesStatus406 => new byte[] { (byte)'4', (byte)'0', (byte)'6' }; + private static ReadOnlySpan BytesStatus407 => new byte[] { (byte)'4', (byte)'0', (byte)'7' }; + private static ReadOnlySpan BytesStatus408 => new byte[] { (byte)'4', (byte)'0', (byte)'8' }; + private static ReadOnlySpan BytesStatus409 => new byte[] { (byte)'4', (byte)'0', (byte)'9' }; + private static ReadOnlySpan BytesStatus410 => new byte[] { (byte)'4', (byte)'1', (byte)'0' }; + private static ReadOnlySpan BytesStatus411 => new byte[] { (byte)'4', (byte)'1', (byte)'1' }; + private static ReadOnlySpan BytesStatus412 => new byte[] { (byte)'4', (byte)'1', (byte)'2' }; + private static ReadOnlySpan BytesStatus413 => new byte[] { (byte)'4', (byte)'1', (byte)'3' }; + private static ReadOnlySpan BytesStatus414 => new byte[] { (byte)'4', (byte)'1', (byte)'4' }; + private static ReadOnlySpan BytesStatus415 => new byte[] { (byte)'4', (byte)'1', (byte)'5' }; + private static ReadOnlySpan BytesStatus416 => new byte[] { (byte)'4', (byte)'1', (byte)'6' }; + private static ReadOnlySpan BytesStatus417 => new byte[] { (byte)'4', (byte)'1', (byte)'7' }; + private static ReadOnlySpan BytesStatus418 => new byte[] { (byte)'4', (byte)'1', (byte)'8' }; + private static ReadOnlySpan BytesStatus419 => new byte[] { (byte)'4', (byte)'1', (byte)'9' }; + private static ReadOnlySpan BytesStatus421 => new byte[] { (byte)'4', (byte)'2', (byte)'1' }; + private static ReadOnlySpan BytesStatus422 => new byte[] { (byte)'4', (byte)'2', (byte)'2' }; + private static ReadOnlySpan BytesStatus423 => new byte[] { (byte)'4', (byte)'2', (byte)'3' }; + private static ReadOnlySpan BytesStatus424 => new byte[] { (byte)'4', (byte)'2', (byte)'4' }; + private static ReadOnlySpan BytesStatus426 => new byte[] { (byte)'4', (byte)'2', (byte)'6' }; + private static ReadOnlySpan BytesStatus428 => new byte[] { (byte)'4', (byte)'2', (byte)'8' }; + private static ReadOnlySpan BytesStatus429 => new byte[] { (byte)'4', (byte)'2', (byte)'9' }; + private static ReadOnlySpan BytesStatus431 => new byte[] { (byte)'4', (byte)'3', (byte)'1' }; + private static ReadOnlySpan BytesStatus451 => new byte[] { (byte)'4', (byte)'5', (byte)'1' }; + + private static ReadOnlySpan BytesStatus500 => new byte[] { (byte)'5', (byte)'0', (byte)'0' }; + private static ReadOnlySpan BytesStatus501 => new byte[] { (byte)'5', (byte)'0', (byte)'1' }; + private static ReadOnlySpan BytesStatus502 => new byte[] { (byte)'5', (byte)'0', (byte)'2' }; + private static ReadOnlySpan BytesStatus503 => new byte[] { (byte)'5', (byte)'0', (byte)'3' }; + private static ReadOnlySpan BytesStatus504 => new byte[] { (byte)'5', (byte)'0', (byte)'4' }; + private static ReadOnlySpan BytesStatus505 => new byte[] { (byte)'5', (byte)'0', (byte)'5' }; + private static ReadOnlySpan BytesStatus506 => new byte[] { (byte)'5', (byte)'0', (byte)'6' }; + private static ReadOnlySpan BytesStatus507 => new byte[] { (byte)'5', (byte)'0', (byte)'7' }; + private static ReadOnlySpan BytesStatus508 => new byte[] { (byte)'5', (byte)'0', (byte)'8' }; + private static ReadOnlySpan BytesStatus510 => new byte[] { (byte)'5', (byte)'1', (byte)'0' }; + private static ReadOnlySpan BytesStatus511 => new byte[] { (byte)'5', (byte)'1', (byte)'1' }; + + public static ReadOnlySpan ToStatusBytes(int statusCode) + { + switch (statusCode) + { + case (int)HttpStatusCode.Continue: + return BytesStatus100; + case (int)HttpStatusCode.SwitchingProtocols: + return BytesStatus101; + case (int)HttpStatusCode.Processing: + return BytesStatus102; + + case (int)HttpStatusCode.OK: + return BytesStatus200; + case (int)HttpStatusCode.Created: + return BytesStatus201; + case (int)HttpStatusCode.Accepted: + return BytesStatus202; + case (int)HttpStatusCode.NonAuthoritativeInformation: + return BytesStatus203; + case (int)HttpStatusCode.NoContent: + return BytesStatus204; + case (int)HttpStatusCode.ResetContent: + return BytesStatus205; + case (int)HttpStatusCode.PartialContent: + return BytesStatus206; + case (int)HttpStatusCode.MultiStatus: + return BytesStatus207; + case (int)HttpStatusCode.AlreadyReported: + return BytesStatus208; + case (int)HttpStatusCode.IMUsed: + return BytesStatus226; + + case (int)HttpStatusCode.MultipleChoices: + return BytesStatus300; + case (int)HttpStatusCode.MovedPermanently: + return BytesStatus301; + case (int)HttpStatusCode.Found: + return BytesStatus302; + case (int)HttpStatusCode.SeeOther: + return BytesStatus303; + case (int)HttpStatusCode.NotModified: + return BytesStatus304; + case (int)HttpStatusCode.UseProxy: + return BytesStatus305; + case (int)HttpStatusCode.Unused: + return BytesStatus306; + case (int)HttpStatusCode.TemporaryRedirect: + return BytesStatus307; + case (int)HttpStatusCode.PermanentRedirect: + return BytesStatus308; + + case (int)HttpStatusCode.BadRequest: + return BytesStatus400; + case (int)HttpStatusCode.Unauthorized: + return BytesStatus401; + case (int)HttpStatusCode.PaymentRequired: + return BytesStatus402; + case (int)HttpStatusCode.Forbidden: + return BytesStatus403; + case (int)HttpStatusCode.NotFound: + return BytesStatus404; + case (int)HttpStatusCode.MethodNotAllowed: + return BytesStatus405; + case (int)HttpStatusCode.NotAcceptable: + return BytesStatus406; + case (int)HttpStatusCode.ProxyAuthenticationRequired: + return BytesStatus407; + case (int)HttpStatusCode.RequestTimeout: + return BytesStatus408; + case (int)HttpStatusCode.Conflict: + return BytesStatus409; + case (int)HttpStatusCode.Gone: + return BytesStatus410; + case (int)HttpStatusCode.LengthRequired: + return BytesStatus411; + case (int)HttpStatusCode.PreconditionFailed: + return BytesStatus412; + case (int)HttpStatusCode.RequestEntityTooLarge: + return BytesStatus413; + case (int)HttpStatusCode.RequestUriTooLong: + return BytesStatus414; + case (int)HttpStatusCode.UnsupportedMediaType: + return BytesStatus415; + case (int)HttpStatusCode.RequestedRangeNotSatisfiable: + return BytesStatus416; + case (int)HttpStatusCode.ExpectationFailed: + return BytesStatus417; + case (int)418: + return BytesStatus418; + case (int)419: + return BytesStatus419; + case (int)HttpStatusCode.MisdirectedRequest: + return BytesStatus421; + case (int)HttpStatusCode.UnprocessableEntity: + return BytesStatus422; + case (int)HttpStatusCode.Locked: + return BytesStatus423; + case (int)HttpStatusCode.FailedDependency: + return BytesStatus424; + case (int)HttpStatusCode.UpgradeRequired: + return BytesStatus426; + case (int)HttpStatusCode.PreconditionRequired: + return BytesStatus428; + case (int)HttpStatusCode.TooManyRequests: + return BytesStatus429; + case (int)HttpStatusCode.RequestHeaderFieldsTooLarge: + return BytesStatus431; + case (int)HttpStatusCode.UnavailableForLegalReasons: + return BytesStatus451; + + case (int)HttpStatusCode.InternalServerError: + return BytesStatus500; + case (int)HttpStatusCode.NotImplemented: + return BytesStatus501; + case (int)HttpStatusCode.BadGateway: + return BytesStatus502; + case (int)HttpStatusCode.ServiceUnavailable: + return BytesStatus503; + case (int)HttpStatusCode.GatewayTimeout: + return BytesStatus504; + case (int)HttpStatusCode.HttpVersionNotSupported: + return BytesStatus505; + case (int)HttpStatusCode.VariantAlsoNegotiates: + return BytesStatus506; + case (int)HttpStatusCode.InsufficientStorage: + return BytesStatus507; + case (int)HttpStatusCode.LoopDetected: + return BytesStatus508; + case (int)HttpStatusCode.NotExtended: + return BytesStatus510; + case (int)HttpStatusCode.NetworkAuthenticationRequired: + return BytesStatus511; + + default: + return Encoding.ASCII.GetBytes(statusCode.ToString(CultureInfo.InvariantCulture)); + + } + } + } +} diff --git a/src/Shared/Http2/IHttpHeadersHandler.cs b/src/Shared/Http2/IHttpHeadersHandler.cs new file mode 100644 index 0000000000..f86dc9ff7a --- /dev/null +++ b/src/Shared/Http2/IHttpHeadersHandler.cs @@ -0,0 +1,12 @@ +// 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 interface IHttpHeadersHandler + { + void OnHeader(ReadOnlySpan name, ReadOnlySpan value); + void OnHeadersComplete(bool endStream); + } +} \ No newline at end of file diff --git a/src/Shared/Http2/ReadMe.SharedCode.md b/src/Shared/Http2/ReadMe.SharedCode.md new file mode 100644 index 0000000000..4482e11b84 --- /dev/null +++ b/src/Shared/Http2/ReadMe.SharedCode.md @@ -0,0 +1,36 @@ +The code in this directory is shared between CoreFx and AspNetCore. This contains HTTP/2 protocol infrastructure such as an HPACK implementation. Any changes to this dir need to be checked into both repositories. + +Corefx code paths: +- corefx\src\Common\src\System\Net\Http\Http2 +- corefx\src\Common\tests\Tests\System\Net\Http2 +AspNetCore code paths: +- AspNetCore\src\Shared\Http2 +- AspNetCore\src\Shared\test\Shared.Tests\Http2 + +## Copying code +To copy code from CoreFx to AspNetCore set ASPNETCORE_REPO to the AspNetCore repo root and then run CopyToAspNetCore.cmd. +To copy code from AspNetCore to CoreFx set COREFX_REPO to the CoreFx repo root and then run CopyToCoreFx.cmd. + +## Building CoreFx code: +- https://github.com/dotnet/corefx/blob/master/Documentation/building/windows-instructions.md +- https://github.com/dotnet/corefx/blob/master/Documentation/project-docs/developer-guide.md +- Run build.cmd from the root once: `PS D:\github\corefx> .\build.cmd` +- Build the individual projects: +- `PS D:\github\corefx\src\Common\tests> dotnet msbuild /t:rebuild` +- `PS D:\github\corefx\src\System.Net.Http\src> dotnet msbuild /t:rebuild` + +### Running CoreFx tests: +- `PS D:\github\corefx\src\Common\tests> dotnet msbuild /t:rebuildandtest` +- `PS D:\github\corefx\src\System.Net.Http\tests\UnitTests> dotnet msbuild /t:rebuildandtest` + +## Building 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 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` \ No newline at end of file diff --git a/src/Shared/Http2/SR.cs b/src/Shared/Http2/SR.cs new file mode 100644 index 0000000000..7dd7cc5991 --- /dev/null +++ b/src/Shared/Http2/SR.cs @@ -0,0 +1,20 @@ +// 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 System.Net.Http +{ + internal static partial class SR + { + // The resource generator used in AspNetCore does not create this method. This file fills in that functional gap + // so we don't have to modify the shared source. + internal static string Format(string resourceFormat, params object[] args) + { + if (args != null) + { + return string.Format(resourceFormat, args); + } + + return resourceFormat; + } + } +} diff --git a/src/Shared/Http2/SR.resx b/src/Shared/Http2/SR.resx new file mode 100644 index 0000000000..9e785e371f --- /dev/null +++ b/src/Shared/Http2/SR.resx @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The HTTP headers length exceeded the set limit of {0} bytes. + + + The header name format is invalid. + + + HPACK integer exceeds limits or has an overlong encoding. + + + Failed to HPACK encode the headers. + + + Huffman-coded literal string failed to decode. + + + Incomplete header block received. + + + Invalid header index: {0} is outside of static table and no dynamic table entry found. + + + A dynamic table size update of {0} octets is greater than the configured maximum size of {1} octets. + + + Dynamic table size update received after beginning of header block. + + + End of headers reached with incomplete token. + + + Received an invalid header name: '{0}'. + + + Request headers must contain only ASCII characters. + + \ No newline at end of file diff --git a/src/Shared/Shared.sln b/src/Shared/Shared.sln new file mode 100644 index 0000000000..b627de76e6 --- /dev/null +++ b/src/Shared/Shared.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 16 +VisualStudioVersion = 16.0.0.0 +MinimumVisualStudioVersion = 16.0.0.0 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Shared.Tests", "test\Shared.Tests\Microsoft.AspNetCore.Shared.Tests.csproj", "{06CD38EF-7733-4284-B3E4-825B6B63E1DD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {06CD38EF-7733-4284-B3E4-825B6B63E1DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06CD38EF-7733-4284-B3E4-825B6B63E1DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06CD38EF-7733-4284-B3E4-825B6B63E1DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06CD38EF-7733-4284-B3E4-825B6B63E1DD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9B7E5B1E-6E6D-4185-9088-2C7C779C6AB2} + EndGlobalSection +EndGlobal diff --git a/src/Shared/test/Shared.Tests/Http2/DynamicTableTest.cs b/src/Shared/test/Shared.Tests/Http2/DynamicTableTest.cs new file mode 100644 index 0000000000..fe0e4c7ec5 --- /dev/null +++ b/src/Shared/test/Shared.Tests/Http2/DynamicTableTest.cs @@ -0,0 +1,255 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http.HPack; +using System.Reflection; +using System.Text; +using Xunit; + +namespace System.Net.Http.Unit.Tests.HPack +{ + public class DynamicTableTest + { + private readonly HeaderField _header1 = new HeaderField(Encoding.ASCII.GetBytes("header-1"), Encoding.ASCII.GetBytes("value1")); + private readonly HeaderField _header2 = new HeaderField(Encoding.ASCII.GetBytes("header-02"), Encoding.ASCII.GetBytes("value_2")); + + [Fact] + public void DynamicTable_IsInitiallyEmpty() + { + DynamicTable dynamicTable = new DynamicTable(4096); + Assert.Equal(0, dynamicTable.Count); + Assert.Equal(0, dynamicTable.Size); + Assert.Equal(4096, dynamicTable.MaxSize); + } + + [Fact] + public void DynamicTable_Count_IsNumberOfEntriesInDynamicTable() + { + DynamicTable dynamicTable = new DynamicTable(4096); + + dynamicTable.Insert(_header1.Name, _header1.Value); + Assert.Equal(1, dynamicTable.Count); + + dynamicTable.Insert(_header2.Name, _header2.Value); + Assert.Equal(2, dynamicTable.Count); + } + + [Fact] + public void DynamicTable_Size_IsCurrentDynamicTableSize() + { + DynamicTable dynamicTable = new DynamicTable(4096); + Assert.Equal(0, dynamicTable.Size); + + dynamicTable.Insert(_header1.Name, _header1.Value); + Assert.Equal(_header1.Length, dynamicTable.Size); + + dynamicTable.Insert(_header2.Name, _header2.Value); + Assert.Equal(_header1.Length + _header2.Length, dynamicTable.Size); + } + + [Fact] + public void DynamicTable_FirstEntry_IsMostRecentEntry() + { + DynamicTable dynamicTable = new DynamicTable(4096); + dynamicTable.Insert(_header1.Name, _header1.Value); + dynamicTable.Insert(_header2.Name, _header2.Value); + + VerifyTableEntries(dynamicTable, _header2, _header1); + } + + [Fact] + public void BoundsCheck_ThrowsIndexOutOfRangeException() + { + DynamicTable dynamicTable = new DynamicTable(4096); + Assert.Throws(() => dynamicTable[0]); + + dynamicTable.Insert(_header1.Name, _header1.Value); + Assert.Throws(() => dynamicTable[1]); + } + + [Fact] + public void DynamicTable_InsertEntryLargerThanMaxSize_NoOp() + { + DynamicTable dynamicTable = new DynamicTable(_header1.Length - 1); + dynamicTable.Insert(_header1.Name, _header1.Value); + + Assert.Equal(0, dynamicTable.Count); + Assert.Equal(0, dynamicTable.Size); + } + + [Fact] + public void DynamicTable_InsertEntryLargerThanRemainingSpace_NoOp() + { + DynamicTable dynamicTable = new DynamicTable(_header1.Length); + dynamicTable.Insert(_header1.Name, _header1.Value); + + VerifyTableEntries(dynamicTable, _header1); + + dynamicTable.Insert(_header2.Name, _header2.Value); + + Assert.Equal(0, dynamicTable.Count); + Assert.Equal(0, dynamicTable.Size); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void DynamicTable_WrapsRingBuffer_Success(int targetInsertIndex) + { + FieldInfo insertIndexField = typeof(DynamicTable).GetField("_insertIndex", BindingFlags.NonPublic | BindingFlags.Instance); + DynamicTable table = new DynamicTable(maxSize: 256); + Stack insertedHeaders = new Stack(); + + // Insert into dynamic table until its insert index into its ring buffer loops back to 0. + do + { + InsertOne(); + } + while ((int)insertIndexField.GetValue(table) != 0); + + // Finally loop until the insert index reaches the target. + while ((int)insertIndexField.GetValue(table) != targetInsertIndex) + { + InsertOne(); + } + + void InsertOne() + { + byte[] data = Encoding.ASCII.GetBytes($"header-{insertedHeaders.Count}"); + + insertedHeaders.Push(data); + table.Insert(data, data); + } + + // Now check to see that we can retrieve the remaining headers. + // Some headers will have been evacuated from the table during this process, so we don't exhaust the entire insertedHeaders stack. + Assert.True(table.Count > 0); + Assert.True(table.Count < insertedHeaders.Count); + + for (int i = 0; i < table.Count; ++i) + { + HeaderField dynamicField = table[i]; + byte[] expectedData = insertedHeaders.Pop(); + + Assert.True(expectedData.AsSpan().SequenceEqual(dynamicField.Name)); + Assert.True(expectedData.AsSpan().SequenceEqual(dynamicField.Value)); + } + } + + [Theory] + [MemberData(nameof(CreateResizeData))] + public void DynamicTable_Resize_Success(int initialMaxSize, int finalMaxSize, int insertSize) + { + // This is purely to make it simple to perfectly reach our initial max size to test growing a full but non-wrapping buffer. + Debug.Assert((insertSize % 64) == 0, $"{nameof(insertSize)} must be a multiple of 64 ({nameof(HeaderField)}.{nameof(HeaderField.RfcOverhead)} * 2)"); + + DynamicTable dynamicTable = new DynamicTable(maxSize: initialMaxSize); + int insertedSize = 0; + + while (insertedSize != insertSize) + { + byte[] data = Encoding.ASCII.GetBytes($"header-{dynamicTable.Size}".PadRight(16, ' ')); + Debug.Assert(data.Length == 16); + + dynamicTable.Insert(data, data); + insertedSize += data.Length * 2 + HeaderField.RfcOverhead; + } + + List headers = new List(); + + for (int i = 0; i < dynamicTable.Count; ++i) + { + headers.Add(dynamicTable[i]); + } + + dynamicTable.Resize(finalMaxSize); + + int expectedCount = Math.Min(finalMaxSize / 64, headers.Count); + Assert.Equal(expectedCount, dynamicTable.Count); + + for (int i = 0; i < dynamicTable.Count; ++i) + { + Assert.True(headers[i].Name.AsSpan().SequenceEqual(dynamicTable[i].Name)); + Assert.True(headers[i].Value.AsSpan().SequenceEqual(dynamicTable[i].Value)); + } + } + + [Fact] + public void DynamicTable_ResizingEvictsOldestEntries() + { + DynamicTable dynamicTable = new DynamicTable(4096); + dynamicTable.Insert(_header1.Name, _header1.Value); + dynamicTable.Insert(_header2.Name, _header2.Value); + + VerifyTableEntries(dynamicTable, _header2, _header1); + + dynamicTable.Resize(_header2.Length); + + VerifyTableEntries(dynamicTable, _header2); + } + + [Fact] + public void DynamicTable_ResizingToZeroEvictsAllEntries() + { + DynamicTable dynamicTable = new DynamicTable(4096); + dynamicTable.Insert(_header1.Name, _header1.Value); + dynamicTable.Insert(_header2.Name, _header2.Value); + + dynamicTable.Resize(0); + + Assert.Equal(0, dynamicTable.Count); + Assert.Equal(0, dynamicTable.Size); + } + + [Fact] + public void DynamicTable_CanBeResizedToLargerMaxSize() + { + DynamicTable dynamicTable = new DynamicTable(_header1.Length); + dynamicTable.Insert(_header1.Name, _header1.Value); + dynamicTable.Insert(_header2.Name, _header2.Value); + + // _header2 is larger than _header1, so an attempt at inserting it + // would first clear the table then return without actually inserting it, + // given it is larger than the current max size. + Assert.Equal(0, dynamicTable.Count); + Assert.Equal(0, dynamicTable.Size); + + dynamicTable.Resize(dynamicTable.MaxSize + _header2.Length); + dynamicTable.Insert(_header2.Name, _header2.Value); + + VerifyTableEntries(dynamicTable, _header2); + } + + public static IEnumerable CreateResizeData() + { + int[] values = new[] { 128, 256, 384, 512 }; + return from initialMaxSize in values + from finalMaxSize in values + from insertSize in values + select new object[] { initialMaxSize, finalMaxSize, insertSize }; + } + + private void VerifyTableEntries(DynamicTable dynamicTable, params HeaderField[] entries) + { + Assert.Equal(entries.Length, dynamicTable.Count); + Assert.Equal(entries.Sum(e => e.Length), dynamicTable.Size); + + for (int i = 0; i < entries.Length; i++) + { + HeaderField headerField = dynamicTable[i]; + + Assert.NotSame(entries[i].Name, headerField.Name); + Assert.Equal(entries[i].Name, headerField.Name); + + Assert.NotSame(entries[i].Value, headerField.Value); + Assert.Equal(entries[i].Value, headerField.Value); + } + } + } +} diff --git a/src/Servers/Kestrel/Core/test/HPackDecoderTests.cs b/src/Shared/test/Shared.Tests/Http2/HPackDecoderTest.cs similarity index 78% rename from src/Servers/Kestrel/Core/test/HPackDecoderTests.cs rename to src/Shared/test/Shared.Tests/Http2/HPackDecoderTest.cs index d9318e01e9..01dd76c88b 100644 --- a/src/Servers/Kestrel/Core/test/HPackDecoderTests.cs +++ b/src/Shared/test/Shared.Tests/Http2/HPackDecoderTest.cs @@ -1,23 +1,20 @@ // 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 under the Apache License, Version 2.0. +// See THIRD-PARTY-NOTICES.TXT in the project root for license information. -using System; using System.Buffers; -using System.Collections.Generic; using System.Linq; +using System.Collections.Generic; using System.Text; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -using Microsoft.Net.Http.Headers; +using System.Net.Http.HPack; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +namespace System.Net.Http.Unit.Tests.HPack { public class HPackDecoderTests : IHttpHeadersHandler { private const int DynamicTableInitialMaxSize = 4096; - private const int MaxRequestHeaderFieldSize = 8192; + private const int MaxHeaderFieldSize = 8192; // Indexed Header Field Representation - Static Table - Index 2 (:method: GET) private static readonly byte[] _indexedHeaderStatic = new byte[] { 0x82 }; @@ -61,7 +58,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // v a l u e * // 11101110 00111010 00101101 00101111 - private static readonly byte[] _headerValueHuffmanBytes = new byte [] { 0xee, 0x3a, 0x2d, 0x2f }; + private static readonly byte[] _headerValueHuffmanBytes = new byte[] { 0xee, 0x3a, 0x2d, 0x2f }; private static readonly byte[] _headerName = new byte[] { (byte)_headerNameBytes.Length } .Concat(_headerNameBytes) @@ -95,21 +92,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public HPackDecoderTests() { _dynamicTable = new DynamicTable(DynamicTableInitialMaxSize); - _decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxRequestHeaderFieldSize, _dynamicTable); + _decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize, _dynamicTable); } - void IHttpHeadersHandler.OnHeader(Span name, Span value) + void IHttpHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan value) { - _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiStringNonNullCharacters(); + string headerName = Encoding.ASCII.GetString(name); + string headerValue = Encoding.ASCII.GetString(value); + + _decodedHeaders[headerName] = headerValue; } - void IHttpHeadersHandler.OnHeadersComplete() { } + void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } [Fact] public void DecodesIndexedHeaderField_StaticTable() { - _decoder.Decode(new ReadOnlySequence(_indexedHeaderStatic), endHeaders: true, handler: this); - Assert.Equal("GET", _decodedHeaders[HeaderNames.Method]); + _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this); + Assert.Equal("GET", _decodedHeaders[":method"]); } [Fact] @@ -119,23 +119,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _dynamicTable.Insert(_headerNameBytes, _headerValueBytes); // Index it - _decoder.Decode(new ReadOnlySequence(_indexedHeaderDynamic), endHeaders: true, handler: this); + _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this); Assert.Equal(_headerValueString, _decodedHeaders[_headerNameString]); } [Fact] public void DecodesIndexedHeaderField_OutOfRange_Error() { - var exception = Assert.Throws(() => - _decoder.Decode(new ReadOnlySequence(_indexedHeaderDynamic), endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message); + HPackDecodingException exception = Assert.Throws(() => + _decoder.Decode(_indexedHeaderDynamic, endHeaders: true, handler: this)); + Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName() { - var encoded = _literalHeaderFieldWithIndexingNewName + byte[] encoded = _literalHeaderFieldWithIndexingNewName .Concat(_headerName) .Concat(_headerValue) .ToArray(); @@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedName() { - var encoded = _literalHeaderFieldWithIndexingNewName + byte[] encoded = _literalHeaderFieldWithIndexingNewName .Concat(_headerNameHuffman) .Concat(_headerValue) .ToArray(); @@ -157,7 +157,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedValue() { - var encoded = _literalHeaderFieldWithIndexingNewName + byte[] encoded = _literalHeaderFieldWithIndexingNewName .Concat(_headerName) .Concat(_headerValueHuffman) .ToArray(); @@ -168,7 +168,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_NewName_HuffmanEncodedNameAndValue() { - var encoded = _literalHeaderFieldWithIndexingNewName + byte[] encoded = _literalHeaderFieldWithIndexingNewName .Concat(_headerNameHuffman) .Concat(_headerValueHuffman) .ToArray(); @@ -179,7 +179,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName() { - var encoded = _literalHeaderFieldWithIndexingIndexedName + byte[] encoded = _literalHeaderFieldWithIndexingIndexedName .Concat(_headerValue) .ToArray(); @@ -189,7 +189,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithIncrementalIndexing_IndexedName_HuffmanEncodedValue() { - var encoded = _literalHeaderFieldWithIndexingIndexedName + byte[] encoded = _literalHeaderFieldWithIndexingIndexedName .Concat(_headerValueHuffman) .ToArray(); @@ -203,15 +203,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // 11 1110 (Indexed Name - Index 62 encoded with 6-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw. - var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(new byte[] { 0x7e }), endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x7e }, endHeaders: true, handler: this)); + Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_NewName() { - var encoded = _literalHeaderFieldWithoutIndexingNewName + byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(_headerName) .Concat(_headerValue) .ToArray(); @@ -222,7 +222,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedName() { - var encoded = _literalHeaderFieldWithoutIndexingNewName + byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(_headerNameHuffman) .Concat(_headerValue) .ToArray(); @@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedValue() { - var encoded = _literalHeaderFieldWithoutIndexingNewName + byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(_headerName) .Concat(_headerValueHuffman) .ToArray(); @@ -244,7 +244,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_NewName_HuffmanEncodedNameAndValue() { - var encoded = _literalHeaderFieldWithoutIndexingNewName + byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(_headerNameHuffman) .Concat(_headerValueHuffman) .ToArray(); @@ -255,7 +255,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName() { - var encoded = _literalHeaderFieldWithoutIndexingIndexedName + byte[] encoded = _literalHeaderFieldWithoutIndexingIndexedName .Concat(_headerValue) .ToArray(); @@ -265,7 +265,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldWithoutIndexing_IndexedName_HuffmanEncodedValue() { - var encoded = _literalHeaderFieldWithoutIndexingIndexedName + byte[] encoded = _literalHeaderFieldWithoutIndexingIndexedName .Concat(_headerValueHuffman) .ToArray(); @@ -279,15 +279,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // 1111 0010 1111 (Indexed Name - Index 62 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw. - var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(new byte[] { 0x0f, 0x2f }), endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x0f, 0x2f }, endHeaders: true, handler: this)); + Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_NewName() { - var encoded = _literalHeaderFieldNeverIndexedNewName + byte[] encoded = _literalHeaderFieldNeverIndexedNewName .Concat(_headerName) .Concat(_headerValue) .ToArray(); @@ -298,7 +298,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedName() { - var encoded = _literalHeaderFieldNeverIndexedNewName + byte[] encoded = _literalHeaderFieldNeverIndexedNewName .Concat(_headerNameHuffman) .Concat(_headerValue) .ToArray(); @@ -309,7 +309,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedValue() { - var encoded = _literalHeaderFieldNeverIndexedNewName + byte[] encoded = _literalHeaderFieldNeverIndexedNewName .Concat(_headerName) .Concat(_headerValueHuffman) .ToArray(); @@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void DecodesLiteralHeaderFieldNeverIndexed_NewName_HuffmanEncodedNameAndValue() { - var encoded = _literalHeaderFieldNeverIndexedNewName + byte[] encoded = _literalHeaderFieldNeverIndexedNewName .Concat(_headerNameHuffman) .Concat(_headerValueHuffman) .ToArray(); @@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // 0001 (Literal Header Field Never Indexed Representation) // 1111 0010 1011 (Indexed Name - Index 58 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Concatenated with value bytes - var encoded = _literalHeaderFieldNeverIndexedIndexedName + byte[] encoded = _literalHeaderFieldNeverIndexedIndexedName .Concat(_headerValue) .ToArray(); @@ -347,7 +347,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // 0001 (Literal Header Field Never Indexed Representation) // 1111 0010 1011 (Indexed Name - Index 58 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Concatenated with Huffman encoded value bytes - var encoded = _literalHeaderFieldNeverIndexedIndexedName + byte[] encoded = _literalHeaderFieldNeverIndexedIndexedName .Concat(_headerValueHuffman) .ToArray(); @@ -361,8 +361,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // 1111 0010 1111 (Indexed Name - Index 62 encoded with 4-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) // Index 62 is the first entry in the dynamic table. If there's nothing there, the decoder should throw. - var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(new byte[] { 0x1f, 0x2f }), endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.FormatHPackErrorIndexOutOfRange(62), exception.Message); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(new byte[] { 0x1f, 0x2f }, endHeaders: true, handler: this)); + Assert.Equal(SR.Format(SR.net_http_hpack_invalid_index, 62), exception.Message); Assert.Empty(_decodedHeaders); } @@ -374,7 +374,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - _decoder.Decode(new ReadOnlySequence(new byte[] { 0x3e }), endHeaders: true, handler: this); + _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this); Assert.Equal(30, _dynamicTable.MaxSize); Assert.Empty(_decodedHeaders); @@ -388,9 +388,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - var data = new ReadOnlySequence(_indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray()); - var exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock, exception.Message); + byte[] data = _indexedHeaderStatic.Concat(new byte[] { 0x3e }).ToArray(); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); + Assert.Equal(SR.net_http_hpack_late_dynamic_table_size_update, exception.Message); } [Fact] @@ -398,14 +398,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - _decoder.Decode(new ReadOnlySequence(_indexedHeaderStatic), endHeaders: false, handler: this); - Assert.Equal("GET", _decodedHeaders[HeaderNames.Method]); + _decoder.Decode(_indexedHeaderStatic, endHeaders: false, handler: this); + Assert.Equal("GET", _decodedHeaders[":method"]); // 001 (Dynamic Table Size Update) // 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) - var data = new ReadOnlySequence(new byte[] { 0x3e }); - var exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.HPackErrorDynamicTableSizeUpdateNotAtBeginningOfHeaderBlock, exception.Message); + byte[] data = new byte[] { 0x3e }; + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(data, endHeaders: true, handler: this)); + Assert.Equal(SR.net_http_hpack_late_dynamic_table_size_update, exception.Message); } [Fact] @@ -413,12 +413,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - _decoder.Decode(new ReadOnlySequence(_indexedHeaderStatic), endHeaders: true, handler: this); - Assert.Equal("GET", _decodedHeaders[HeaderNames.Method]); + _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: this); + Assert.Equal("GET", _decodedHeaders[":method"]); // 001 (Dynamic Table Size Update) // 11110 (30 encoded with 5-bit prefix - see http://httpwg.org/specs/rfc7541.html#integer.representation) - _decoder.Decode(new ReadOnlySequence(new byte[] { 0x3e }), endHeaders: true, handler: this); + _decoder.Decode(new byte[] { 0x3e }, endHeaders: true, handler: this); Assert.Equal(30, _dynamicTable.MaxSize); } @@ -431,38 +431,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(DynamicTableInitialMaxSize, _dynamicTable.MaxSize); - var exception = Assert.Throws(() => - _decoder.Decode(new ReadOnlySequence(new byte[] { 0x3f, 0xe2, 0x1f }), endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.FormatHPackErrorDynamicTableSizeUpdateTooLarge(4097, DynamicTableInitialMaxSize), exception.Message); + HPackDecodingException exception = Assert.Throws(() => + _decoder.Decode(new byte[] { 0x3f, 0xe2, 0x1f }, endHeaders: true, handler: this)); + Assert.Equal(SR.Format(SR.net_http_hpack_large_table_size_update, 4097, DynamicTableInitialMaxSize), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesStringLength_GreaterThanLimit_Error() { - var encoded = _literalHeaderFieldWithoutIndexingNewName + byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(new byte[] { 0xff, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix .ToArray(); - var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(encoded), endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.FormatHPackStringLengthTooLarge(MaxRequestHeaderFieldSize + 1, MaxRequestHeaderFieldSize), exception.Message); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); + Assert.Equal(SR.Format(SR.net_http_headers_exceeded_length, MaxHeaderFieldSize), exception.Message); Assert.Empty(_decodedHeaders); } [Fact] public void DecodesStringLength_LimitConfigurable() { - var decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxRequestHeaderFieldSize + 1); - var string8193 = new string('a', MaxRequestHeaderFieldSize + 1); + HPackDecoder decoder = new HPackDecoder(DynamicTableInitialMaxSize, MaxHeaderFieldSize + 1); + string string8193 = new string('a', MaxHeaderFieldSize + 1); - var encoded = _literalHeaderFieldWithoutIndexingNewName + byte[] encoded = _literalHeaderFieldWithoutIndexingNewName .Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding .Concat(Encoding.ASCII.GetBytes(string8193)) .Concat(new byte[] { 0x7f, 0x82, 0x3f }) // 8193 encoded with 7-bit prefix, no Huffman encoding .Concat(Encoding.ASCII.GetBytes(string8193)) .ToArray(); - decoder.Decode(new ReadOnlySequence(encoded), endHeaders: true, handler: this); + decoder.Decode(encoded, endHeaders: true, handler: this); Assert.Equal(string8193, _decodedHeaders[string8193]); } @@ -552,8 +552,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(_incompleteHeaderBlockData))] public void DecodesIncompleteHeaderBlock_Error(byte[] encoded) { - var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(encoded), endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.HPackErrorIncompleteHeaderBlock, exception.Message); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); + Assert.Equal(SR.net_http_hpack_incomplete_header_block, exception.Message); Assert.Empty(_decodedHeaders); } @@ -586,8 +586,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(_huffmanDecodingErrorData))] public void WrapsHuffmanDecodingExceptionInHPackDecodingException(byte[] encoded) { - var exception = Assert.Throws(() => _decoder.Decode(new ReadOnlySequence(encoded), endHeaders: true, handler: this)); - Assert.Equal(CoreStrings.HPackHuffmanError, exception.Message); + HPackDecodingException exception = Assert.Throws(() => _decoder.Decode(encoded, endHeaders: true, handler: this)); + Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message); Assert.IsType(exception.InnerException); Assert.Empty(_decodedHeaders); } @@ -607,7 +607,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.Equal(0, _dynamicTable.Count); Assert.Equal(0, _dynamicTable.Size); - _decoder.Decode(new ReadOnlySequence(encoded), endHeaders: true, handler: this); + _decoder.Decode(encoded, endHeaders: true, handler: this); Assert.Equal(expectedHeaderValue, _decodedHeaders[expectedHeaderName]); diff --git a/src/Shared/test/Shared.Tests/Http2/HPackIntegerTest.cs b/src/Shared/test/Shared.Tests/Http2/HPackIntegerTest.cs new file mode 100644 index 0000000000..f7362aee4e --- /dev/null +++ b/src/Shared/test/Shared.Tests/Http2/HPackIntegerTest.cs @@ -0,0 +1,129 @@ +// 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.HPack; +using Xunit; + +namespace System.Net.Http.Unit.Tests.HPack +{ + public class HPackIntegerTest + { + [Theory] + [MemberData(nameof(IntegerCodecExactSamples))] + public void HPack_IntegerEncode(int value, int bits, byte[] expectedResult) + { + Span actualResult = new byte[64]; + bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten); + + Assert.True(success); + Assert.Equal(expectedResult.Length, bytesWritten); + Assert.True(actualResult.Slice(0, bytesWritten).SequenceEqual(expectedResult)); + } + + [Theory] + [MemberData(nameof(IntegerCodecExactSamples))] + public void HPack_IntegerEncode_ShortBuffer(int value, int bits, byte[] expectedResult) + { + Span actualResult = new byte[expectedResult.Length - 1]; + bool success = IntegerEncoder.Encode(value, bits, actualResult, out int bytesWritten); + + Assert.False(success); + } + + [Theory] + [MemberData(nameof(IntegerCodecExactSamples))] + public void HPack_IntegerDecode(int expectedResult, int bits, byte[] encoded) + { + IntegerDecoder integerDecoder = new IntegerDecoder(); + + bool finished = integerDecoder.BeginTryDecode(encoded[0], bits, out int actualResult); + + int i = 1; + for (; !finished && i < encoded.Length; ++i) + { + finished = integerDecoder.TryDecode(encoded[i], out actualResult); + } + + Assert.True(finished); + Assert.Equal(encoded.Length, i); + + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void IntegerEncoderDecoderRoundtrips() + { + IntegerDecoder decoder = new IntegerDecoder(); + + for (int i = 0; i < 2048; ++i) + { + for (int prefixLength = 1; prefixLength <= 8; ++prefixLength) + { + Span integerBytes = stackalloc byte[5]; + Assert.True(IntegerEncoder.Encode(i, prefixLength, integerBytes, out int length)); + + bool decodeResult = decoder.BeginTryDecode(integerBytes[0], prefixLength, out int intResult); + + for (int j = 1; j < length; j++) + { + Assert.False(decodeResult); + decodeResult = decoder.TryDecode(integerBytes[j], out intResult); + } + + Assert.True(decodeResult); + Assert.Equal(i, intResult); + } + } + } + + public static IEnumerable IntegerCodecExactSamples() + { + yield return new object[] { 10, 5, new byte[] { 0x0A } }; + yield return new object[] { 1337, 5, new byte[] { 0x1F, 0x9A, 0x0A } }; + yield return new object[] { 42, 8, new byte[] { 0x2A } }; + yield return new object[] { 7, 3, new byte[] { 0x7, 0x0 } }; + yield return new object[] { int.MaxValue, 1, new byte[] { 0x01, 0xfe, 0xff, 0xff, 0xff, 0x07 } }; + yield return new object[] { int.MaxValue, 8, new byte[] { 0xff, 0x80, 0xfe, 0xff, 0xff, 0x07 } }; + } + + [Theory] + [MemberData(nameof(IntegerData_OverMax))] + public void IntegerDecode_Throws_IfMaxExceeded(int prefixLength, byte[] octets) + { + var decoder = new IntegerDecoder(); + var result = decoder.BeginTryDecode(octets[0], prefixLength, out var intResult); + + for (var j = 1; j < octets.Length - 1; j++) + { + Assert.False(decoder.TryDecode(octets[j], out intResult)); + } + + Assert.Throws(() => decoder.TryDecode(octets[octets.Length - 1], out intResult)); + } + + public static TheoryData IntegerData_OverMax + { + get + { + var data = new TheoryData(); + + data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1 + data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x08 }); // MSB exceeds maximum + data.Add(1, new byte[] { 0x01, 0xff, 0xff, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set + + data.Add(7, new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x08 }); // 1 bit too large + data.Add(7, new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F }); + data.Add(7, new byte[] { 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80 }); // A continuation byte (0x80) where the byte after it would be too large. + data.Add(7, new byte[] { 0x7F, 0xFF, 0x00 }); // Encoded with 1 byte too many. + + data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x07 }); // Int32.MaxValue + 1 + data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x08 }); // MSB exceeds maximum + data.Add(8, new byte[] { 0xff, 0x81, 0xfe, 0xff, 0xff, 0x80 }); // Undefined since continuation bit set + + return data; + } + } + } +} diff --git a/src/Servers/Kestrel/Core/test/HuffmanTests.cs b/src/Shared/test/Shared.Tests/Http2/HuffmanDecodingTests.cs similarity index 72% rename from src/Servers/Kestrel/Core/test/HuffmanTests.cs rename to src/Shared/test/Shared.Tests/Http2/HuffmanDecodingTests.cs index dfae6afe6e..4383de415b 100644 --- a/src/Servers/Kestrel/Core/test/HuffmanTests.cs +++ b/src/Shared/test/Shared.Tests/Http2/HuffmanDecodingTests.cs @@ -1,15 +1,193 @@ -// 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.Collections.Generic; +using System.Linq; +using System.Net.Http.HPack; using System.Text; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; using Xunit; -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +namespace System.Net.Http.Unit.Tests.HPack { - public class HuffmanTests + public class HuffmanDecodingTests { + // Encoded values are 30 bits at most, so are stored in the table in a uint. + // Convert to ulong here and put the encoded value in the most significant bits. + // This makes the encoding logic below simpler. + private static (ulong code, int bitLength) GetEncodedValue(byte b) + { + (uint code, int bitLength) = Huffman.Encode(b); + return (((ulong)code) << 32, bitLength); + } + + private static int Encode(byte[] source, byte[] destination, bool injectEOS) + { + ulong currentBits = 0; // We can have 7 bits of rollover plus 30 bits for the next encoded value, so use a ulong + int currentBitCount = 0; + int dstOffset = 0; + + for (int i = 0; i < source.Length; i++) + { + (ulong code, int bitLength) = GetEncodedValue(source[i]); + + // inject EOS if instructed to + if (injectEOS) + { + code |= (ulong)0b11111111_11111111_11111111_11111100 << (32 - bitLength); + bitLength += 30; + injectEOS = false; + } + + currentBits |= code >> currentBitCount; + currentBitCount += bitLength; + + while (currentBitCount >= 8) + { + destination[dstOffset++] = (byte)(currentBits >> 56); + currentBits = currentBits << 8; + currentBitCount -= 8; + } + } + + // Fill any trailing bits with ones, per RFC + if (currentBitCount > 0) + { + currentBits |= 0xFFFFFFFFFFFFFFFF >> currentBitCount; + destination[dstOffset++] = (byte)(currentBits >> 56); + } + + return dstOffset; + } + + [Fact] + public void HuffmanDecoding_ValidEncoding_Succeeds() + { + foreach (byte[] input in TestData()) + { + // Worst case encoding is 30 bits per input byte, so make the encoded buffer 4 times as big + byte[] encoded = new byte[input.Length * 4]; + int encodedByteCount = Encode(input, encoded, false); + + // Worst case decoding is an output byte per 5 input bits, so make the decoded buffer 2 times as big + byte[] decoded = new byte[encoded.Length * 2]; + + int decodedByteCount = Huffman.Decode(new ReadOnlySpan(encoded, 0, encodedByteCount), ref decoded); + + Assert.Equal(input.Length, decodedByteCount); + Assert.Equal(input, decoded.Take(decodedByteCount)); + } + } + + [Fact] + public void HuffmanDecoding_InvalidEncoding_Throws() + { + foreach (byte[] encoded in InvalidEncodingData()) + { + // Worst case decoding is an output byte per 5 input bits, so make the decoded buffer 2 times as big + byte[] decoded = new byte[encoded.Length * 2]; + + Assert.Throws(() => Huffman.Decode(encoded, ref decoded)); + } + } + + // This input sequence will encode to 17 bits, thus offsetting the next character to encode + // by exactly one bit. We use this below to generate a prefix that encodes all of the possible starting + // bit offsets for a character, from 0 to 7. + private static readonly byte[] s_offsetByOneBit = new byte[] { (byte)'c', (byte)'l', (byte)'r' }; + + public static IEnumerable TestData() + { + // Single byte data + for (int i = 0; i < 256; i++) + { + yield return new byte[] { (byte)i }; + } + + // Ensure that decoding every possible value leaves the decoder in a correct state so that + // a subsequent value can be decoded (here, 'a') + for (int i = 0; i < 256; i++) + { + yield return new byte[] { (byte)i, (byte)'a' }; + } + + // Ensure that every possible bit starting position for every value is encoded properly + // s_offsetByOneBit encodes to exactly 17 bits, leaving 1 bit for the next byte + // So by repeating this sequence, we can generate any starting bit position we want. + byte[] currentPrefix = new byte[0]; + for (int prefixBits = 1; prefixBits <= 8; prefixBits++) + { + currentPrefix = currentPrefix.Concat(s_offsetByOneBit).ToArray(); + + // Make sure we're actually getting the correct number of prefix bits + int encodedBits = currentPrefix.Select(b => Huffman.Encode(b).bitLength).Sum(); + Assert.Equal(prefixBits % 8, encodedBits % 8); + + for (int i = 0; i < 256; i++) + { + yield return currentPrefix.Concat(new byte[] { (byte)i }.Concat(currentPrefix)).ToArray(); + } + } + + // Finally, one really big chunk of randomly generated data. + byte[] data = new byte[1024 * 1024]; + new Random(42).NextBytes(data); + yield return data; + } + + private static IEnumerable InvalidEncodingData() + { + // For encodings greater than 8 bits, truncate one or more bytes to generate an invalid encoding + byte[] source = new byte[1]; + byte[] destination = new byte[10]; + for (int i = 0; i < 256; i++) + { + source[0] = (byte)i; + int encodedByteCount = Encode(source, destination, false); + if (encodedByteCount > 1) + { + yield return destination.Take(encodedByteCount - 1).ToArray(); + if (encodedByteCount > 2) + { + yield return destination.Take(encodedByteCount - 2).ToArray(); + if (encodedByteCount > 3) + { + yield return destination.Take(encodedByteCount - 3).ToArray(); + } + } + } + } + + // Pad encodings with invalid trailing one bits. This is disallowed. + byte[] pad1 = new byte[] { 0xFF }; + byte[] pad2 = new byte[] { 0xFF, 0xFF, }; + byte[] pad3 = new byte[] { 0xFF, 0xFF, 0xFF }; + byte[] pad4 = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; + + for (int i = 0; i < 256; i++) + { + source[0] = (byte)i; + int encodedByteCount = Encode(source, destination, false); + yield return destination.Take(encodedByteCount).Concat(pad1).ToArray(); + yield return destination.Take(encodedByteCount).Concat(pad2).ToArray(); + yield return destination.Take(encodedByteCount).Concat(pad3).ToArray(); + yield return destination.Take(encodedByteCount).Concat(pad4).ToArray(); + } + + // send single EOS + yield return new byte[] { 0b11111111, 0b11111111, 0b11111111, 0b11111100 }; + + // send combinations with EOS in the middle + source = new byte[2]; + destination = new byte[24]; + for (int i = 0; i < 256; i++) + { + source[0] = source[1] = (byte)i; + int encodedByteCount = Encode(source, destination, true); + yield return destination.Take(encodedByteCount).ToArray(); + } + } + public static readonly TheoryData _validData = new TheoryData { // Single 5-bit symbol @@ -67,8 +245,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(_validData))] public void HuffmanDecodeArray(byte[] encoded, byte[] expected) { - var dst = new byte[expected.Length]; - Assert.Equal(expected.Length, Huffman.Decode(new ReadOnlySpan(encoded), dst)); + byte[] dst = new byte[expected.Length]; + Assert.Equal(expected.Length, Huffman.Decode(new ReadOnlySpan(encoded), ref dst)); Assert.Equal(expected, dst); } @@ -88,8 +266,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(_longPaddingData))] public void ThrowsOnPaddingLongerThanSevenBits(byte[] encoded) { - var exception = Assert.Throws(() => Huffman.Decode(new ReadOnlySpan(encoded), new byte[encoded.Length * 2])); - Assert.Equal(CoreStrings.HPackHuffmanErrorIncomplete, exception.Message); + byte[] dst = new byte[encoded.Length * 2]; + Exception exception = Assert.Throws(() => Huffman.Decode(new ReadOnlySpan(encoded), ref dst)); + Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message); } public static readonly TheoryData _eosData = new TheoryData @@ -104,17 +283,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(_eosData))] public void ThrowsOnEOS(byte[] encoded) { - var exception = Assert.Throws(() => Huffman.Decode(new ReadOnlySpan(encoded), new byte[encoded.Length * 2])); - Assert.Equal(CoreStrings.HPackHuffmanErrorEOS, exception.Message); + byte[] dst = new byte[encoded.Length * 2]; + Exception exception = Assert.Throws(() => Huffman.Decode(new ReadOnlySpan(encoded), ref dst)); + Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message); } [Fact] - public void ThrowsOnDestinationBufferTooSmall() + public void ResizesOnDestinationBufferTooSmall() { // h e l l o * - var encoded = new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 }; - var exception = Assert.Throws(() => Huffman.Decode(new ReadOnlySpan(encoded), new byte[encoded.Length])); - Assert.Equal(CoreStrings.HPackHuffmanErrorDestinationTooSmall, exception.Message); + byte[] encoded = new byte[] { 0b100111_00, 0b101_10100, 0b0_101000_0, 0b0111_1111 }; + byte[] originalDestination = new byte[encoded.Length]; + byte[] actualDestination = originalDestination; + int decodedCount = Huffman.Decode(new ReadOnlySpan(encoded), ref actualDestination); + Assert.Equal(5, decodedCount); + Assert.NotSame(originalDestination, actualDestination); } public static readonly TheoryData _incompleteSymbolData = new TheoryData @@ -145,28 +328,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(_incompleteSymbolData))] public void ThrowsOnIncompleteSymbol(byte[] encoded) { - var exception = Assert.Throws(() => Huffman.Decode(new ReadOnlySpan(encoded), new byte[encoded.Length * 2])); - Assert.Equal(CoreStrings.HPackHuffmanErrorIncomplete, exception.Message); + byte[] dst = new byte[encoded.Length * 2]; + Exception exception = Assert.Throws(() => Huffman.Decode(new ReadOnlySpan(encoded), ref dst)); + Assert.Equal(SR.net_http_hpack_huffman_decode_failed, exception.Message); } [Fact] public void DecodeCharactersThatSpans5Octets() { - var expectedLength = 2; - var decodedBytes = new byte[expectedLength]; + int expectedLength = 2; + byte[] decodedBytes = new byte[expectedLength]; // B LF EOS - var encoded = new byte[] { 0b1011101_1, 0b11111111, 0b11111111, 0b11111111, 0b11100_111 }; - var decodedLength = Huffman.Decode(new ReadOnlySpan(encoded, 0, encoded.Length), decodedBytes); + byte[] encoded = new byte[] { 0b1011101_1, 0b11111111, 0b11111111, 0b11111111, 0b11100_111 }; + int decodedLength = Huffman.Decode(new ReadOnlySpan(encoded, 0, encoded.Length), ref decodedBytes); Assert.Equal(expectedLength, decodedLength); - Assert.Equal(new byte [] { (byte)'B', (byte)'\n' }, decodedBytes); + Assert.Equal(new byte[] { (byte)'B', (byte)'\n' }, decodedBytes); } [Theory] [MemberData(nameof(HuffmanData))] public void HuffmanEncode(int code, uint expectedEncoded, int expectedBitLength) { - var (encoded, bitLength) = Huffman.Encode(code); + (uint encoded, int bitLength) = Huffman.Encode(code); Assert.Equal(expectedEncoded, encoded); Assert.Equal(expectedBitLength, bitLength); } @@ -175,7 +359,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(HuffmanData))] public void HuffmanDecode(int code, uint encoded, int bitLength) { - Assert.Equal(code, Huffman.DecodeValue(encoded, bitLength, out var decodedBits)); + Assert.Equal(code, Huffman.DecodeValue(encoded, bitLength, out int decodedBits)); Assert.Equal(bitLength, decodedBits); } @@ -183,14 +367,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [MemberData(nameof(HuffmanData))] public void HuffmanEncodeDecode( int code, -// Suppresses the warning about an unused theory parameter because -// this test shares data with other methods + // Suppresses the warning about an unused theory parameter because + // this test shares data with other methods #pragma warning disable xUnit1026 uint encoded, #pragma warning restore xUnit1026 int bitLength) { - Assert.Equal(code, Huffman.DecodeValue(Huffman.Encode(code).encoded, bitLength, out var decodedBits)); + Assert.Equal(code, Huffman.DecodeValue(Huffman.Encode(code).encoded, bitLength, out int decodedBits)); Assert.Equal(bitLength, decodedBits); } @@ -198,7 +382,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { get { - var data = new TheoryData(); + TheoryData data = new TheoryData(); data.Add(0, 0b11111111_11000000_00000000_00000000, 13); data.Add(1, 0b11111111_11111111_10110000_00000000, 23); diff --git a/src/Shared/test/Shared.Tests/Http2/ReadMe.SharedCode.md b/src/Shared/test/Shared.Tests/Http2/ReadMe.SharedCode.md new file mode 100644 index 0000000000..e023a0930f --- /dev/null +++ b/src/Shared/test/Shared.Tests/Http2/ReadMe.SharedCode.md @@ -0,0 +1,3 @@ +The code in this directory is shared between CoreFx and AspNetCore. This contains tests for HTTP/2 protocol infrastructure such as HPACK. Any changes to this dir need to be checked into both repositories. + +See corefx\src\Common\src\System\Net\Http\Http2\ReadMe.SharedCode.md or AspNetCore\src\Shared\Http2\ReadMe.SharedCode.md for additional details. \ No newline at end of file diff --git a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj index 83fe4babb7..1e41510bfb 100644 --- a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -7,15 +7,16 @@ - - - - - - - - - + + + + + + + + + + @@ -30,6 +31,10 @@ + + System.Net.Http.SR + +