From dc3869b00b55eeded6640d6daec414fe38929abf Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 22 Mar 2020 23:11:58 +1300 Subject: [PATCH 01/14] Add HPack dynamic compression --- ...pNetCore.Server.Kestrel.Core.netcoreapp.cs | 1 + src/Servers/Kestrel/Core/src/CoreStrings.resx | 3 + src/Servers/Kestrel/Core/src/Http2Limits.cs | 8 +- .../Internal/Http2/HPack/HPackHeaderEntry.cs | 74 +++ .../Internal/Http2/HPack/Http2HPackEncoder.cs | 407 +++++++++++++++ .../src/Internal/Http2/HPack/StatusCodes.cs | 151 ++++++ .../src/Internal/Http2/HPackHeaderWriter.cs | 157 ------ .../src/Internal/Http2/Http2Connection.cs | 11 +- .../src/Internal/Http2/Http2FrameWriter.cs | 24 +- .../Internal/Http2/Http2HeaderEnumerator.cs | 75 ++- .../Kestrel/Core/src/KestrelServerOptions.cs | 8 + ...soft.AspNetCore.Server.Kestrel.Core.csproj | 6 +- .../Core/test/HPackHeaderWriterTests.cs | 398 +++++++-------- .../Core/test/Http2FrameWriterTests.cs | 14 +- .../Core/test/Http2HPackEncoderTests.cs | 480 ++++++++++++++++++ .../Core/test/KestrelServerLimitsTests.cs | 3 +- ...ark.cs => Http2ConnectionBenchmarkBase.cs} | 19 +- .../Http2ConnectionEmptyBenchmark.cs | 29 ++ .../Http2ConnectionHeadersBenchmark.cs | 48 ++ .../Http2FrameWriterBenchmark.cs | 2 +- .../test/PipeWriterHttp2FrameExtensions.cs | 7 +- .../Http2/Http2ConnectionTests.cs | 278 +++++++--- .../Http2/Http2StreamTests.cs | 178 +++---- .../Http2/Http2TestBase.cs | 17 +- .../Http2/Http2TimeoutTests.cs | 32 +- .../HttpClientHttp2InteropTests.cs | 2 +- .../runtime/Http2/Hpack/HPackEncoder.cs | 132 ++++- 27 files changed, 1971 insertions(+), 593 deletions(-) create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackHeaderEntry.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Http2HPackEncoder.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs create mode 100644 src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs rename src/Servers/Kestrel/perf/Kestrel.Performance/{Http2ConnectionBenchmark.cs => Http2ConnectionBenchmarkBase.cs} (92%) create mode 100644 src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs create mode 100644 src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs 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 6673afaa1cc1..d242bfc4dfa9 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 @@ -130,6 +130,7 @@ public KestrelServerOptions() { } public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public bool DisableResponseDynamicHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public bool EnableAltSvc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx index f84ed1d2cef1..1d270be8ee2d 100644 --- a/src/Servers/Kestrel/Core/src/CoreStrings.resx +++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx @@ -599,4 +599,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l Unable to resolve service for type 'Microsoft.AspNetCore.Connections.IConnectionListenerFactory' while attempting to activate 'Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer'. + + A value greater than or equal to zero is required. + \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Http2Limits.cs b/src/Servers/Kestrel/Core/src/Http2Limits.cs index 68d101f07647..713159f66a66 100644 --- a/src/Servers/Kestrel/Core/src/Http2Limits.cs +++ b/src/Servers/Kestrel/Core/src/Http2Limits.cs @@ -39,9 +39,9 @@ public int MaxStreamsPerConnection } /// - /// Limits the size of the header compression table, in octets, the HPACK decoder on the server can use. + /// Limits the size of the header compression tables, in octets, the HPACK encoder and decoder on the server can use. /// - /// Value must be greater than 0, defaults to 4096 + /// Value must be greater than or equal to 0, defaults to 4096 /// /// public int HeaderTableSize @@ -49,9 +49,9 @@ public int HeaderTableSize get => _headerTableSize; set { - if (value <= 0) + if (value < 0) { - throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanZeroRequired); + throw new ArgumentOutOfRangeException(nameof(value), value, CoreStrings.GreaterThanOrEqualToZeroRequired); } _headerTableSize = value; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackHeaderEntry.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackHeaderEntry.cs new file mode 100644 index 000000000000..f6e39e4f788e --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackHeaderEntry.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Net.Http.HPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack +{ + [DebuggerDisplay("Name = {Name} Value = {Value}")] + internal class HPackHeaderEntry + { + // Header name and value + public string Name; + public string Value; + + // Chained list of headers in the same bucket + public HPackHeaderEntry Next; + public int Hash; + + // Compute dynamic table index + public int Index; + + // Doubly linked list + public HPackHeaderEntry Before; + public HPackHeaderEntry After; + + /// + /// Initialize header values. An entry will be reinitialized when reused. + /// + public void Initialize(int hash, string name, string value, int index, HPackHeaderEntry next) + { + Debug.Assert(name != null); + Debug.Assert(value != null); + + Name = name; + Value = value; + Index = index; + Hash = hash; + Next = next; + } + + public uint CalculateSize() + { + return (uint)HeaderField.GetLength(Name.Length, Value.Length); + } + + /// + /// Remove entry from the linked list and reset header values. + /// + public void Remove() + { + Before.After = After; + After.Before = Before; + Before = null; + After = null; + Next = null; + Hash = 0; + Name = null; + Value = null; + } + + /// + /// Add before an entry in the linked list. + /// + public void AddBefore(HPackHeaderEntry existingEntry) + { + After = existingEntry; + Before = existingEntry.Before; + Before.After = this; + After.Before = this; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Http2HPackEncoder.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Http2HPackEncoder.cs new file mode 100644 index 000000000000..e07392af0da0 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Http2HPackEncoder.cs @@ -0,0 +1,407 @@ +// 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.Diagnostics; +using System.Net.Http; +using System.Net.Http.HPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack +{ + internal class Http2HPackEncoder + { + // Internal for testing + internal readonly HPackHeaderEntry Head; + + private readonly bool _disableDynamicCompression; + private readonly HPackHeaderEntry[] _headerBuckets; + private readonly byte _hashMask; + private uint _headerTableSize; + private uint _maxHeaderTableSize; + private bool _pendingTableSizeUpdate; + private HPackHeaderEntry _removed; + + public Http2HPackEncoder(bool disableDynamicCompression = false, uint maxHeaderTableSize = Http2PeerSettings.DefaultHeaderTableSize) + { + _disableDynamicCompression = disableDynamicCompression; + _maxHeaderTableSize = maxHeaderTableSize; + Head = new HPackHeaderEntry(); + Head.Initialize(-1, string.Empty, string.Empty, int.MaxValue, null); + // Bucket count balances memory usage and the expected low number of headers (constrained by the header table size). + // Performance with different bucket counts hasn't been measured in detail. + _headerBuckets = new HPackHeaderEntry[16]; + _hashMask = (byte)(_headerBuckets.Length - 1); + Head.Before = Head.After = Head; + } + + public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) + { + if (_maxHeaderTableSize != maxHeaderTableSize) + { + _maxHeaderTableSize = maxHeaderTableSize; + _pendingTableSizeUpdate = true; + + // Check capacity and remove entries that exceed the new capacity + EnsureCapacity(0); + } + } + + /// + /// Begin encoding headers in the first HEADERS frame. + /// + public bool BeginEncodeHeaders(int statusCode, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + { + length = 0; + + if (_pendingTableSizeUpdate) + { + if (!HPackEncoder.EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out var sizeUpdateLength)) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + length += sizeUpdateLength; + _pendingTableSizeUpdate = false; + } + + if (!EncodeStatusHeader(statusCode, buffer.Slice(length), out var statusCodeLength)) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + length += statusCodeLength; + + if (!headersEnumerator.MoveNext()) + { + return true; + } + + // We're ok with not throwing if no headers were encoded because we've already encoded the status. + // There is a small chance that the header will encode if there is no other content in the next HEADERS frame. + var done = EncodeHeadersCore(headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: false, out var headersLength); + length += headersLength; + return done; + } + + /// + /// Begin encoding headers in the first HEADERS frame. + /// + public bool BeginEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + { + length = 0; + + if (_pendingTableSizeUpdate) + { + if (!HPackEncoder.EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out var sizeUpdateLength)) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + length += sizeUpdateLength; + _pendingTableSizeUpdate = false; + } + + if (!headersEnumerator.MoveNext()) + { + return true; + } + + var done = EncodeHeadersCore(headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: true, out var headersLength); + length += headersLength; + return done; + } + + /// + /// Continue encoding headers in the next HEADERS frame. The enumerator should already have a current value. + /// + public bool ContinueEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + { + return EncodeHeadersCore(headersEnumerator, buffer, throwIfNoneEncoded: true, out length); + } + + private bool EncodeStatusHeader(int statusCode, Span buffer, out int length) + { + switch (statusCode) + { + case 200: + case 204: + case 206: + case 304: + case 400: + case 404: + case 500: + // Status codes which exist in the HTTP/2 StaticTable. + return HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], buffer, out length); + default: + const string name = ":status"; + var value = StatusCodes.ToStatusString(statusCode); + return EncodeHeader(buffer, H2StaticTable.Status200, name, value, out length); + } + } + + private bool EncodeHeadersCore(Http2HeadersEnumerator headersEnumerator, Span buffer, bool throwIfNoneEncoded, out int length) + { + var currentLength = 0; + do + { + if (!EncodeHeader( + buffer.Slice(currentLength), + headersEnumerator.HPackStaticTableId, + headersEnumerator.Current.Key, + headersEnumerator.Current.Value, + out var headerLength)) + { + // If the header wasn't written, and no headers have been written, then the header is too large. + // Throw an error to avoid an infinite loop of attempting to write large header. + if (currentLength == 0 && throwIfNoneEncoded) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + + length = currentLength; + return false; + } + + currentLength += headerLength; + } + while (headersEnumerator.MoveNext()); + + length = currentLength; + return true; + } + + private bool EncodeHeader(Span buffer, int staticTableIndex, string name, string value, out int bytesWritten) + { + // Never index sensitive value. + if (IsSensitive(staticTableIndex, name)) + { + var index = ResolveDynamicTableIndex(staticTableIndex, name); + + return index == -1 + ? HPackEncoder.EncodeLiteralHeaderFieldNeverIndexingNewName(name, value, buffer, out bytesWritten) + : HPackEncoder.EncodeLiteralHeaderFieldNeverIndexing(index, value, buffer, out bytesWritten); + } + + // No dynamic table. Only use the static table. + if (_disableDynamicCompression || _maxHeaderTableSize == 0 || IsNotDynamicallyIndexed(staticTableIndex)) + { + return staticTableIndex == -1 + ? HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) + : HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(staticTableIndex, value, buffer, out bytesWritten); + } + + // Header is greater than the maximum table size. + // Don't attempt to add dynamic header as all existing dynamic headers will be removed. + if (HeaderField.GetLength(name.Length, value.Length) > _maxHeaderTableSize) + { + var index = ResolveDynamicTableIndex(staticTableIndex, name); + + return index == -1 + ? HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) + : HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(index, value, buffer, out bytesWritten); + } + + return EncodeDynamicHeader(buffer, staticTableIndex, name, value, out bytesWritten); + } + + private bool IsSensitive(int staticTableIndex, string name) + { + // Set-Cookie could contain sensitive data. + if (staticTableIndex == H2StaticTable.SetCookie) + { + return true; + } + if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + private bool IsNotDynamicallyIndexed(int staticTableIndex) + { + // Content-Length is added to static content. Content length is different for each + // file, and is unlikely to be reused because of browser caching. + return staticTableIndex == H2StaticTable.ContentLength; + } + + private int ResolveDynamicTableIndex(int staticTableIndex, string name) + { + if (staticTableIndex != -1) + { + // Prefer static table index. + return staticTableIndex; + } + + return CalculateDynamicTableIndex(name); + } + + private bool EncodeDynamicHeader(Span buffer, int staticTableIndex, string name, string value, out int bytesWritten) + { + var headerField = GetEntry(name, value); + if (headerField != null) + { + // Already exists in dynamic table. Write index. + var index = CalculateDynamicTableIndex(headerField.Index); + return HPackEncoder.EncodeIndexedHeaderField(index, buffer, out bytesWritten); + } + else + { + // Doesn't exist in dynamic table. Add new entry to dynamic table. + var headerSize = (uint)HeaderField.GetLength(name.Length, value.Length); + + var index = ResolveDynamicTableIndex(staticTableIndex, name); + var success = index == -1 + ? HPackEncoder.EncodeLiteralHeaderFieldIndexingNewName(name, value, buffer, out bytesWritten) + : HPackEncoder.EncodeLiteralHeaderFieldIndexing(index, value, buffer, out bytesWritten); + + if (success) + { + EnsureCapacity(headerSize); + AddHeaderEntry(name, value, headerSize); + } + + return success; + } + } + + /// + /// Ensure there is capacity for the new header. If there is not enough capacity then remove + /// existing headers until space is available. + /// + private void EnsureCapacity(uint headerSize) + { + Debug.Assert(headerSize <= _maxHeaderTableSize, "Header is bigger than dynamic table size."); + + while (_maxHeaderTableSize - _headerTableSize < headerSize) + { + var removed = RemoveHeaderEntry(); + // Removed entries are tracked to be reused. + PushRemovedEntry(removed); + } + } + + private HPackHeaderEntry GetEntry(string name, string value) + { + if (_headerTableSize == 0) + { + return null; + } + var hash = name.GetHashCode(); + var bucketIndex = CalculateBucketIndex(hash); + for (var e = _headerBuckets[bucketIndex]; e != null; e = e.Next) + { + // We've already looked up entries based on a hash of the name. + // Compare value before name as it is more likely to be different. + if (e.Hash == hash && + string.Equals(value, e.Value, StringComparison.Ordinal) && + string.Equals(name, e.Name, StringComparison.Ordinal)) + { + return e; + } + } + return null; + } + + private int CalculateDynamicTableIndex(string name) + { + if (_headerTableSize == 0) + { + return -1; + } + var hash = name.GetHashCode(); + var bucketIndex = CalculateBucketIndex(hash); + for (var e = _headerBuckets[bucketIndex]; e != null; e = e.Next) + { + if (e.Hash == hash && string.Equals(name, e.Name, StringComparison.Ordinal)) + { + return CalculateDynamicTableIndex(e.Index); + } + } + return -1; + } + + private int CalculateDynamicTableIndex(int index) + { + return index == -1 ? -1 : index - Head.Before.Index + 1 + H2StaticTable.Count; + } + + private void AddHeaderEntry(string name, string value, uint headerSize) + { + Debug.Assert(headerSize <= _maxHeaderTableSize, "Header is bigger than dynamic table size."); + Debug.Assert(headerSize <= _maxHeaderTableSize - _headerTableSize, "Not enough room in dynamic table."); + + var hash = name.GetHashCode(); + var bucketIndex = CalculateBucketIndex(hash); + var oldEntry = _headerBuckets[bucketIndex]; + // Attempt to reuse removed entry + var newEntry = PopRemovedEntry() ?? new HPackHeaderEntry(); + newEntry.Initialize(hash, name, value, Head.Before.Index - 1, oldEntry); + _headerBuckets[bucketIndex] = newEntry; + newEntry.AddBefore(Head); + _headerTableSize += headerSize; + } + + private void PushRemovedEntry(HPackHeaderEntry removed) + { + if (_removed != null) + { + removed.Next = _removed; + } + _removed = removed; + } + + private HPackHeaderEntry PopRemovedEntry() + { + if (_removed != null) + { + var removed = _removed; + _removed = _removed.Next; + return removed; + } + + return null; + } + + /// + /// Remove the oldest entry. + /// + private HPackHeaderEntry RemoveHeaderEntry() + { + if (_headerTableSize == 0) + { + return null; + } + var eldest = Head.After; + var hash = eldest.Hash; + var bucketIndex = CalculateBucketIndex(hash); + var prev = _headerBuckets[bucketIndex]; + var e = prev; + while (e != null) + { + HPackHeaderEntry next = e.Next; + if (e == eldest) + { + if (prev == eldest) + { + _headerBuckets[bucketIndex] = next; + } + else + { + prev.Next = next; + } + _headerTableSize -= eldest.CalculateSize(); + eldest.Remove(); + return eldest; + } + prev = e; + e = next; + } + return null; + } + + private int CalculateBucketIndex(int hash) + { + return hash & _hashMask; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs new file mode 100644 index 000000000000..6ca14f75c154 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs @@ -0,0 +1,151 @@ +// 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.Globalization; +using System.Net; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack +{ + internal static class StatusCodes + { + public static string ToStatusString(int statusCode) + { + switch (statusCode) + { + case (int)HttpStatusCode.Continue: + return "100"; + case (int)HttpStatusCode.SwitchingProtocols: + return "101"; + case (int)HttpStatusCode.Processing: + return "102"; + + case (int)HttpStatusCode.OK: + return "200"; + case (int)HttpStatusCode.Created: + return "201"; + case (int)HttpStatusCode.Accepted: + return "202"; + case (int)HttpStatusCode.NonAuthoritativeInformation: + return "203"; + case (int)HttpStatusCode.NoContent: + return "204"; + case (int)HttpStatusCode.ResetContent: + return "205"; + case (int)HttpStatusCode.PartialContent: + return "206"; + case (int)HttpStatusCode.MultiStatus: + return "207"; + case (int)HttpStatusCode.AlreadyReported: + return "208"; + case (int)HttpStatusCode.IMUsed: + return "226"; + + case (int)HttpStatusCode.MultipleChoices: + return "300"; + case (int)HttpStatusCode.MovedPermanently: + return "301"; + case (int)HttpStatusCode.Found: + return "302"; + case (int)HttpStatusCode.SeeOther: + return "303"; + case (int)HttpStatusCode.NotModified: + return "304"; + case (int)HttpStatusCode.UseProxy: + return "305"; + case (int)HttpStatusCode.Unused: + return "306"; + case (int)HttpStatusCode.TemporaryRedirect: + return "307"; + case (int)HttpStatusCode.PermanentRedirect: + return "308"; + + case (int)HttpStatusCode.BadRequest: + return "400"; + case (int)HttpStatusCode.Unauthorized: + return "401"; + case (int)HttpStatusCode.PaymentRequired: + return "402"; + case (int)HttpStatusCode.Forbidden: + return "403"; + case (int)HttpStatusCode.NotFound: + return "404"; + case (int)HttpStatusCode.MethodNotAllowed: + return "405"; + case (int)HttpStatusCode.NotAcceptable: + return "406"; + case (int)HttpStatusCode.ProxyAuthenticationRequired: + return "407"; + case (int)HttpStatusCode.RequestTimeout: + return "408"; + case (int)HttpStatusCode.Conflict: + return "409"; + case (int)HttpStatusCode.Gone: + return "410"; + case (int)HttpStatusCode.LengthRequired: + return "411"; + case (int)HttpStatusCode.PreconditionFailed: + return "412"; + case (int)HttpStatusCode.RequestEntityTooLarge: + return "413"; + case (int)HttpStatusCode.RequestUriTooLong: + return "414"; + case (int)HttpStatusCode.UnsupportedMediaType: + return "415"; + case (int)HttpStatusCode.RequestedRangeNotSatisfiable: + return "416"; + case (int)HttpStatusCode.ExpectationFailed: + return "417"; + case (int)418: + return "418"; + case (int)419: + return "419"; + case (int)HttpStatusCode.MisdirectedRequest: + return "421"; + case (int)HttpStatusCode.UnprocessableEntity: + return "422"; + case (int)HttpStatusCode.Locked: + return "423"; + case (int)HttpStatusCode.FailedDependency: + return "424"; + case (int)HttpStatusCode.UpgradeRequired: + return "426"; + case (int)HttpStatusCode.PreconditionRequired: + return "428"; + case (int)HttpStatusCode.TooManyRequests: + return "429"; + case (int)HttpStatusCode.RequestHeaderFieldsTooLarge: + return "431"; + case (int)HttpStatusCode.UnavailableForLegalReasons: + return "451"; + + case (int)HttpStatusCode.InternalServerError: + return "500"; + case (int)HttpStatusCode.NotImplemented: + return "501"; + case (int)HttpStatusCode.BadGateway: + return "502"; + case (int)HttpStatusCode.ServiceUnavailable: + return "503"; + case (int)HttpStatusCode.GatewayTimeout: + return "504"; + case (int)HttpStatusCode.HttpVersionNotSupported: + return "505"; + case (int)HttpStatusCode.VariantAlsoNegotiates: + return "506"; + case (int)HttpStatusCode.InsufficientStorage: + return "507"; + case (int)HttpStatusCode.LoopDetected: + return "508"; + case (int)HttpStatusCode.NotExtended: + return "510"; + case (int)HttpStatusCode.NetworkAuthenticationRequired: + return "511"; + + default: + return statusCode.ToString(CultureInfo.InvariantCulture); + + } + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs deleted file mode 100644 index 1598a18c7f67..000000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs +++ /dev/null @@ -1,157 +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.Net.Http; -using System.Net.Http.HPack; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 -{ - internal static class HPackHeaderWriter - { - /// - /// Begin encoding headers in the first HEADERS frame. - /// - public static bool BeginEncodeHeaders(int statusCode, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) - { - if (!HPackEncoder.EncodeStatusHeader(statusCode, buffer, out var statusCodeLength)) - { - throw new HPackEncodingException(SR.net_http_hpack_encode_failure); - } - - if (!headersEnumerator.MoveNext()) - { - length = statusCodeLength; - return true; - } - - // We're ok with not throwing if no headers were encoded because we've already encoded the status. - // There is a small chance that the header will encode if there is no other content in the next HEADERS frame. - var done = EncodeHeaders(headersEnumerator, buffer.Slice(statusCodeLength), throwIfNoneEncoded: false, out var headersLength); - length = statusCodeLength + headersLength; - - return done; - } - - /// - /// Begin encoding headers in the first HEADERS frame. - /// - public static bool BeginEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) - { - if (!headersEnumerator.MoveNext()) - { - length = 0; - return true; - } - - return EncodeHeaders(headersEnumerator, buffer, throwIfNoneEncoded: true, out length); - } - - /// - /// Continue encoding headers in the next HEADERS frame. The enumerator should already have a current value. - /// - public static bool ContinueEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) - { - return EncodeHeaders(headersEnumerator, buffer, throwIfNoneEncoded: true, out length); - } - - private static bool EncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, bool throwIfNoneEncoded, out int length) - { - var currentLength = 0; - do - { - if (!EncodeHeader(headersEnumerator.KnownHeaderType, headersEnumerator.Current.Key, headersEnumerator.Current.Value, buffer.Slice(currentLength), out int headerLength)) - { - // The the header wasn't written and no headers have been written then the header is too large. - // Throw an error to avoid an infinite loop of attempting to write large header. - if (currentLength == 0 && throwIfNoneEncoded) - { - throw new HPackEncodingException(SR.net_http_hpack_encode_failure); - } - - length = currentLength; - return false; - } - - currentLength += headerLength; - } - while (headersEnumerator.MoveNext()); - - length = currentLength; - - return true; - } - - private static bool EncodeHeader(KnownHeaderType knownHeaderType, string name, string value, Span buffer, out int length) - { - var hPackStaticTableId = GetResponseHeaderStaticTableId(knownHeaderType); - - if (hPackStaticTableId == -1) - { - return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out length); - } - else - { - return HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(hPackStaticTableId, value, buffer, out length); - } - } - - private static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType) - { - switch (responseHeaderType) - { - case KnownHeaderType.CacheControl: - return H2StaticTable.CacheControl; - case KnownHeaderType.Date: - return H2StaticTable.Date; - case KnownHeaderType.TransferEncoding: - return H2StaticTable.TransferEncoding; - case KnownHeaderType.Via: - return H2StaticTable.Via; - case KnownHeaderType.Allow: - return H2StaticTable.Allow; - case KnownHeaderType.ContentType: - return H2StaticTable.ContentType; - case KnownHeaderType.ContentEncoding: - return H2StaticTable.ContentEncoding; - case KnownHeaderType.ContentLanguage: - return H2StaticTable.ContentLanguage; - case KnownHeaderType.ContentLocation: - return H2StaticTable.ContentLocation; - case KnownHeaderType.ContentRange: - return H2StaticTable.ContentRange; - case KnownHeaderType.Expires: - return H2StaticTable.Expires; - case KnownHeaderType.LastModified: - return H2StaticTable.LastModified; - case KnownHeaderType.AcceptRanges: - return H2StaticTable.AcceptRanges; - case KnownHeaderType.Age: - return H2StaticTable.Age; - case KnownHeaderType.ETag: - return H2StaticTable.ETag; - case KnownHeaderType.Location: - return H2StaticTable.Location; - case KnownHeaderType.ProxyAuthenticate: - return H2StaticTable.ProxyAuthenticate; - case KnownHeaderType.RetryAfter: - return H2StaticTable.RetryAfter; - case KnownHeaderType.Server: - return H2StaticTable.Server; - case KnownHeaderType.SetCookie: - return H2StaticTable.SetCookie; - case KnownHeaderType.Vary: - return H2StaticTable.Vary; - case KnownHeaderType.WWWAuthenticate: - return H2StaticTable.WwwAuthenticate; - case KnownHeaderType.AccessControlAllowOrigin: - return H2StaticTable.AccessControlAllowOrigin; - case KnownHeaderType.ContentLength: - return H2StaticTable.ContentLength; - default: - return -1; - } - } - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 8666bebc64a4..acf81b3e1293 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -87,7 +87,7 @@ public Http2Connection(HttpConnectionContext context) httpLimits.MinResponseDataRate, context.ConnectionId, context.MemoryPool, - context.ServiceContext.Log); + context.ServiceContext); var inputOptions = new PipeOptions(pool: context.MemoryPool, readerScheduler: context.ServiceContext.Scheduler, @@ -743,6 +743,15 @@ private Task ProcessSettingsFrameAsync(in ReadOnlySequence payload) } } + // Maximum HPack encoder size is limited by Http2Limits.HeaderTableSize, configured max the server. + // + // Note that the client HPack decoder doesn't care about the ACK so we don't need to lock sending the + // ACK and updating the table size on the server together. + // The client will wait until a size agreed upon by it (sent in SETTINGS_HEADER_TABLE_SIZE) and the + // server (sent as a dynamic table size update in the next HEADERS frame) is received before applying + // the new size. + _frameWriter.UpdateMaxHeaderTableSize(Math.Min(_clientSettings.HeaderTableSize, (uint)Limits.Http2.HeaderTableSize)); + return ackTask.AsTask(); } catch (Http2SettingsParameterOutOfRangeException ex) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index fd8d5530678c..76bb3d732cc5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -15,6 +15,7 @@ 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; @@ -38,6 +39,7 @@ internal class Http2FrameWriter private readonly ITimeoutControl _timeoutControl; private readonly MinDataRate _minResponseDataRate; private readonly TimingPipeFlusher _flusher; + private readonly Http2HPackEncoder _hpackEncoder; private uint _maxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize; private byte[] _headerEncodingBuffer; @@ -55,7 +57,7 @@ public Http2FrameWriter( MinDataRate minResponseDataRate, string connectionId, MemoryPool memoryPool, - IKestrelTrace log) + ServiceContext serviceContext) { // Allow appending more data to the PipeWriter when a flush is pending. _outputWriter = new ConcurrentPipeWriter(outputPipeWriter, memoryPool, _writeLock); @@ -63,12 +65,22 @@ public Http2FrameWriter( _http2Connection = http2Connection; _connectionOutputFlowControl = connectionOutputFlowControl; _connectionId = connectionId; - _log = log; + _log = serviceContext.Log; _timeoutControl = timeoutControl; _minResponseDataRate = minResponseDataRate; - _flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log); + _flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, serviceContext.Log); _outgoingFrame = new Http2Frame(); _headerEncodingBuffer = new byte[_maxFrameSize]; + + _hpackEncoder = new Http2HPackEncoder(serviceContext.ServerOptions.DisableResponseDynamicHeaderCompression); + } + + public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) + { + lock (_writeLock) + { + _hpackEncoder.UpdateMaxHeaderTableSize(maxHeaderTableSize); + } } public void UpdateMaxFrameSize(uint maxFrameSize) @@ -175,7 +187,7 @@ public void WriteResponseHeaders(int streamId, int statusCode, Http2HeadersFrame _headersEnumerator.Initialize(headers); _outgoingFrame.PrepareHeaders(headerFrameFlags, streamId); var buffer = _headerEncodingBuffer.AsSpan(); - var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _headersEnumerator, buffer, out var payloadLength); + var done = _hpackEncoder.BeginEncodeHeaders(statusCode, _headersEnumerator, buffer, out var payloadLength); FinishWritingHeaders(streamId, payloadLength, done); } catch (HPackEncodingException hex) @@ -201,7 +213,7 @@ public ValueTask WriteResponseTrailers(int streamId, HttpResponseTr _headersEnumerator.Initialize(headers); _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_STREAM, streamId); var buffer = _headerEncodingBuffer.AsSpan(); - var done = HPackHeaderWriter.BeginEncodeHeaders(_headersEnumerator, buffer, out var payloadLength); + var done = _hpackEncoder.BeginEncodeHeaders(_headersEnumerator, buffer, out var payloadLength); FinishWritingHeaders(streamId, payloadLength, done); } catch (HPackEncodingException hex) @@ -230,7 +242,7 @@ private void FinishWritingHeaders(int streamId, int payloadLength, bool done) { _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - done = HPackHeaderWriter.ContinueEncodeHeaders(_headersEnumerator, buffer, out payloadLength); + done = _hpackEncoder.ContinueEncodeHeaders(_headersEnumerator, buffer, out payloadLength); _outgoingFrame.PayloadLength = payloadLength; if (done) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs index 421650b9fde2..db21bfb0fddd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2HeaderEnumerator.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; +using System.Net.Http.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.Extensions.Primitives; @@ -15,8 +16,9 @@ internal sealed class Http2HeadersEnumerator : IEnumerator> _genericEnumerator; private StringValues.Enumerator _stringValuesEnumerator; + private KnownHeaderType _knownHeaderType; - public KnownHeaderType KnownHeaderType { get; private set; } + public int HPackStaticTableId => GetResponseHeaderStaticTableId(_knownHeaderType); public KeyValuePair Current { get; private set; } object IEnumerator.Current => Current; @@ -33,7 +35,7 @@ public void Initialize(HttpResponseHeaders headers) _stringValuesEnumerator = default; Current = default; - KnownHeaderType = default; + _knownHeaderType = default; } public void Initialize(HttpResponseTrailers headers) @@ -45,7 +47,7 @@ public void Initialize(HttpResponseTrailers headers) _stringValuesEnumerator = default; Current = default; - KnownHeaderType = default; + _knownHeaderType = default; } public void Initialize(IDictionary headers) @@ -57,7 +59,7 @@ public void Initialize(IDictionary headers) _stringValuesEnumerator = default; Current = default; - KnownHeaderType = default; + _knownHeaderType = default; } public bool MoveNext() @@ -110,7 +112,7 @@ private bool TryGetNextStringEnumerator(out StringValues.Enumerator enumerator) else { enumerator = _genericEnumerator.Current.Value.GetEnumerator(); - KnownHeaderType = default; + _knownHeaderType = default; return true; } } @@ -124,7 +126,7 @@ private bool TryGetNextStringEnumerator(out StringValues.Enumerator enumerator) else { enumerator = _trailersEnumerator.Current.Value.GetEnumerator(); - KnownHeaderType = _trailersEnumerator.CurrentKnownType; + _knownHeaderType = _trailersEnumerator.CurrentKnownType; return true; } } @@ -138,7 +140,7 @@ private bool TryGetNextStringEnumerator(out StringValues.Enumerator enumerator) else { enumerator = _headersEnumerator.Current.Value.GetEnumerator(); - KnownHeaderType = _headersEnumerator.CurrentKnownType; + _knownHeaderType = _headersEnumerator.CurrentKnownType; return true; } } @@ -159,11 +161,68 @@ public void Reset() _headersEnumerator.Reset(); } _stringValuesEnumerator = default; - KnownHeaderType = default; + _knownHeaderType = default; } public void Dispose() { } + + internal static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType) + { + switch (responseHeaderType) + { + case KnownHeaderType.CacheControl: + return H2StaticTable.CacheControl; + case KnownHeaderType.Date: + return H2StaticTable.Date; + case KnownHeaderType.TransferEncoding: + return H2StaticTable.TransferEncoding; + case KnownHeaderType.Via: + return H2StaticTable.Via; + case KnownHeaderType.Allow: + return H2StaticTable.Allow; + case KnownHeaderType.ContentType: + return H2StaticTable.ContentType; + case KnownHeaderType.ContentEncoding: + return H2StaticTable.ContentEncoding; + case KnownHeaderType.ContentLanguage: + return H2StaticTable.ContentLanguage; + case KnownHeaderType.ContentLocation: + return H2StaticTable.ContentLocation; + case KnownHeaderType.ContentRange: + return H2StaticTable.ContentRange; + case KnownHeaderType.Expires: + return H2StaticTable.Expires; + case KnownHeaderType.LastModified: + return H2StaticTable.LastModified; + case KnownHeaderType.AcceptRanges: + return H2StaticTable.AcceptRanges; + case KnownHeaderType.Age: + return H2StaticTable.Age; + case KnownHeaderType.ETag: + return H2StaticTable.ETag; + case KnownHeaderType.Location: + return H2StaticTable.Location; + case KnownHeaderType.ProxyAuthenticate: + return H2StaticTable.ProxyAuthenticate; + case KnownHeaderType.RetryAfter: + return H2StaticTable.RetryAfter; + case KnownHeaderType.Server: + return H2StaticTable.Server; + case KnownHeaderType.SetCookie: + return H2StaticTable.SetCookie; + case KnownHeaderType.Vary: + return H2StaticTable.Vary; + case KnownHeaderType.WWWAuthenticate: + return H2StaticTable.WwwAuthenticate; + case KnownHeaderType.AccessControlAllowOrigin: + return H2StaticTable.AccessControlAllowOrigin; + case KnownHeaderType.ContentLength: + return H2StaticTable.ContentLength; + default: + return -1; + } + } } } diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 71def30d7399..6ae930bb3b56 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -55,6 +55,14 @@ public class KestrelServerOptions /// public bool DisableStringReuse { get; set; } = false; + /// + /// Gets or sets a value that controls whether dynamic compression of response headers is enabled across HTTP/2 and HTTP/3 requests. + /// + /// + /// Defaults to false. + /// + public bool DisableResponseDynamicHeaderCompression { get; set; } = false; + /// /// Enables the Listen options callback to resolve and use services registered by the application during startup. /// Typically initialized by UseKestrel()"/>. 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 72a38463da28..a38f17f12e7a 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 @@ -16,9 +16,9 @@ - - - + + + diff --git a/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs b/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs index 3b290d712be6..bab351521168 100644 --- a/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs +++ b/src/Servers/Kestrel/Core/test/HPackHeaderWriterTests.cs @@ -1,199 +1,199 @@ -// 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; -using Microsoft.Extensions.Primitives; -using Xunit; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests -{ - public class HPackHeaderWriterTests - { - public static TheoryData[], byte[], int?> SinglePayloadData - { - get - { - var data = new TheoryData[], byte[], int?>(); - - // Lowercase header name letters only - data.Add( - new[] - { - new KeyValuePair("CustomHeader", "CustomValue"), - }, - new byte[] - { - // 0 12 c u s t o m - 0x00, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, - // h e a d e r 11 C - 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43, - // u s t o m V a l - 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, - // u e - 0x75, 0x65 - }, - null); - // Lowercase header name letters only - data.Add( - new[] - { - new KeyValuePair("CustomHeader!#$%&'*+-.^_`|~", "CustomValue"), - }, - new byte[] - { - // 0 27 c u s t o m - 0x00, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, - // h e a d e r ! # - 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23, - // $ % & ' * + - . - 0x24, 0x25, 0x26, 0x27, 0x2a, 0x2b, 0x2d, 0x2e, - // ^ _ ` | ~ 11 C u - 0x5e, 0x5f, 0x60, 0x7c, 0x7e, 0x0b, 0x43, 0x75, - // s t o m V a l u - 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, - // e - 0x65 - }, - null); - // Single Payload - data.Add( - new[] - { - new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), - new KeyValuePair("content-type", "text/html; charset=utf-8"), - new KeyValuePair("server", "Kestrel") - }, - new byte[] - { - 0x88, 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, - 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, - 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, - 0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, - 0x30, 0x20, 0x47, 0x4d, 0x54, 0x00, 0x0c, 0x63, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74, - 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63, - 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75, - 0x74, 0x66, 0x2d, 0x38, 0x00, 0x06, 0x73, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73, - 0x74, 0x72, 0x65, 0x6c - }, - 200); - - return data; - } - } - - [Theory] - [MemberData(nameof(SinglePayloadData))] - public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode) - { - var payload = new byte[1024]; - var length = 0; - if (statusCode.HasValue) - { - Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, GetHeadersEnumerator(headers), payload, out length)); - } - else - { - Assert.True(HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out length)); - } - Assert.Equal(expectedPayload.Length, length); - - for (var i = 0; i < length; i++) - { - Assert.True(expectedPayload[i] == payload[i], $"{expectedPayload[i]} != {payload[i]} at {i} (len {length})"); - } - - Assert.Equal(expectedPayload, new ArraySegment(payload, 0, length)); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize) - { - var statusCode = 200; - var headers = new[] - { - new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), - new KeyValuePair("content-type", "text/html; charset=utf-8"), - new KeyValuePair("server", "Kestrel") - }; - - var expectedStatusCodePayload = new byte[] - { - 0x88 - }; - - var expectedDateHeaderPayload = new byte[] - { - 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d, - 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, 0x4a, - 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, 0x20, - 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x30, - 0x20, 0x47, 0x4d, 0x54 - }; - - var expectedContentTypeHeaderPayload = new byte[] - { - 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x18, 0x74, - 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c, - 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, - 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38 - }; - - var expectedServerHeaderPayload = new byte[] - { - 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c - }; - - Span payload = new byte[1024]; - var offset = 0; - var headerEnumerator = GetHeadersEnumerator(headers); - - // When !exactSize, slices are one byte short of fitting the next header - var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1); - Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, headerEnumerator, payload.Slice(offset, sliceLength), out var length)); - Assert.Equal(expectedStatusCodePayload.Length, length); - Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray()); - - offset += length; - - sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1); - Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); - Assert.Equal(expectedDateHeaderPayload.Length, length); - Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray()); - - offset += length; - - sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1); - Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); - Assert.Equal(expectedContentTypeHeaderPayload.Length, length); - Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray()); - - offset += length; - - sliceLength = expectedServerHeaderPayload.Length; - Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); - Assert.Equal(expectedServerHeaderPayload.Length, length); - Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray()); - } - - private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers) - { - var groupedHeaders = headers - .GroupBy(k => k.Key) - .ToDictionary(g => g.Key, g => new StringValues(g.Select(gg => gg.Value).ToArray())); - - var enumerator = new Http2HeadersEnumerator(); - enumerator.Initialize(groupedHeaders); - return enumerator; - } - } -} +//// 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; +//using Microsoft.Extensions.Primitives; +//using Xunit; + +//namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +//{ +// public class HPackHeaderWriterTests +// { +// public static TheoryData[], byte[], int?> SinglePayloadData +// { +// get +// { +// var data = new TheoryData[], byte[], int?>(); + +// // Lowercase header name letters only +// data.Add( +// new[] +// { +// new KeyValuePair("CustomHeader", "CustomValue"), +// }, +// new byte[] +// { +// // 0 12 c u s t o m +// 0x00, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, +// // h e a d e r 11 C +// 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43, +// // u s t o m V a l +// 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, +// // u e +// 0x75, 0x65 +// }, +// null); +// // Lowercase header name letters only +// data.Add( +// new[] +// { +// new KeyValuePair("CustomHeader!#$%&'*+-.^_`|~", "CustomValue"), +// }, +// new byte[] +// { +// // 0 27 c u s t o m +// 0x00, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, +// // h e a d e r ! # +// 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23, +// // $ % & ' * + - . +// 0x24, 0x25, 0x26, 0x27, 0x2a, 0x2b, 0x2d, 0x2e, +// // ^ _ ` | ~ 11 C u +// 0x5e, 0x5f, 0x60, 0x7c, 0x7e, 0x0b, 0x43, 0x75, +// // s t o m V a l u +// 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, +// // e +// 0x65 +// }, +// null); +// // Single Payload +// data.Add( +// new[] +// { +// new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), +// new KeyValuePair("content-type", "text/html; charset=utf-8"), +// new KeyValuePair("server", "Kestrel") +// }, +// new byte[] +// { +// 0x88, 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, +// 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, +// 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, +// 0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, +// 0x30, 0x20, 0x47, 0x4d, 0x54, 0x00, 0x0c, 0x63, +// 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74, +// 0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74, +// 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63, +// 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75, +// 0x74, 0x66, 0x2d, 0x38, 0x00, 0x06, 0x73, 0x65, +// 0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73, +// 0x74, 0x72, 0x65, 0x6c +// }, +// 200); + +// return data; +// } +// } + +// [Theory] +// [MemberData(nameof(SinglePayloadData))] +// public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode) +// { +// var payload = new byte[1024]; +// var length = 0; +// if (statusCode.HasValue) +// { +// Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, GetHeadersEnumerator(headers), payload, out length)); +// } +// else +// { +// Assert.True(HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out length)); +// } +// Assert.Equal(expectedPayload.Length, length); + +// for (var i = 0; i < length; i++) +// { +// Assert.True(expectedPayload[i] == payload[i], $"{expectedPayload[i]} != {payload[i]} at {i} (len {length})"); +// } + +// Assert.Equal(expectedPayload, new ArraySegment(payload, 0, length)); +// } + +// [Theory] +// [InlineData(true)] +// [InlineData(false)] +// public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize) +// { +// var statusCode = 200; +// var headers = new[] +// { +// new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), +// new KeyValuePair("content-type", "text/html; charset=utf-8"), +// new KeyValuePair("server", "Kestrel") +// }; + +// var expectedStatusCodePayload = new byte[] +// { +// 0x88 +// }; + +// var expectedDateHeaderPayload = new byte[] +// { +// 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d, +// 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, 0x4a, +// 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, 0x20, +// 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x30, +// 0x20, 0x47, 0x4d, 0x54 +// }; + +// var expectedContentTypeHeaderPayload = new byte[] +// { +// 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, +// 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x18, 0x74, +// 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c, +// 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, +// 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38 +// }; + +// var expectedServerHeaderPayload = new byte[] +// { +// 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, +// 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c +// }; + +// Span payload = new byte[1024]; +// var offset = 0; +// var headerEnumerator = GetHeadersEnumerator(headers); + +// // When !exactSize, slices are one byte short of fitting the next header +// var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1); +// Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, headerEnumerator, payload.Slice(offset, sliceLength), out var length)); +// Assert.Equal(expectedStatusCodePayload.Length, length); +// Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray()); + +// offset += length; + +// sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1); +// Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); +// Assert.Equal(expectedDateHeaderPayload.Length, length); +// Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray()); + +// offset += length; + +// sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1); +// Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); +// Assert.Equal(expectedContentTypeHeaderPayload.Length, length); +// Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray()); + +// offset += length; + +// sliceLength = expectedServerHeaderPayload.Length; +// Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); +// Assert.Equal(expectedServerHeaderPayload.Length, length); +// Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray()); +// } + +// private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers) +// { +// var groupedHeaders = headers +// .GroupBy(k => k.Key) +// .ToDictionary(g => g.Key, g => new StringValues(g.Select(gg => gg.Value).ToArray())); + +// var enumerator = new Http2HeadersEnumerator(); +// enumerator.Initialize(groupedHeaders); +// return enumerator; +// } +// } +//} diff --git a/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs b/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs index 6300fcf85bd9..30913d952e35 100644 --- a/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs +++ b/src/Servers/Kestrel/Core/test/Http2FrameWriterTests.cs @@ -41,7 +41,7 @@ public async Task WriteWindowUpdate_UnsetsReservedBit() { // Arrange var pipe = new Pipe(new PipeOptions(_dirtyMemoryPool, PipeScheduler.Inline, PipeScheduler.Inline)); - var frameWriter = new Http2FrameWriter(pipe.Writer, null, null, null, null, null, null, _dirtyMemoryPool, new Mock().Object); + var frameWriter = CreateFrameWriter(pipe); // Act await frameWriter.WriteWindowUpdateAsync(1, 1); @@ -52,12 +52,22 @@ public async Task WriteWindowUpdate_UnsetsReservedBit() Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x01 }, payload.Skip(Http2FrameReader.HeaderLength).Take(4).ToArray()); } + private Http2FrameWriter CreateFrameWriter(Pipe pipe) + { + var serviceContext = new Internal.ServiceContext + { + ServerOptions = new KestrelServerOptions(), + Log = new Mock().Object + }; + return new Http2FrameWriter(pipe.Writer, null, null, null, null, null, null, _dirtyMemoryPool, serviceContext); + } + [Fact] public async Task WriteGoAway_UnsetsReservedBit() { // Arrange var pipe = new Pipe(new PipeOptions(_dirtyMemoryPool, PipeScheduler.Inline, PipeScheduler.Inline)); - var frameWriter = new Http2FrameWriter(pipe.Writer, null, null, null, null, null, null, _dirtyMemoryPool, new Mock().Object); + var frameWriter = CreateFrameWriter(pipe); // Act await frameWriter.WriteGoAwayAsync(1, Http2ErrorCode.NO_ERROR); diff --git a/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs new file mode 100644 index 000000000000..b562379e90a0 --- /dev/null +++ b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs @@ -0,0 +1,480 @@ +// 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 System.Net.Http.HPack; +using System.Text; +using System.Threading.Tasks; +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.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests +{ + public class Http2HPackEncoderTests + { + [Fact] + public void BeginEncodeHeaders_Status302_NewIndexValue() + { + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(headers); + + var http2HPackEncoder = new Http2HPackEncoder(); + Assert.True(http2HPackEncoder.BeginEncodeHeaders(302, enumerator, buffer, out var length)); + + var result = buffer.Slice(0, length).ToArray(); + var hex = BitConverter.ToString(result); + Assert.Equal("48-03-33-30-32", hex); + + var statusHeader = GetHeaderEntry(http2HPackEncoder, 0); + Assert.Equal(":status", statusHeader.Name); + Assert.Equal("302", statusHeader.Value); + } + + [Fact] + public void BeginEncodeHeaders_CacheControlPrivate_NewIndexValue() + { + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + headers.HeaderCacheControl = "private"; + + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(headers); + + var http2HPackEncoder = new Http2HPackEncoder(); + Assert.True(http2HPackEncoder.BeginEncodeHeaders(302, enumerator, buffer, out var length)); + + var result = buffer.Slice(5, length - 5).ToArray(); + var hex = BitConverter.ToString(result); + Assert.Equal("58-07-70-72-69-76-61-74-65", hex); + + var statusHeader = GetHeaderEntry(http2HPackEncoder, 0); + Assert.Equal("Cache-Control", statusHeader.Name); + Assert.Equal("private", statusHeader.Value); + } + + [Fact] + public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() + { + // Test follows example https://tools.ietf.org/html/rfc7541#appendix-C.5 + + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + headers.HeaderCacheControl = "private"; + headers.HeaderDate = "Mon, 21 Oct 2013 20:13:21 GMT"; + headers.HeaderLocation = "https://www.example.com"; + + var enumerator = new Http2HeadersEnumerator(); + + var http2HPackEncoder = new Http2HPackEncoder(maxHeaderTableSize: 256); + + // First response + enumerator.Initialize(headers); + Assert.True(http2HPackEncoder.BeginEncodeHeaders(302, enumerator, buffer, out var length)); + + var result = buffer.Slice(0, length).ToArray(); + var hex = BitConverter.ToString(result); + Assert.Equal( + "48-03-33-30-32-58-07-70-72-69-76-61-74-65-61-1D-" + + "4D-6F-6E-2C-20-32-31-20-4F-63-74-20-32-30-31-33-" + + "20-32-30-3A-31-33-3A-32-31-20-47-4D-54-6E-17-68-" + + "74-74-70-73-3A-2F-2F-77-77-77-2E-65-78-61-6D-70-" + + "6C-65-2E-63-6F-6D", hex); + + var entries = GetHeaderEntries(http2HPackEncoder); + Assert.Collection(entries, + e => + { + Assert.Equal("Location", e.Name); + Assert.Equal("https://www.example.com", e.Value); + }, + e => + { + Assert.Equal("Date", e.Name); + Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value); + }, + e => + { + Assert.Equal("Cache-Control", e.Name); + Assert.Equal("private", e.Value); + }, + e => + { + Assert.Equal(":status", e.Name); + Assert.Equal("302", e.Value); + }); + + // Second response + enumerator.Initialize(headers); + Assert.True(http2HPackEncoder.BeginEncodeHeaders(307, enumerator, buffer, out length)); + + result = buffer.Slice(0, length).ToArray(); + hex = BitConverter.ToString(result); + Assert.Equal("48-03-33-30-37-C1-C0-BF", hex); + + entries = GetHeaderEntries(http2HPackEncoder); + Assert.Collection(entries, + e => + { + Assert.Equal(":status", e.Name); + Assert.Equal("307", e.Value); + }, + e => + { + Assert.Equal("Location", e.Name); + Assert.Equal("https://www.example.com", e.Value); + }, + e => + { + Assert.Equal("Date", e.Name); + Assert.Equal("Mon, 21 Oct 2013 20:13:21 GMT", e.Value); + }, + e => + { + Assert.Equal("Cache-Control", e.Name); + Assert.Equal("private", e.Value); + }); + + // Third response + headers.HeaderDate = "Mon, 21 Oct 2013 20:13:22 GMT"; + headers.HeaderContentEncoding = "gzip"; + headers.HeaderSetCookie = "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"; + + enumerator.Initialize(headers); + Assert.True(http2HPackEncoder.BeginEncodeHeaders(200, enumerator, buffer, out length)); + + result = buffer.Slice(0, length).ToArray(); + hex = BitConverter.ToString(result); + Assert.Equal( + "88-C1-61-1D-4D-6F-6E-2C-20-32-31-20-4F-63-74-20-" + + "32-30-31-33-20-32-30-3A-31-33-3A-32-32-20-47-4D-" + + "54-5A-04-67-7A-69-70-C1-1F-28-38-66-6F-6F-3D-41-" + + "53-44-4A-4B-48-51-4B-42-5A-58-4F-51-57-45-4F-50-" + + "49-55-41-58-51-57-45-4F-49-55-3B-20-6D-61-78-2D-" + + "61-67-65-3D-33-36-30-30-3B-20-76-65-72-73-69-6F-" + + "6E-3D-31", hex); + + entries = GetHeaderEntries(http2HPackEncoder); + Assert.Collection(entries, + e => + { + Assert.Equal("Content-Encoding", e.Name); + Assert.Equal("gzip", e.Value); + }, + e => + { + Assert.Equal("Date", e.Name); + Assert.Equal("Mon, 21 Oct 2013 20:13:22 GMT", e.Value); + }, + e => + { + Assert.Equal(":status", e.Name); + Assert.Equal("307", e.Value); + }, + e => + { + Assert.Equal("Location", e.Name); + Assert.Equal("https://www.example.com", e.Value); + }); + } + + [Theory] + [InlineData("Set-Cookie", true)] + [InlineData("Content-Disposition", true)] + [InlineData("Content-Length", false)] + public void BeginEncodeHeaders_ExcludedHeaders_NotAddedToTable(string headerName, bool neverIndex) + { + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + headers.Append(headerName, "1"); + + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(headers); + + var http2HPackEncoder = new Http2HPackEncoder(maxHeaderTableSize: Http2PeerSettings.DefaultHeaderTableSize); + Assert.True(http2HPackEncoder.BeginEncodeHeaders(enumerator, buffer, out _)); + + if (neverIndex) + { + Assert.Equal(0x10, buffer[0] & 0x10); + } + else + { + Assert.Equal(0, buffer[0] & 0x40); + } + + Assert.Empty(GetHeaderEntries(http2HPackEncoder)); + } + + [Fact] + public void BeginEncodeHeaders_HeaderExceedHeaderTableSize_NoIndexAndNoHeaderEntry() + { + Span buffer = new byte[1024 * 16]; + + var headers = new HttpResponseHeaders(); + headers.Append("x-Custom", new string('!', (int)Http2PeerSettings.DefaultHeaderTableSize)); + + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(headers); + + var http2HPackEncoder = new Http2HPackEncoder(); + Assert.True(http2HPackEncoder.BeginEncodeHeaders(200, enumerator, buffer, out var length)); + + Assert.Empty(GetHeaderEntries(http2HPackEncoder)); + } + + public static TheoryData[], byte[], int?> SinglePayloadData + { + get + { + var data = new TheoryData[], byte[], int?>(); + + // Lowercase header name letters only + data.Add( + new[] + { + new KeyValuePair("CustomHeader", "CustomValue"), + }, + new byte[] + { + // 12 c u s t o m + 0x40, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, + // h e a d e r 11 C + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x0b, 0x43, + // u s t o m V a l + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, + // u e + 0x75, 0x65 + }, + null); + // Lowercase header name letters only + data.Add( + new[] + { + new KeyValuePair("CustomHeader!#$%&'*+-.^_`|~", "CustomValue"), + }, + new byte[] + { + // 27 c u s t o m + 0x40, 0x1b, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, + // h e a d e r ! # + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x21, 0x23, + // $ % & ' * + - . + 0x24, 0x25, 0x26, 0x27, 0x2a, 0x2b, 0x2d, 0x2e, + // ^ _ ` | ~ 11 C u + 0x5e, 0x5f, 0x60, 0x7c, 0x7e, 0x0b, 0x43, 0x75, + // s t o m V a l u + 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x61, 0x6c, 0x75, + // e + 0x65 + }, + null); + // Single Payload + data.Add( + new[] + { + new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), + new KeyValuePair("content-type", "text/html; charset=utf-8"), + new KeyValuePair("server", "Kestrel") + }, + new byte[] + { + 0x88, 0x40, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, + 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, + 0x4a, 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, + 0x20, 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, + 0x30, 0x20, 0x47, 0x4d, 0x54, 0x40, 0x0c, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x74, 0x65, 0x78, 0x74, + 0x2f, 0x68, 0x74, 0x6d, 0x6c, 0x3b, 0x20, 0x63, + 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x75, + 0x74, 0x66, 0x2d, 0x38, 0x40, 0x06, 0x73, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x07, 0x4b, 0x65, 0x73, + 0x74, 0x72, 0x65, 0x6c + }, + 200); + + return data; + } + } + + [Theory] + [MemberData(nameof(SinglePayloadData))] + public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode) + { + Http2HPackEncoder http2HPackEncoder = new Http2HPackEncoder(); + + var payload = new byte[1024]; + var length = 0; + if (statusCode.HasValue) + { + Assert.True(http2HPackEncoder.BeginEncodeHeaders(statusCode.Value, GetHeadersEnumerator(headers), payload, out length)); + } + else + { + Assert.True(http2HPackEncoder.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out length)); + } + Assert.Equal(expectedPayload.Length, length); + + for (var i = 0; i < length; i++) + { + Assert.True(expectedPayload[i] == payload[i], $"{expectedPayload[i]} != {payload[i]} at {i} (len {length})"); + } + + Assert.Equal(expectedPayload, new ArraySegment(payload, 0, length)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize) + { + var statusCode = 200; + var headers = new[] + { + new KeyValuePair("date", "Mon, 24 Jul 2017 19:22:30 GMT"), + new KeyValuePair("content-type", "text/html; charset=utf-8"), + new KeyValuePair("server", "Kestrel") + }; + + var expectedStatusCodePayload = new byte[] + { + 0x88 + }; + + var expectedDateHeaderPayload = new byte[] + { + 0x40, 0x04, 0x64, 0x61, 0x74, 0x65, 0x1d, 0x4d, + 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x34, 0x20, 0x4a, + 0x75, 0x6c, 0x20, 0x32, 0x30, 0x31, 0x37, 0x20, + 0x31, 0x39, 0x3a, 0x32, 0x32, 0x3a, 0x33, 0x30, + 0x20, 0x47, 0x4d, 0x54 + }; + + var expectedContentTypeHeaderPayload = new byte[] + { + 0x40, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x18, 0x74, + 0x65, 0x78, 0x74, 0x2f, 0x68, 0x74, 0x6d, 0x6c, + 0x3b, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, + 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38 + }; + + var expectedServerHeaderPayload = new byte[] + { + 0x40, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c + }; + + var http2HPackEncoder = new Http2HPackEncoder(); + + Span payload = new byte[1024]; + var offset = 0; + var headerEnumerator = GetHeadersEnumerator(headers); + + // When !exactSize, slices are one byte short of fitting the next header + var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1); + Assert.False(http2HPackEncoder.BeginEncodeHeaders(statusCode, headerEnumerator, payload.Slice(offset, sliceLength), out var length)); + Assert.Equal(expectedStatusCodePayload.Length, length); + Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray()); + + offset += length; + + sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1); + Assert.False(http2HPackEncoder.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); + Assert.Equal(expectedDateHeaderPayload.Length, length); + Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray()); + + offset += length; + + sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1); + Assert.False(http2HPackEncoder.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); + Assert.Equal(expectedContentTypeHeaderPayload.Length, length); + Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray()); + + offset += length; + + sliceLength = expectedServerHeaderPayload.Length; + Assert.True(http2HPackEncoder.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); + Assert.Equal(expectedServerHeaderPayload.Length, length); + Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray()); + } + + [Fact] + public void BeginEncodeHeaders_MaxHeaderTableSizeUpdated_SizeUpdateInHeaders() + { + Span buffer = new byte[1024 * 16]; + + var hpackEncoder = new Http2HPackEncoder(); + hpackEncoder.UpdateMaxHeaderTableSize(100); + + var enumerator = new Http2HeadersEnumerator(); + + // First request + enumerator.Initialize(new Dictionary()); + Assert.True(hpackEncoder.BeginEncodeHeaders(enumerator, buffer, out var length)); + + Assert.Equal(2, length); + + const byte DynamicTableSizeUpdateMask = 0xe0; + + var integerDecoder = new IntegerDecoder(); + Assert.False(integerDecoder.BeginTryDecode((byte)(buffer[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out _)); + Assert.True(integerDecoder.TryDecode(buffer[1], out var result)); + + Assert.Equal(100, result); + + // Second request + enumerator.Initialize(new Dictionary()); + Assert.True(hpackEncoder.BeginEncodeHeaders(enumerator, buffer, out length)); + + Assert.Equal(0, length); + } + + private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers) + { + var groupedHeaders = headers + .GroupBy(k => k.Key) + .ToDictionary(g => g.Key, g => new StringValues(g.Select(gg => gg.Value).ToArray())); + + var enumerator = new Http2HeadersEnumerator(); + enumerator.Initialize(groupedHeaders); + return enumerator; + } + + private HPackHeaderEntry GetHeaderEntry(Http2HPackEncoder encoder, int index) + { + var entry = encoder.Head; + while (index-- >= 0) + { + entry = entry.Before; + } + return entry; + } + + private List GetHeaderEntries(Http2HPackEncoder encoder) + { + var headers = new List(); + + var entry = encoder.Head; + while (entry.Before != encoder.Head) + { + entry = entry.Before; + headers.Add(entry); + }; + + return headers; + } + } +} diff --git a/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs b/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs index 5c7623337c8d..fd8146804e17 100644 --- a/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs +++ b/src/Servers/Kestrel/Core/test/KestrelServerLimitsTests.cs @@ -333,11 +333,10 @@ public void Http2HeaderTableSizeDefault() [Theory] [InlineData(int.MinValue)] [InlineData(-1)] - [InlineData(0)] public void Http2HeaderTableSizeInvalid(int value) { var ex = Assert.Throws(() => new KestrelServerLimits().Http2.HeaderTableSize = value); - Assert.StartsWith(CoreStrings.GreaterThanZeroRequired, ex.Message); + Assert.StartsWith(CoreStrings.GreaterThanOrEqualToZeroRequired, ex.Message); } [Fact] diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs similarity index 92% rename from src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs rename to src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs index b3e8f15403ce..800dd6584375 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs @@ -16,6 +16,7 @@ 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.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Abstractions; @@ -24,28 +25,25 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { - public class Http2ConnectionBenchmark + public abstract class Http2ConnectionBenchmarkBase { private MemoryPool _memoryPool; private HttpRequestHeaders _httpRequestHeaders; private Http2Connection _connection; + private Http2HPackEncoder _hpackEncoder; private Http2HeadersEnumerator _requestHeadersEnumerator; private int _currentStreamId; private byte[] _headersBuffer; private DuplexPipe.DuplexPipePair _connectionPair; private Http2Frame _httpFrame; - private string _responseData; private int _dataWritten; - [Params(0, 10, 1024 * 1024)] - public int ResponseDataLength { get; set; } + protected abstract Task ProcessRequest(HttpContext httpContext); - [GlobalSetup] - public void GlobalSetup() + public virtual void GlobalSetup() { _memoryPool = SlabMemoryPoolFactory.Create(); _httpFrame = new Http2Frame(); - _responseData = new string('!', ResponseDataLength); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); @@ -58,6 +56,7 @@ public void GlobalSetup() _httpRequestHeaders.Append(HeaderNames.Authority, new StringValues("localhost:80")); _headersBuffer = new byte[1024 * 16]; + _hpackEncoder = new Http2HPackEncoder(); var serviceContext = new ServiceContext { @@ -83,7 +82,7 @@ public void GlobalSetup() _currentStreamId = 1; - _ = _connection.ProcessRequestsAsync(new DummyApplication(c => ResponseDataLength == 0 ? Task.CompletedTask : c.Response.WriteAsync(_responseData), new MockHttpContextFactory())); + _ = _connection.ProcessRequestsAsync(new DummyApplication(ProcessRequest, new MockHttpContextFactory())); _connectionPair.Application.Output.Write(Http2Connection.ClientPreface); _connectionPair.Application.Output.WriteSettings(new Http2PeerSettings @@ -102,11 +101,11 @@ public void GlobalSetup() } [Benchmark] - public async Task EmptyRequest() + public async Task MakeRequest() { _requestHeadersEnumerator.Initialize(_httpRequestHeaders); _requestHeadersEnumerator.MoveNext(); - _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _httpFrame); + _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _hpackEncoder, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _httpFrame); await _connectionPair.Application.Output.FlushAsync(); while (true) diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs new file mode 100644 index 000000000000..6859b5b675dc --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionEmptyBenchmark.cs @@ -0,0 +1,29 @@ +// 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.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class Http2ConnectionBenchmark : Http2ConnectionBenchmarkBase + { + [Params(0, 128, 1024)] + public int ResponseDataLength { get; set; } + + private string _responseData; + + [GlobalSetup] + public override void GlobalSetup() + { + base.GlobalSetup(); + _responseData = new string('!', ResponseDataLength); + } + + protected override Task ProcessRequest(HttpContext httpContext) + { + return ResponseDataLength == 0 ? Task.CompletedTask : httpContext.Response.WriteAsync(_responseData); + } + } +} diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs new file mode 100644 index 000000000000..5ac19dd0b9ce --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionHeadersBenchmark.cs @@ -0,0 +1,48 @@ +// 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.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class Http2ConnectionHeadersBenchmark : Http2ConnectionBenchmarkBase + { + [Params(1, 4, 32)] + public int HeadersCount { get; set; } + + [Params(true, false)] + public bool HeadersChange { get; set; } + + private int _headerIndex; + private string[] _headerNames; + + [GlobalSetup] + public override void GlobalSetup() + { + base.GlobalSetup(); + + _headerNames = new string[HeadersCount * (HeadersChange ? 1000 : 1)]; + for (var i = 0; i < _headerNames.Length; i++) + { + _headerNames[i] = "CustomHeader" + i; + } + } + + protected override Task ProcessRequest(HttpContext httpContext) + { + for (var i = 0; i < HeadersCount; i++) + { + var headerName = _headerNames[_headerIndex % HeadersCount]; + httpContext.Response.Headers[headerName] = "The quick brown fox jumps over the lazy dog."; + if (HeadersChange) + { + _headerIndex++; + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs index 839558d1a3ad..ff58ed573d27 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2FrameWriterBenchmark.cs @@ -36,7 +36,7 @@ public void GlobalSetup() minResponseDataRate: null, "TestConnectionId", _memoryPool, - new KestrelTrace(NullLogger.Instance)); + new Core.Internal.ServiceContext()); _responseHeaders = new HttpResponseHeaders(); _responseHeaders.HeaderContentType = "application/json"; diff --git a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs index 481278ae4297..a7ed4ebdfd10 100644 --- a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs +++ b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs @@ -8,6 +8,7 @@ using System.IO.Pipelines; using System.Net.Http.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; namespace Microsoft.AspNetCore.Testing { @@ -25,13 +26,13 @@ public static void WriteSettings(this PipeWriter writer, Http2PeerSettings clien writer.Write(payload); } - public static void WriteStartStream(this PipeWriter writer, int streamId, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream, Http2Frame frame = null) + public static void WriteStartStream(this PipeWriter writer, int streamId, Http2HPackEncoder hpackEncoder, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream, Http2Frame frame = null) { frame ??= new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); var buffer = headerEncodingBuffer.AsSpan(); - var done = HPackHeaderWriter.BeginEncodeHeaders(headers, buffer, out var length); + var done = hpackEncoder.BeginEncodeHeaders(headers, buffer, out var length); frame.PayloadLength = length; if (done) @@ -51,7 +52,7 @@ public static void WriteStartStream(this PipeWriter writer, int streamId, Http2H { frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - done = HPackHeaderWriter.ContinueEncodeHeaders(headers, buffer, out length); + done = hpackEncoder.ContinueEncodeHeaders(headers, buffer, out length); frame.PayloadLength = length; if (done) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 7e4336c5a8d6..f7295c2298d8 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -58,7 +58,7 @@ await InitializeConnectionAsync(async c => await SendWindowUpdateAsync(streamId: 1, 65535); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -90,7 +90,7 @@ await ExpectAsync(Http2FrameType.DATA, await StartStreamAsync(3, GetHeaders(responseBodySize: 3), endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); @@ -197,7 +197,7 @@ await InitializeConnectionAsync(async c => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -274,7 +274,7 @@ public async Task RequestHeaderStringReuse_MultipleStreams_KnownHeaderReused() await StartStreamAsync(1, requestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -293,7 +293,7 @@ await ExpectAsync(Http2FrameType.PING, await StartStreamAsync(3, requestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -323,7 +323,7 @@ await InitializeConnectionAsync(async context => serverTcs.SetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -356,7 +356,7 @@ public async Task StreamPool_MultipleStreamsConcurrent_StreamsReturnedToPool() await SendDataAsync(1, _helloBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -371,7 +371,7 @@ await ExpectAsync(Http2FrameType.DATA, await SendDataAsync(3, _helloBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -415,7 +415,7 @@ await InitializeConnectionAsync(async context => appDelegateTcs.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -438,7 +438,7 @@ await ExpectAsync(Http2FrameType.PING, appDelegateTcs.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -475,7 +475,7 @@ await InitializeConnectionAsync(async context => serverTcs.SetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -593,7 +593,7 @@ public async Task ServerSettings_ChangesRequestMaxFrameSize() await SendDataAsync(1, new byte[length], endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); // The client's settings is still defaulted to Http2PeerSettings.MinAllowedMaxFrameSize so the echo response will come back in two separate frames @@ -622,7 +622,7 @@ public async Task DATA_Received_ReadByStream() await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -648,7 +648,7 @@ public async Task DATA_Received_MaxSize_ReadByStream() await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -691,7 +691,7 @@ public async Task DATA_Received_GreaterThanInitialWindowSize_ReadByStream() } await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -812,7 +812,7 @@ public async Task DATA_Received_Multiple_ReadByStream() await SendDataAsync(1, _noData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -840,7 +840,7 @@ public async Task DATA_Received_Multiplexed_ReadByStreams() await SendDataAsync(1, _helloBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var stream1DataFrame1 = await ExpectAsync(Http2FrameType.DATA, @@ -851,7 +851,7 @@ await ExpectAsync(Http2FrameType.HEADERS, await SendDataAsync(3, _helloBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); var stream3DataFrame1 = await ExpectAsync(Http2FrameType.DATA, @@ -920,7 +920,7 @@ public async Task DATA_Received_Multiplexed_GreaterThanInitialWindowSize_ReadByS } await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -972,7 +972,7 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 0); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); @@ -1050,7 +1050,7 @@ await InitializeConnectionAsync(async context => stream3ReadFinished.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -1065,7 +1065,7 @@ await ExpectAsync(Http2FrameType.DATA, stream1ReadFinished.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1092,7 +1092,7 @@ public async Task DATA_Received_WithPadding_ReadByStream(byte padLength) await SendDataWithPaddingAsync(1, _helloWorldBytes, padLength, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -1137,7 +1137,7 @@ public async Task DATA_Received_WithPadding_CountsTowardsInputFlowControl(byte p } await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1196,7 +1196,7 @@ public async Task DATA_Received_ButNotConsumedByApp_CountsTowardsInputFlowContro } await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1265,7 +1265,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1440,7 +1440,7 @@ public async Task DATA_Received_StreamClosed_ConnectionError() await StartStreamAsync(1, _postRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1482,7 +1482,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1532,7 +1532,7 @@ public async Task DATA_Received_StreamClosedImplicitly_ConnectionError() await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1636,7 +1636,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1657,7 +1657,7 @@ await ExpectAsync(Http2FrameType.DATA, await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1691,7 +1691,7 @@ public async Task DATA_Sent_DespiteStreamOutputFlowControl_IfEmptyAndEndsStream( await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1706,7 +1706,7 @@ public async Task HEADERS_Received_Decoded() await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1726,7 +1726,7 @@ public async Task HEADERS_Received_WithPadding_Decoded(byte padLength) await SendHeadersWithPaddingAsync(1, _browserRequestHeaders, padLength, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1743,7 +1743,7 @@ public async Task HEADERS_Received_WithPriority_Decoded() await SendHeadersWithPriorityAsync(1, _browserRequestHeaders, priority: 42, streamDependency: 0, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1763,7 +1763,7 @@ public async Task HEADERS_Received_WithPriorityAndPadding_Decoded(byte padLength await SendHeadersWithPaddingAndPriorityAsync(1, _browserRequestHeaders, padLength, priority: 42, streamDependency: 0, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1789,7 +1789,7 @@ public async Task HEADERS_Received_WithTrailers_Available(bool sendData) // The second stream should end first, since the first one is waiting for the request body. await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1801,7 +1801,7 @@ await ExpectAsync(Http2FrameType.HEADERS, await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, _requestTrailers); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1835,7 +1835,7 @@ public async Task HEADERS_Received_ContainsExpect100Continue_100ContinueSent() await SendDataAsync(1, _helloBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1885,17 +1885,75 @@ await InitializeConnectionAsync(async context => finishSecondRequest.TrySetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); finishFirstRequest.TrySetResult(null); + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 6, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task HEADERS_HeaderTableSizeLimitZero_Received_DynamicTableUpdate() + { + _serviceContext.ServerOptions.Limits.Http2.HeaderTableSize = 0; + + await InitializeConnectionAsync(_noopApplication, expectedSettingsCount: 4); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + _hpackEncoder.UpdateMaxHeaderTableSize(0); + + var headerFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 38, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + const byte DynamicTableSizeUpdateMask = 0xe0; + + var integerDecoder = new IntegerDecoder(); + Assert.True(integerDecoder.BeginTryDecode((byte)(headerFrame.Payload.Span[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out var result)); + + // Dynamic table update from the server + Assert.Equal(0, result); + + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 3); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task HEADERS_DisableDynamicHeaderCompression_HeadersNotCompressed() + { + _serviceContext.ServerOptions.DisableResponseDynamicHeaderCompression = true; + + await InitializeConnectionAsync(_noopApplication); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + await ExpectAsync(Http2FrameType.HEADERS, withLength: 37, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + await ExpectAsync(Http2FrameType.HEADERS, + withLength: 37, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 3); + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); } @@ -1918,7 +1976,7 @@ public async Task HEADERS_OverMaxStreamLimit_Refused() requestBlocker.SetResult(0); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1961,7 +2019,7 @@ public async Task HEADERS_Received_StreamClosed_ConnectionError() await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -2004,7 +2062,7 @@ public async Task HEADERS_Received_StreamClosedImplicitly_ConnectionError() await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -2228,7 +2286,7 @@ public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeade await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -2373,7 +2431,7 @@ public async Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsTrailers_N await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -2511,7 +2569,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2536,7 +2594,7 @@ await ExpectAsync(Http2FrameType.DATA, // The headers, but not the data for stream 3, can be sent prior to any window updates. await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); @@ -2615,12 +2673,12 @@ await InitializeConnectionAsync(async context => } }); - async Task VerifyStreamBackpressure(int streamId) + async Task VerifyStreamBackpressure(int streamId, int headersLength) { await StartStreamAsync(streamId, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: headersLength, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: streamId); @@ -2633,9 +2691,9 @@ await ExpectAsync(Http2FrameType.HEADERS, Assert.False(writeTasks[streamId].IsCompleted); } - await VerifyStreamBackpressure(1); - await VerifyStreamBackpressure(3); - await VerifyStreamBackpressure(5); + await VerifyStreamBackpressure(1, 32); + await VerifyStreamBackpressure(3, 2); + await VerifyStreamBackpressure(5, 2); await SendRstStreamAsync(1); await writeTasks[1].DefaultTimeout(); @@ -2913,6 +2971,7 @@ public async Task SETTINGS_Custom_Sent() { CreateConnection(); + _connection.ServerSettings.HeaderTableSize = 0; _connection.ServerSettings.MaxConcurrentStreams = 1; _connection.ServerSettings.MaxHeaderListSize = 4 * 1024; _connection.ServerSettings.InitialWindowSize = 1024 * 1024 * 10; @@ -2923,23 +2982,27 @@ public async Task SETTINGS_Custom_Sent() await SendSettingsAsync(); var frame = await ExpectAsync(Http2FrameType.SETTINGS, - withLength: Http2FrameReader.SettingSize * 3, + withLength: Http2FrameReader.SettingSize * 4, withFlags: 0, withStreamId: 0); // Only non protocol defaults are sent var settings = Http2FrameReader.ReadSettings(frame.PayloadSequence); - Assert.Equal(3, settings.Count); + Assert.Equal(4, settings.Count); var setting = settings[0]; + Assert.Equal(Http2SettingsParameter.SETTINGS_HEADER_TABLE_SIZE, setting.Parameter); + Assert.Equal(0u, setting.Value); + + setting = settings[1]; Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS, setting.Parameter); Assert.Equal(1u, setting.Value); - setting = settings[1]; + setting = settings[2]; Assert.Equal(Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE, setting.Parameter); Assert.Equal(1024 * 1024 * 10u, setting.Value); - setting = settings[2]; + setting = settings[3]; Assert.Equal(Http2SettingsParameter.SETTINGS_MAX_HEADER_LIST_SIZE, setting.Parameter); Assert.Equal(4 * 1024u, setting.Value); @@ -3100,7 +3163,7 @@ public async Task SETTINGS_Received_ChangesAllowedResponseMaxFrameSize() _connection.ServerSettings.MaxFrameSize = Http2PeerSettings.MaxAllowedMaxFrameSize; // This includes the default response headers such as :status, etc - var defaultResponseHeaderLength = 33; + var defaultResponseHeaderLength = 32; var headerValueLength = Http2PeerSettings.MinAllowedMaxFrameSize; // First byte is always 0 // Second byte is the length of header name which is 1 @@ -3170,7 +3233,7 @@ await InitializeConnectionAsync(context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3204,7 +3267,56 @@ await ExpectAsync(Http2FrameType.SETTINGS, withFlags: (byte)Http2SettingsFrameFlags.ACK, withStreamId: 0); - await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + // Start request + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var headerFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 36, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + // Headers start with :status = 200 + Assert.Equal(0x88, headerFrame.Payload.Span[0]); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + } + + [Fact] + public async Task SETTINGS_Received_WithLargeHeaderTableSizeLimit_ChangesHeaderTableSize() + { + _serviceContext.ServerOptions.Limits.Http2.HeaderTableSize = 40000; + + await InitializeConnectionAsync(_noopApplication, expectedSettingsCount: 4); + + // Update client settings + _clientSettings.HeaderTableSize = 65536; // Chrome's default, larger than the 4kb spec default + await SendSettingsAsync(); + + // ACK + await ExpectAsync(Http2FrameType.SETTINGS, + withLength: 0, + withFlags: (byte)Http2SettingsFrameFlags.ACK, + withStreamId: 0); + + // Start request + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var headerFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 40, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + const byte DynamicTableSizeUpdateMask = 0xe0; + + var integerDecoder = new IntegerDecoder(); + Assert.False(integerDecoder.BeginTryDecode((byte)(headerFrame.Payload.Span[0] & ~DynamicTableSizeUpdateMask), prefixLength: 5, out _)); + Assert.False(integerDecoder.TryDecode(headerFrame.Payload.Span[1], out _)); + Assert.False(integerDecoder.TryDecode(headerFrame.Payload.Span[2], out _)); + Assert.True(integerDecoder.TryDecode(headerFrame.Payload.Span[3], out var result)); + + Assert.Equal(40000, result); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } [Fact] @@ -3319,7 +3431,7 @@ public async Task GOAWAY_Received_SetsConnectionStateToClosingAndWaitForAllStrea await SendDataAsync(1, _helloBytes, true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3332,7 +3444,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await SendDataAsync(3, _helloBytes, true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -3405,7 +3517,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -3428,13 +3540,13 @@ await ExpectAsync(Http2FrameType.DATA, // The headers, but not the data for the stream, can still be sent. await StartStreamAsync(3, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await StartStreamAsync(5, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 5); @@ -3493,12 +3605,12 @@ await InitializeConnectionAsync(async context => } }); - async Task VerifyStreamBackpressure(int streamId) + async Task VerifyStreamBackpressure(int streamId, int headersLength) { await StartStreamAsync(streamId, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: headersLength, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: streamId); @@ -3511,9 +3623,9 @@ await ExpectAsync(Http2FrameType.HEADERS, Assert.False(writeTasks[streamId].IsCompleted); } - await VerifyStreamBackpressure(1); - await VerifyStreamBackpressure(3); - await VerifyStreamBackpressure(5); + await VerifyStreamBackpressure(1, 32); + await VerifyStreamBackpressure(3, 2); + await VerifyStreamBackpressure(5, 2); // Close all pipes and wait for response to drain _pair.Application.Output.Complete(); @@ -3731,7 +3843,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -3788,7 +3900,7 @@ public async Task WINDOW_UPDATE_Received_OnStream_Respected() await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -3827,7 +3939,7 @@ public async Task WINDOW_UPDATE_Received_OnStream_Respected_WhenInitialWindowSiz await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -3879,7 +3991,7 @@ public async Task CONTINUATION_Received_Decoded() await StartStreamAsync(1, _twoContinuationsRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3906,7 +4018,7 @@ public async Task CONTINUATION_Received_WithTrailers_Available(bool sendData) // The second stream should end first, since the first one is waiting for the request body. await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -3929,7 +4041,7 @@ await ExpectAsync(Http2FrameType.HEADERS, await SendContinuationAsync(1, Http2ContinuationFrameFlags.END_HEADERS, trailers); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -4027,7 +4139,7 @@ public async Task CONTINUATION_Received_HeaderBlockDoesNotContainMandatoryPseudo await SendEmptyContinuationFrameAsync(1, Http2ContinuationFrameFlags.END_HEADERS); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -4042,7 +4154,7 @@ public async Task CONTINUATION_Sent_WhenHeadersLargerThanFrameLength() await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 12343, + withLength: 12342, withFlags: (byte)Http2HeadersFrameFlags.END_STREAM, withStreamId: 1); var continuationFrame1 = await ExpectAsync(Http2FrameType.CONTINUATION, @@ -4201,7 +4313,7 @@ public async Task StopProcessingNextRequestSendsGracefulGOAWAYAndWaitsForStreams await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -4251,7 +4363,7 @@ public async Task StopProcessingNextRequestSendsGracefulGOAWAYThenFinalGOAWAYWhe await SendDataAsync(1, _helloBytes, true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -4284,8 +4396,8 @@ public async Task AcceptNewStreamsDuringClosingConnection() await StartStreamAsync(3, _browserRequestHeaders, endStream: false); await SendDataAsync(1, _helloBytes, true); - await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + var f = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -4298,7 +4410,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await SendDataAsync(3, _helloBytes, true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -4388,7 +4500,7 @@ public async Task AppDoesNotReadRequestBody_ResetsAndDrainsRequest(int intFinalF await StartStreamAsync(1, headers, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index a59207ccda35..efed04f3023a 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -79,7 +79,7 @@ public async Task HEADERS_Received_CustomMethod_Accepted() await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 52, + withLength: 51, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -104,7 +104,7 @@ public async Task HEADERS_Received_CONNECTMethod_Accepted() await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 53, + withLength: 52, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -131,7 +131,7 @@ public async Task HEADERS_Received_OPTIONSStar_LeftOutOfPath() await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 57, + withLength: 56, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -159,7 +159,7 @@ public async Task HEADERS_Received_OPTIONSSlash_Accepted() await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 58, + withLength: 57, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -193,7 +193,7 @@ await InitializeConnectionAsync(context => await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 100, + withLength: 99, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -235,7 +235,7 @@ await InitializeConnectionAsync(context => await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -297,7 +297,7 @@ public async Task HEADERS_Received_MissingAuthority_200Status() await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -326,7 +326,7 @@ public async Task HEADERS_Received_EmptyAuthority_200Status() await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -355,7 +355,7 @@ public async Task HEADERS_Received_MissingAuthorityFallsBackToHost_200Status() await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 47, + withLength: 46, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -386,7 +386,7 @@ public async Task HEADERS_Received_EmptyAuthorityIgnoredOverHost_200Status() await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 47, + withLength: 46, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -417,7 +417,7 @@ public async Task HEADERS_Received_AuthorityOverridesHost_200Status() await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 47, + withLength: 46, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -448,7 +448,7 @@ public async Task HEADERS_Received_AuthorityOverridesInvalidHost_200Status() await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 47, + withLength: 46, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -570,7 +570,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -611,7 +611,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -655,7 +655,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[8], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -698,7 +698,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[8], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -751,7 +751,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[8], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -983,7 +983,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1015,7 +1015,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.RST_STREAM, @@ -1054,7 +1054,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1092,7 +1092,7 @@ await InitializeConnectionAsync(context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1125,7 +1125,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1160,7 +1160,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1198,7 +1198,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1236,7 +1236,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1276,7 +1276,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1323,7 +1323,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1361,7 +1361,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1397,7 +1397,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1441,7 +1441,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1475,7 +1475,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1508,7 +1508,7 @@ await InitializeConnectionAsync(context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -1552,7 +1552,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1591,7 +1591,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 41, + withLength: 40, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1634,7 +1634,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1674,7 +1674,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[6], endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 41, + withLength: 40, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1733,7 +1733,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[6], endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 41, + withLength: 40, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1788,7 +1788,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(1, new byte[12], endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -1814,7 +1814,7 @@ await InitializeConnectionAsync(context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1852,7 +1852,7 @@ await InitializeConnectionAsync(context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_STREAM | Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); @@ -1883,7 +1883,7 @@ await InitializeConnectionAsync(context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame1 = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var trailersFrame1 = await ExpectAsync(Http2FrameType.HEADERS, @@ -1894,12 +1894,12 @@ await InitializeConnectionAsync(context => await StartStreamAsync(3, _browserRequestHeaders, endStream: true); var headersFrame2 = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 3); var trailersFrame2 = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 25, + withLength: 1, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 3); @@ -1930,7 +1930,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -1980,7 +1980,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2039,7 +2039,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2074,7 +2074,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2124,7 +2124,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true).DefaultTimeout(); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1).DefaultTimeout(); @@ -2189,7 +2189,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2235,7 +2235,7 @@ await InitializeConnectionAsync(context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -2269,7 +2269,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -2532,7 +2532,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -2623,7 +2623,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2671,7 +2671,7 @@ await InitializeConnectionAsync(async context => // Just the StatusCode gets written before aborting in the continuation frame await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.NONE, withStreamId: 1); @@ -2700,7 +2700,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -2743,7 +2743,7 @@ await ExpectAsync(Http2FrameType.SETTINGS, await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -2789,7 +2789,7 @@ await InitializeConnectionAsync(httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -2835,7 +2835,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -2884,7 +2884,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -2937,7 +2937,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -2987,7 +2987,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3037,7 +3037,7 @@ void NonAsyncMethod() await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3080,7 +3080,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3126,7 +3126,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); var dataFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3168,7 +3168,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3213,7 +3213,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3279,7 +3279,7 @@ void NonAsyncMethod() await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3325,7 +3325,7 @@ await InitializeConnectionAsync(httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3361,7 +3361,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3413,7 +3413,7 @@ await InitializeConnectionAsync(async httpContext => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -3465,7 +3465,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3498,7 +3498,7 @@ await InitializeConnectionAsync(async context => // Don't receive content length because we called WriteAsync which caused an invalid response var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS | (byte)Http2HeadersFrameFlags.END_STREAM, withStreamId: 1); @@ -3531,7 +3531,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3583,7 +3583,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3639,7 +3639,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS, @@ -3705,7 +3705,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3761,7 +3761,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3826,7 +3826,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -3885,7 +3885,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -3941,7 +3941,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -4003,7 +4003,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4077,7 +4077,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4153,7 +4153,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4224,7 +4224,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 38, + withLength: 37, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4296,7 +4296,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4380,7 +4380,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4461,7 +4461,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4548,7 +4548,7 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, headers, endStream: false); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), withStreamId: 1); var bodyFrame = await ExpectAsync(Http2FrameType.DATA, @@ -4608,7 +4608,7 @@ await InitializeConnectionAsync(context => await StartStreamAsync(1, LatinHeaderData, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index f80e5ad386e7..2caa916e948d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -24,6 +24,7 @@ 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; @@ -121,6 +122,7 @@ protected static IEnumerable> ReadRateRequestHeader internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); internal readonly HPackDecoder _hpackDecoder; + internal readonly Http2HPackEncoder _hpackEncoder; private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize]; internal readonly TimeoutControl _timeoutControl; @@ -165,6 +167,7 @@ protected static IEnumerable> ReadRateRequestHeader public Http2TestBase() { _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize); + _hpackEncoder = new Http2HPackEncoder(); _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object); _mockTimeoutControl = new Mock(_timeoutControl) { CallBase = true }; @@ -501,7 +504,7 @@ protected Task StartStreamAsync(int streamId, IEnumerable(TaskCreationOptions.RunContinuationsAsynchronously); _runningStreams[streamId] = tcs; - writableBuffer.WriteStartStream(streamId, GetHeadersEnumerator(headers), _headerEncodingBuffer, endStream); + writableBuffer.WriteStartStream(streamId, _hpackEncoder, GetHeadersEnumerator(headers), _headerEncodingBuffer, endStream); return FlushAsync(writableBuffer); } @@ -541,7 +544,7 @@ protected Task SendHeadersWithPaddingAsync(int streamId, IEnumerable SendHeadersAsync(int streamId, Http2HeadersFrameFlags frame.PrepareHeaders(flags, streamId); var buffer = _headerEncodingBuffer.AsMemory(); - var done = HPackHeaderWriter.BeginEncodeHeaders(headersEnumerator, buffer.Span, out var length); + var done = _hpackEncoder.BeginEncodeHeaders(headersEnumerator, buffer.Span, out var length); frame.PayloadLength = length; Http2FrameWriter.WriteHeader(frame, outputWriter); @@ -815,7 +818,7 @@ internal async Task SendContinuationAsync(int streamId, Http2ContinuationF frame.PrepareContinuation(flags, streamId); var buffer = _headerEncodingBuffer.AsMemory(); - var done = HPackHeaderWriter.ContinueEncodeHeaders(headersEnumerator, buffer.Span, out var length); + var done = _hpackEncoder.ContinueEncodeHeaders(headersEnumerator, buffer.Span, out var length); frame.PayloadLength = length; Http2FrameWriter.WriteHeader(frame, outputWriter); @@ -843,7 +846,7 @@ internal async Task SendContinuationAsync(int streamId, Http2ContinuationF frame.PrepareContinuation(flags, streamId); var buffer = _headerEncodingBuffer.AsMemory(); - var done = HPackHeaderWriter.BeginEncodeHeaders(GetHeadersEnumerator(headers), buffer.Span, out var length); + var done = _hpackEncoder.BeginEncodeHeaders(GetHeadersEnumerator(headers), buffer.Span, out var length); frame.PayloadLength = length; Http2FrameWriter.WriteHeader(frame, outputWriter); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs index 087664a7e069..4432e85dc601 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs @@ -101,7 +101,7 @@ public async Task HEADERS_NotReceivedAfterFirstRequest_WithinKeepAliveTimeout_Cl _mockTimeoutControl.Verify(c => c.SetTimeout(It.IsAny(), TimeoutReason.RequestHeaders), Times.Once); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 36, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -283,7 +283,7 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsC await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -336,7 +336,7 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsC await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -390,7 +390,7 @@ public async Task DATA_Sent_TooSlowlyDueToFlowControlOnSmallWrite_AbortsConnecti await SendDataAsync(1, _helloWorldBytes, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -445,7 +445,7 @@ public async Task DATA_Sent_TooSlowlyDueToOutputFlowControlOnLargeWrite_AbortsCo await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -501,7 +501,7 @@ public async Task DATA_Sent_TooSlowlyDueToOutputFlowControlOnMultipleStreams_Abo await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); await ExpectAsync(Http2FrameType.DATA, @@ -513,7 +513,7 @@ await ExpectAsync(Http2FrameType.DATA, await SendDataAsync(3, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -567,7 +567,7 @@ public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGraceP await SendDataAsync(1, _helloWorldBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -616,7 +616,7 @@ public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTi await SendDataAsync(1, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -669,7 +669,7 @@ public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfter await SendDataAsync(1, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -682,7 +682,7 @@ await ExpectAsync(Http2FrameType.DATA, await SendDataAsync(3, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -738,7 +738,7 @@ public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNon await SendDataAsync(1, _maxData, endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -756,7 +756,7 @@ await ExpectAsync(Http2FrameType.DATA, await SendDataAsync(3, _maxData, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -813,7 +813,7 @@ await InitializeConnectionAsync(context => await SendDataAsync(1, _helloWorldBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 1); @@ -885,7 +885,7 @@ await InitializeConnectionAsync(async context => await SendDataAsync(3, _helloWorldBytes, endStream: false); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 32, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 3); await ExpectAsync(Http2FrameType.DATA, @@ -902,7 +902,7 @@ await ExpectAsync(Http2FrameType.DATA, backpressureTcs.SetResult(null); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 37, + withLength: 6, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs index 18faacc44d28..f53e083bc3da 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs @@ -1118,7 +1118,7 @@ public async Task ResponseHeaders_MultipleFrames_Accepted(string scheme) Assert.Equal(oneKbString + i, response.Headers.GetValues("header" + i).Single()); } - Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending HEADERS frame for stream ID 1 with length 15612 and flags END_STREAM"))); + Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending HEADERS frame for stream ID 1 with length 15610 and flags END_STREAM"))); Assert.Equal(2, TestSink.Writes.Where(context => context.Message.Contains("sending CONTINUATION frame for stream ID 1 with length 15585 and flags NONE")).Count()); Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending CONTINUATION frame for stream ID 1 with length 14546 and flags END_HEADERS"))); diff --git a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs index d09f1841349d..3540918d99de 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs @@ -109,6 +109,70 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, string val return false; } + /// Encodes a "Literal Header Field never Indexing". + public static bool EncodeLiteralHeaderFieldNeverIndexing(int index, string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.3 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | Index (4+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + if ((uint)destination.Length >= 2) + { + destination[0] = 0x10; + 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 with Indexing". + public static bool EncodeLiteralHeaderFieldIndexing(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 | 1 | Index (6+) | + // +---+---+-----------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + if ((uint)destination.Length >= 2) + { + destination[0] = 0x40; + if (IntegerEncoder.Encode(index, 6, 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 EncodeStringLiteral must be used to encode the associated value. @@ -144,6 +208,27 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexing(int index, Span return false; } + /// Encodes a "Literal Header Field with Indexing - New Name". + public static bool EncodeLiteralHeaderFieldIndexingNewName(string name, 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 | 1 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + return EncodeLiteralHeaderNewNameCore(0x40, name, value, destination, out bytesWritten); + } + /// Encodes a "Literal Header Field without Indexing - New Name". public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, string value, Span destination, out int bytesWritten) { @@ -162,9 +247,35 @@ public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, s // | Value String (Length octets) | // +-------------------------------+ + return EncodeLiteralHeaderNewNameCore(0, name, value, destination, out bytesWritten); + } + + /// Encodes a "Literal Header Field never Indexing - New Name". + public static bool EncodeLiteralHeaderFieldNeverIndexingNewName(string name, string value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.2.3 + // ------------------------------------------------------ + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 0 | 1 | 0 | + // +---+---+-----------------------+ + // | H | Name Length (7+) | + // +---+---------------------------+ + // | Name String (Length octets) | + // +---+---------------------------+ + // | H | Value Length (7+) | + // +---+---------------------------+ + // | Value String (Length octets) | + // +-------------------------------+ + + return EncodeLiteralHeaderNewNameCore(0x10, name, value, destination, out bytesWritten); + } + + private static bool EncodeLiteralHeaderNewNameCore(byte mask, string name, string value, Span destination, out int bytesWritten) + { if ((uint)destination.Length >= 3) { - destination[0] = 0; + destination[0] = mask; if (EncodeLiteralHeaderName(name, destination.Slice(1), out int nameLength) && EncodeStringLiteral(value, destination.Slice(1 + nameLength), out int valueLength)) { @@ -372,6 +483,25 @@ public static bool EncodeStringLiteral(string value, Span destination, out return false; } + public static bool EncodeDynamicTableSizeUpdate(int value, Span destination, out int bytesWritten) + { + // From https://tools.ietf.org/html/rfc7541#section-6.3 + // ---------------------------------------------------- + // 0 1 2 3 4 5 6 7 + // +---+---+---+---+---+---+---+---+ + // | 0 | 0 | 1 | Max size (5+) | + // +---+---------------------------+ + + if (destination.Length != 0) + { + destination[0] = 0x20; + return IntegerEncoder.Encode(value, 5, destination, out bytesWritten); + } + + bytesWritten = 0; + return false; + } + public static bool EncodeStringLiterals(ReadOnlySpan values, string? separator, Span destination, out int bytesWritten) { bytesWritten = 0; From fbf3235df30c3f7ed54a438d8d7209ef67451eff Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 29 Mar 2020 12:29:46 +1300 Subject: [PATCH 02/14] Move dynamic compression to shared code --- .../src/Internal/Http2/HPack/StatusCodes.cs | 151 -------------- .../src/Internal/Http2/HPackHeaderWriter.cs | 173 ++++++++++++++++ .../src/Internal/Http2/Http2FrameWriter.cs | 10 +- .../Core/test/Http2HPackEncoderTests.cs | 65 +++--- .../Http2ConnectionBenchmarkBase.cs | 6 +- .../test/PipeWriterHttp2FrameExtensions.cs | 7 +- .../Http2/Http2TestBase.cs | 17 +- .../Http2/Hpack/HPackEncoder.Dynamic.cs} | 192 ++++-------------- .../runtime/Http2/Hpack/HPackEncoder.cs | 2 +- .../runtime/Http2/Hpack}/HPackHeaderEntry.cs | 3 +- src/Shared/runtime/Http2/Hpack/StatusCodes.cs | 139 +++++++++++++ 11 files changed, 402 insertions(+), 363 deletions(-) delete mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs rename src/{Servers/Kestrel/Core/src/Internal/Http2/HPack/Http2HPackEncoder.cs => Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs} (54%) rename src/{Servers/Kestrel/Core/src/Internal/Http2/HPack => Shared/runtime/Http2/Hpack}/HPackHeaderEntry.cs (95%) 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 6ca14f75c154..000000000000 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/StatusCodes.cs +++ /dev/null @@ -1,151 +0,0 @@ -// 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.Globalization; -using System.Net; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack -{ - internal static class StatusCodes - { - public static string ToStatusString(int statusCode) - { - switch (statusCode) - { - case (int)HttpStatusCode.Continue: - return "100"; - case (int)HttpStatusCode.SwitchingProtocols: - return "101"; - case (int)HttpStatusCode.Processing: - return "102"; - - case (int)HttpStatusCode.OK: - return "200"; - case (int)HttpStatusCode.Created: - return "201"; - case (int)HttpStatusCode.Accepted: - return "202"; - case (int)HttpStatusCode.NonAuthoritativeInformation: - return "203"; - case (int)HttpStatusCode.NoContent: - return "204"; - case (int)HttpStatusCode.ResetContent: - return "205"; - case (int)HttpStatusCode.PartialContent: - return "206"; - case (int)HttpStatusCode.MultiStatus: - return "207"; - case (int)HttpStatusCode.AlreadyReported: - return "208"; - case (int)HttpStatusCode.IMUsed: - return "226"; - - case (int)HttpStatusCode.MultipleChoices: - return "300"; - case (int)HttpStatusCode.MovedPermanently: - return "301"; - case (int)HttpStatusCode.Found: - return "302"; - case (int)HttpStatusCode.SeeOther: - return "303"; - case (int)HttpStatusCode.NotModified: - return "304"; - case (int)HttpStatusCode.UseProxy: - return "305"; - case (int)HttpStatusCode.Unused: - return "306"; - case (int)HttpStatusCode.TemporaryRedirect: - return "307"; - case (int)HttpStatusCode.PermanentRedirect: - return "308"; - - case (int)HttpStatusCode.BadRequest: - return "400"; - case (int)HttpStatusCode.Unauthorized: - return "401"; - case (int)HttpStatusCode.PaymentRequired: - return "402"; - case (int)HttpStatusCode.Forbidden: - return "403"; - case (int)HttpStatusCode.NotFound: - return "404"; - case (int)HttpStatusCode.MethodNotAllowed: - return "405"; - case (int)HttpStatusCode.NotAcceptable: - return "406"; - case (int)HttpStatusCode.ProxyAuthenticationRequired: - return "407"; - case (int)HttpStatusCode.RequestTimeout: - return "408"; - case (int)HttpStatusCode.Conflict: - return "409"; - case (int)HttpStatusCode.Gone: - return "410"; - case (int)HttpStatusCode.LengthRequired: - return "411"; - case (int)HttpStatusCode.PreconditionFailed: - return "412"; - case (int)HttpStatusCode.RequestEntityTooLarge: - return "413"; - case (int)HttpStatusCode.RequestUriTooLong: - return "414"; - case (int)HttpStatusCode.UnsupportedMediaType: - return "415"; - case (int)HttpStatusCode.RequestedRangeNotSatisfiable: - return "416"; - case (int)HttpStatusCode.ExpectationFailed: - return "417"; - case (int)418: - return "418"; - case (int)419: - return "419"; - case (int)HttpStatusCode.MisdirectedRequest: - return "421"; - case (int)HttpStatusCode.UnprocessableEntity: - return "422"; - case (int)HttpStatusCode.Locked: - return "423"; - case (int)HttpStatusCode.FailedDependency: - return "424"; - case (int)HttpStatusCode.UpgradeRequired: - return "426"; - case (int)HttpStatusCode.PreconditionRequired: - return "428"; - case (int)HttpStatusCode.TooManyRequests: - return "429"; - case (int)HttpStatusCode.RequestHeaderFieldsTooLarge: - return "431"; - case (int)HttpStatusCode.UnavailableForLegalReasons: - return "451"; - - case (int)HttpStatusCode.InternalServerError: - return "500"; - case (int)HttpStatusCode.NotImplemented: - return "501"; - case (int)HttpStatusCode.BadGateway: - return "502"; - case (int)HttpStatusCode.ServiceUnavailable: - return "503"; - case (int)HttpStatusCode.GatewayTimeout: - return "504"; - case (int)HttpStatusCode.HttpVersionNotSupported: - return "505"; - case (int)HttpStatusCode.VariantAlsoNegotiates: - return "506"; - case (int)HttpStatusCode.InsufficientStorage: - return "507"; - case (int)HttpStatusCode.LoopDetected: - return "508"; - case (int)HttpStatusCode.NotExtended: - return "510"; - case (int)HttpStatusCode.NetworkAuthenticationRequired: - return "511"; - - default: - return statusCode.ToString(CultureInfo.InvariantCulture); - - } - } - } -} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs new file mode 100644 index 000000000000..33c7b920f395 --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs @@ -0,0 +1,173 @@ +// 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.Net.Http; +using System.Net.Http.HPack; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 +{ + internal static class HPackHeaderWriter + { + /// + /// Begin encoding headers in the first HEADERS frame. + /// + public static bool BeginEncodeHeaders(int statusCode, HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + { + length = 0; + + if (!hpackEncoder.EnsureDynamicTableSizeUpdate(buffer, out var sizeUpdateLength)) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + length += sizeUpdateLength; + + if (!EncodeStatusHeader(statusCode, hpackEncoder, buffer.Slice(length), out var statusCodeLength)) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + length += statusCodeLength; + + if (!headersEnumerator.MoveNext()) + { + return true; + } + + // We're ok with not throwing if no headers were encoded because we've already encoded the status. + // There is a small chance that the header will encode if there is no other content in the next HEADERS frame. + var done = EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: false, out var headersLength); + length += headersLength; + return done; + } + + /// + /// Begin encoding headers in the first HEADERS frame. + /// + public static bool BeginEncodeHeaders(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + { + length = 0; + + if (!hpackEncoder.EnsureDynamicTableSizeUpdate(buffer, out var sizeUpdateLength)) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + length += sizeUpdateLength; + + if (!headersEnumerator.MoveNext()) + { + return true; + } + + var done = EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: true, out var headersLength); + length += headersLength; + return done; + } + + /// + /// Continue encoding headers in the next HEADERS frame. The enumerator should already have a current value. + /// + public static bool ContinueEncodeHeaders(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + { + return EncodeHeadersCore(hpackEncoder, headersEnumerator, buffer, throwIfNoneEncoded: true, out length); + } + + private static bool EncodeStatusHeader(int statusCode, HPackEncoder hpackEncoder, Span buffer, out int length) + { + switch (statusCode) + { + case 200: + case 204: + case 206: + case 304: + case 400: + case 404: + case 500: + // Status codes which exist in the HTTP/2 StaticTable. + return HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], buffer, out length); + default: + const string name = ":status"; + var value = StatusCodes.ToStatusString(statusCode); + return hpackEncoder.EncodeHeader(buffer, H2StaticTable.Status200, HeaderEncodingHint.Index, name, value, out length); + } + } + + private static bool EncodeHeadersCore(HPackEncoder hpackEncoder, Http2HeadersEnumerator headersEnumerator, Span buffer, bool throwIfNoneEncoded, out int length) + { + var currentLength = 0; + do + { + var staticTableId = headersEnumerator.HPackStaticTableId; + var name = headersEnumerator.Current.Key; + var value = headersEnumerator.Current.Value; + + var hint = ResolveHeaderEncodingHint(staticTableId, name); + + if (!hpackEncoder.EncodeHeader( + buffer.Slice(currentLength), + staticTableId, + hint, + name, + value, + out var headerLength)) + { + // If the header wasn't written, and no headers have been written, then the header is too large. + // Throw an error to avoid an infinite loop of attempting to write large header. + if (currentLength == 0 && throwIfNoneEncoded) + { + throw new HPackEncodingException(SR.net_http_hpack_encode_failure); + } + + length = currentLength; + return false; + } + + currentLength += headerLength; + } + while (headersEnumerator.MoveNext()); + + length = currentLength; + return true; + } + + private static HeaderEncodingHint ResolveHeaderEncodingHint(int staticTableId, string name) + { + HeaderEncodingHint hint; + if (IsSensitive(staticTableId, name)) + { + hint = HeaderEncodingHint.NeverIndex; + } + else if (IsNotDynamicallyIndexed(staticTableId)) + { + hint = HeaderEncodingHint.IgnoreIndex; + } + else + { + hint = HeaderEncodingHint.Index; + } + + return hint; + } + + private static bool IsSensitive(int staticTableIndex, string name) + { + // Set-Cookie could contain sensitive data. + if (staticTableIndex == H2StaticTable.SetCookie) + { + return true; + } + if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + private static bool IsNotDynamicallyIndexed(int staticTableIndex) + { + // Content-Length is added to static content. Content length is different for each + // file, and is unlikely to be reused because of browser caching. + return staticTableIndex == H2StaticTable.ContentLength; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 76bb3d732cc5..6d5c519017f3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -39,7 +39,7 @@ internal class Http2FrameWriter private readonly ITimeoutControl _timeoutControl; private readonly MinDataRate _minResponseDataRate; private readonly TimingPipeFlusher _flusher; - private readonly Http2HPackEncoder _hpackEncoder; + private readonly HPackEncoder _hpackEncoder; private uint _maxFrameSize = Http2PeerSettings.MinAllowedMaxFrameSize; private byte[] _headerEncodingBuffer; @@ -72,7 +72,7 @@ public Http2FrameWriter( _outgoingFrame = new Http2Frame(); _headerEncodingBuffer = new byte[_maxFrameSize]; - _hpackEncoder = new Http2HPackEncoder(serviceContext.ServerOptions.DisableResponseDynamicHeaderCompression); + _hpackEncoder = new HPackEncoder(serviceContext.ServerOptions.DisableResponseDynamicHeaderCompression); } public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) @@ -187,7 +187,7 @@ public void WriteResponseHeaders(int streamId, int statusCode, Http2HeadersFrame _headersEnumerator.Initialize(headers); _outgoingFrame.PrepareHeaders(headerFrameFlags, streamId); var buffer = _headerEncodingBuffer.AsSpan(); - var done = _hpackEncoder.BeginEncodeHeaders(statusCode, _headersEnumerator, buffer, out var payloadLength); + var done = HPackHeaderWriter.BeginEncodeHeaders(statusCode, _hpackEncoder, _headersEnumerator, buffer, out var payloadLength); FinishWritingHeaders(streamId, payloadLength, done); } catch (HPackEncodingException hex) @@ -213,7 +213,7 @@ public ValueTask WriteResponseTrailers(int streamId, HttpResponseTr _headersEnumerator.Initialize(headers); _outgoingFrame.PrepareHeaders(Http2HeadersFrameFlags.END_STREAM, streamId); var buffer = _headerEncodingBuffer.AsSpan(); - var done = _hpackEncoder.BeginEncodeHeaders(_headersEnumerator, buffer, out var payloadLength); + var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out var payloadLength); FinishWritingHeaders(streamId, payloadLength, done); } catch (HPackEncodingException hex) @@ -242,7 +242,7 @@ private void FinishWritingHeaders(int streamId, int payloadLength, bool done) { _outgoingFrame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - done = _hpackEncoder.ContinueEncodeHeaders(_headersEnumerator, buffer, out payloadLength); + done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, _headersEnumerator, buffer, out payloadLength); _outgoingFrame.PayloadLength = payloadLength; if (done) diff --git a/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs index b562379e90a0..6322c1a62459 100644 --- a/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs +++ b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs @@ -10,7 +10,6 @@ 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.Extensions.Primitives; using Xunit; @@ -27,14 +26,14 @@ public void BeginEncodeHeaders_Status302_NewIndexValue() var enumerator = new Http2HeadersEnumerator(); enumerator.Initialize(headers); - var http2HPackEncoder = new Http2HPackEncoder(); - Assert.True(http2HPackEncoder.BeginEncodeHeaders(302, enumerator, buffer, out var length)); + var hpackEncoder = new HPackEncoder(); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length)); var result = buffer.Slice(0, length).ToArray(); var hex = BitConverter.ToString(result); Assert.Equal("48-03-33-30-32", hex); - var statusHeader = GetHeaderEntry(http2HPackEncoder, 0); + var statusHeader = GetHeaderEntry(hpackEncoder, 0); Assert.Equal(":status", statusHeader.Name); Assert.Equal("302", statusHeader.Value); } @@ -50,14 +49,14 @@ public void BeginEncodeHeaders_CacheControlPrivate_NewIndexValue() var enumerator = new Http2HeadersEnumerator(); enumerator.Initialize(headers); - var http2HPackEncoder = new Http2HPackEncoder(); - Assert.True(http2HPackEncoder.BeginEncodeHeaders(302, enumerator, buffer, out var length)); + var hpackEncoder = new HPackEncoder(); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length)); var result = buffer.Slice(5, length - 5).ToArray(); var hex = BitConverter.ToString(result); Assert.Equal("58-07-70-72-69-76-61-74-65", hex); - var statusHeader = GetHeaderEntry(http2HPackEncoder, 0); + var statusHeader = GetHeaderEntry(hpackEncoder, 0); Assert.Equal("Cache-Control", statusHeader.Name); Assert.Equal("private", statusHeader.Value); } @@ -76,11 +75,11 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() var enumerator = new Http2HeadersEnumerator(); - var http2HPackEncoder = new Http2HPackEncoder(maxHeaderTableSize: 256); + var hpackEncoder = new HPackEncoder(maxHeaderTableSize: 256); // First response enumerator.Initialize(headers); - Assert.True(http2HPackEncoder.BeginEncodeHeaders(302, enumerator, buffer, out var length)); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(302, hpackEncoder, enumerator, buffer, out var length)); var result = buffer.Slice(0, length).ToArray(); var hex = BitConverter.ToString(result); @@ -91,7 +90,7 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() "74-74-70-73-3A-2F-2F-77-77-77-2E-65-78-61-6D-70-" + "6C-65-2E-63-6F-6D", hex); - var entries = GetHeaderEntries(http2HPackEncoder); + var entries = GetHeaderEntries(hpackEncoder); Assert.Collection(entries, e => { @@ -116,13 +115,13 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() // Second response enumerator.Initialize(headers); - Assert.True(http2HPackEncoder.BeginEncodeHeaders(307, enumerator, buffer, out length)); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(307, hpackEncoder, enumerator, buffer, out length)); result = buffer.Slice(0, length).ToArray(); hex = BitConverter.ToString(result); Assert.Equal("48-03-33-30-37-C1-C0-BF", hex); - entries = GetHeaderEntries(http2HPackEncoder); + entries = GetHeaderEntries(hpackEncoder); Assert.Collection(entries, e => { @@ -151,7 +150,7 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() headers.HeaderSetCookie = "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"; enumerator.Initialize(headers); - Assert.True(http2HPackEncoder.BeginEncodeHeaders(200, enumerator, buffer, out length)); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out length)); result = buffer.Slice(0, length).ToArray(); hex = BitConverter.ToString(result); @@ -164,7 +163,7 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeExceeded_EvictionsToFit() "61-67-65-3D-33-36-30-30-3B-20-76-65-72-73-69-6F-" + "6E-3D-31", hex); - entries = GetHeaderEntries(http2HPackEncoder); + entries = GetHeaderEntries(hpackEncoder); Assert.Collection(entries, e => { @@ -202,8 +201,8 @@ public void BeginEncodeHeaders_ExcludedHeaders_NotAddedToTable(string headerName var enumerator = new Http2HeadersEnumerator(); enumerator.Initialize(headers); - var http2HPackEncoder = new Http2HPackEncoder(maxHeaderTableSize: Http2PeerSettings.DefaultHeaderTableSize); - Assert.True(http2HPackEncoder.BeginEncodeHeaders(enumerator, buffer, out _)); + var hpackEncoder = new HPackEncoder(maxHeaderTableSize: Http2PeerSettings.DefaultHeaderTableSize); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out _)); if (neverIndex) { @@ -214,7 +213,7 @@ public void BeginEncodeHeaders_ExcludedHeaders_NotAddedToTable(string headerName Assert.Equal(0, buffer[0] & 0x40); } - Assert.Empty(GetHeaderEntries(http2HPackEncoder)); + Assert.Empty(GetHeaderEntries(hpackEncoder)); } [Fact] @@ -228,10 +227,10 @@ public void BeginEncodeHeaders_HeaderExceedHeaderTableSize_NoIndexAndNoHeaderEnt var enumerator = new Http2HeadersEnumerator(); enumerator.Initialize(headers); - var http2HPackEncoder = new Http2HPackEncoder(); - Assert.True(http2HPackEncoder.BeginEncodeHeaders(200, enumerator, buffer, out var length)); + var hpackEncoder = new HPackEncoder(); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(200, hpackEncoder, enumerator, buffer, out var length)); - Assert.Empty(GetHeaderEntries(http2HPackEncoder)); + Assert.Empty(GetHeaderEntries(hpackEncoder)); } public static TheoryData[], byte[], int?> SinglePayloadData @@ -313,17 +312,17 @@ public void BeginEncodeHeaders_HeaderExceedHeaderTableSize_NoIndexAndNoHeaderEnt [MemberData(nameof(SinglePayloadData))] public void EncodesHeadersInSinglePayloadWhenSpaceAvailable(KeyValuePair[] headers, byte[] expectedPayload, int? statusCode) { - Http2HPackEncoder http2HPackEncoder = new Http2HPackEncoder(); + HPackEncoder hpackEncoder = new HPackEncoder(); var payload = new byte[1024]; var length = 0; if (statusCode.HasValue) { - Assert.True(http2HPackEncoder.BeginEncodeHeaders(statusCode.Value, GetHeadersEnumerator(headers), payload, out length)); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(statusCode.Value, hpackEncoder, GetHeadersEnumerator(headers), payload, out length)); } else { - Assert.True(http2HPackEncoder.BeginEncodeHeaders(GetHeadersEnumerator(headers), payload, out length)); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, GetHeadersEnumerator(headers), payload, out length)); } Assert.Equal(expectedPayload.Length, length); @@ -377,7 +376,7 @@ public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize 0x07, 0x4b, 0x65, 0x73, 0x74, 0x72, 0x65, 0x6c }; - var http2HPackEncoder = new Http2HPackEncoder(); + var hpackEncoder = new HPackEncoder(); Span payload = new byte[1024]; var offset = 0; @@ -385,28 +384,28 @@ public void EncodesHeadersInMultiplePayloadsWhenSpaceNotAvailable(bool exactSize // When !exactSize, slices are one byte short of fitting the next header var sliceLength = expectedStatusCodePayload.Length + (exactSize ? 0 : expectedDateHeaderPayload.Length - 1); - Assert.False(http2HPackEncoder.BeginEncodeHeaders(statusCode, headerEnumerator, payload.Slice(offset, sliceLength), out var length)); + Assert.False(HPackHeaderWriter.BeginEncodeHeaders(statusCode, hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out var length)); Assert.Equal(expectedStatusCodePayload.Length, length); Assert.Equal(expectedStatusCodePayload, payload.Slice(0, length).ToArray()); offset += length; sliceLength = expectedDateHeaderPayload.Length + (exactSize ? 0 : expectedContentTypeHeaderPayload.Length - 1); - Assert.False(http2HPackEncoder.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); + Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length)); Assert.Equal(expectedDateHeaderPayload.Length, length); Assert.Equal(expectedDateHeaderPayload, payload.Slice(offset, length).ToArray()); offset += length; sliceLength = expectedContentTypeHeaderPayload.Length + (exactSize ? 0 : expectedServerHeaderPayload.Length - 1); - Assert.False(http2HPackEncoder.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); + Assert.False(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length)); Assert.Equal(expectedContentTypeHeaderPayload.Length, length); Assert.Equal(expectedContentTypeHeaderPayload, payload.Slice(offset, length).ToArray()); offset += length; sliceLength = expectedServerHeaderPayload.Length; - Assert.True(http2HPackEncoder.ContinueEncodeHeaders(headerEnumerator, payload.Slice(offset, sliceLength), out length)); + Assert.True(HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headerEnumerator, payload.Slice(offset, sliceLength), out length)); Assert.Equal(expectedServerHeaderPayload.Length, length); Assert.Equal(expectedServerHeaderPayload, payload.Slice(offset, length).ToArray()); } @@ -416,14 +415,14 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeUpdated_SizeUpdateInHeaders() { Span buffer = new byte[1024 * 16]; - var hpackEncoder = new Http2HPackEncoder(); + var hpackEncoder = new HPackEncoder(); hpackEncoder.UpdateMaxHeaderTableSize(100); var enumerator = new Http2HeadersEnumerator(); // First request enumerator.Initialize(new Dictionary()); - Assert.True(hpackEncoder.BeginEncodeHeaders(enumerator, buffer, out var length)); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out var length)); Assert.Equal(2, length); @@ -437,7 +436,7 @@ public void BeginEncodeHeaders_MaxHeaderTableSizeUpdated_SizeUpdateInHeaders() // Second request enumerator.Initialize(new Dictionary()); - Assert.True(hpackEncoder.BeginEncodeHeaders(enumerator, buffer, out length)); + Assert.True(HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, enumerator, buffer, out length)); Assert.Equal(0, length); } @@ -453,7 +452,7 @@ private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable= 0) @@ -463,7 +462,7 @@ private HPackHeaderEntry GetHeaderEntry(Http2HPackEncoder encoder, int index) return entry; } - private List GetHeaderEntries(Http2HPackEncoder encoder) + private List GetHeaderEntries(HPackEncoder encoder) { var headers = new List(); diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs index 800dd6584375..3886a38b09d0 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs @@ -8,6 +8,7 @@ using System.IO; using System.IO.Pipelines; using System.Linq; +using System.Net.Http.HPack; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; @@ -16,7 +17,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.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging.Abstractions; @@ -30,7 +30,7 @@ public abstract class Http2ConnectionBenchmarkBase private MemoryPool _memoryPool; private HttpRequestHeaders _httpRequestHeaders; private Http2Connection _connection; - private Http2HPackEncoder _hpackEncoder; + private HPackEncoder _hpackEncoder; private Http2HeadersEnumerator _requestHeadersEnumerator; private int _currentStreamId; private byte[] _headersBuffer; @@ -56,7 +56,7 @@ public virtual void GlobalSetup() _httpRequestHeaders.Append(HeaderNames.Authority, new StringValues("localhost:80")); _headersBuffer = new byte[1024 * 16]; - _hpackEncoder = new Http2HPackEncoder(); + _hpackEncoder = new HPackEncoder(); var serviceContext = new ServiceContext { diff --git a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs index a7ed4ebdfd10..a99db7dfe4e5 100644 --- a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs +++ b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs @@ -8,7 +8,6 @@ using System.IO.Pipelines; using System.Net.Http.HPack; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack; namespace Microsoft.AspNetCore.Testing { @@ -26,13 +25,13 @@ public static void WriteSettings(this PipeWriter writer, Http2PeerSettings clien writer.Write(payload); } - public static void WriteStartStream(this PipeWriter writer, int streamId, Http2HPackEncoder hpackEncoder, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream, Http2Frame frame = null) + public static void WriteStartStream(this PipeWriter writer, int streamId, HPackEncoder hpackEncoder, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream, Http2Frame frame = null) { frame ??= new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); var buffer = headerEncodingBuffer.AsSpan(); - var done = hpackEncoder.BeginEncodeHeaders(headers, buffer, out var length); + var done = HPackHeaderWriter.BeginEncodeHeaders(hpackEncoder, headers, buffer, out var length); frame.PayloadLength = length; if (done) @@ -52,7 +51,7 @@ public static void WriteStartStream(this PipeWriter writer, int streamId, Http2H { frame.PrepareContinuation(Http2ContinuationFrameFlags.NONE, streamId); - done = hpackEncoder.ContinueEncodeHeaders(headers, buffer, out length); + done = HPackHeaderWriter.ContinueEncodeHeaders(hpackEncoder, headers, buffer, out length); frame.PayloadLength = length; if (done) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 2caa916e948d..03b5e277ed88 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -24,7 +24,6 @@ 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; @@ -122,7 +121,7 @@ protected static IEnumerable> ReadRateRequestHeader internal readonly Http2PeerSettings _clientSettings = new Http2PeerSettings(); internal readonly HPackDecoder _hpackDecoder; - internal readonly Http2HPackEncoder _hpackEncoder; + internal readonly HPackEncoder _hpackEncoder; private readonly byte[] _headerEncodingBuffer = new byte[Http2PeerSettings.MinAllowedMaxFrameSize]; internal readonly TimeoutControl _timeoutControl; @@ -167,7 +166,7 @@ protected static IEnumerable> ReadRateRequestHeader public Http2TestBase() { _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize); - _hpackEncoder = new Http2HPackEncoder(); + _hpackEncoder = new HPackEncoder(); _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object); _mockTimeoutControl = new Mock(_timeoutControl) { CallBase = true }; @@ -544,7 +543,7 @@ protected Task SendHeadersWithPaddingAsync(int streamId, IEnumerable SendHeadersAsync(int streamId, Http2HeadersFrameFlags frame.PrepareHeaders(flags, streamId); var buffer = _headerEncodingBuffer.AsMemory(); - var done = _hpackEncoder.BeginEncodeHeaders(headersEnumerator, buffer.Span, out var length); + var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, headersEnumerator, buffer.Span, out var length); frame.PayloadLength = length; Http2FrameWriter.WriteHeader(frame, outputWriter); @@ -818,7 +817,7 @@ internal async Task SendContinuationAsync(int streamId, Http2ContinuationF frame.PrepareContinuation(flags, streamId); var buffer = _headerEncodingBuffer.AsMemory(); - var done = _hpackEncoder.ContinueEncodeHeaders(headersEnumerator, buffer.Span, out var length); + var done = HPackHeaderWriter.ContinueEncodeHeaders(_hpackEncoder, headersEnumerator, buffer.Span, out var length); frame.PayloadLength = length; Http2FrameWriter.WriteHeader(frame, outputWriter); @@ -846,7 +845,7 @@ internal async Task SendContinuationAsync(int streamId, Http2ContinuationF frame.PrepareContinuation(flags, streamId); var buffer = _headerEncodingBuffer.AsMemory(); - var done = _hpackEncoder.BeginEncodeHeaders(GetHeadersEnumerator(headers), buffer.Span, out var length); + var done = HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, GetHeadersEnumerator(headers), buffer.Span, out var length); frame.PayloadLength = length; Http2FrameWriter.WriteHeader(frame, outputWriter); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Http2HPackEncoder.cs b/src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs similarity index 54% rename from src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Http2HPackEncoder.cs rename to src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs index e07392af0da0..7f080dabf173 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/Http2HPackEncoder.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs @@ -2,15 +2,16 @@ // 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; +#nullable enable +using System.Collections.Generic; using System.Diagnostics; -using System.Net.Http; -using System.Net.Http.HPack; -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack +namespace System.Net.Http.HPack { - internal class Http2HPackEncoder + internal partial class HPackEncoder { + public const int DefaultHeaderTableSize = 4096; + // Internal for testing internal readonly HPackHeaderEntry Head; @@ -20,9 +21,9 @@ internal class Http2HPackEncoder private uint _headerTableSize; private uint _maxHeaderTableSize; private bool _pendingTableSizeUpdate; - private HPackHeaderEntry _removed; + private HPackHeaderEntry? _removed; - public Http2HPackEncoder(bool disableDynamicCompression = false, uint maxHeaderTableSize = Http2PeerSettings.DefaultHeaderTableSize) + public HPackEncoder(bool disableDynamicCompression = false, uint maxHeaderTableSize = DefaultHeaderTableSize) { _disableDynamicCompression = disableDynamicCompression; _maxHeaderTableSize = maxHeaderTableSize; @@ -47,145 +48,39 @@ public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) } } - /// - /// Begin encoding headers in the first HEADERS frame. - /// - public bool BeginEncodeHeaders(int statusCode, Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) + public bool EnsureDynamicTableSizeUpdate(Span buffer, out int length) { - length = 0; - if (_pendingTableSizeUpdate) { - if (!HPackEncoder.EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out var sizeUpdateLength)) - { - throw new HPackEncodingException(SR.net_http_hpack_encode_failure); - } - length += sizeUpdateLength; + bool success = EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out length); _pendingTableSizeUpdate = false; + return success; } - if (!EncodeStatusHeader(statusCode, buffer.Slice(length), out var statusCodeLength)) - { - throw new HPackEncodingException(SR.net_http_hpack_encode_failure); - } - length += statusCodeLength; - - if (!headersEnumerator.MoveNext()) - { - return true; - } - - // We're ok with not throwing if no headers were encoded because we've already encoded the status. - // There is a small chance that the header will encode if there is no other content in the next HEADERS frame. - var done = EncodeHeadersCore(headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: false, out var headersLength); - length += headersLength; - return done; - } - - /// - /// Begin encoding headers in the first HEADERS frame. - /// - public bool BeginEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) - { length = 0; - - if (_pendingTableSizeUpdate) - { - if (!HPackEncoder.EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out var sizeUpdateLength)) - { - throw new HPackEncodingException(SR.net_http_hpack_encode_failure); - } - length += sizeUpdateLength; - _pendingTableSizeUpdate = false; - } - - if (!headersEnumerator.MoveNext()) - { - return true; - } - - var done = EncodeHeadersCore(headersEnumerator, buffer.Slice(length), throwIfNoneEncoded: true, out var headersLength); - length += headersLength; - return done; - } - - /// - /// Continue encoding headers in the next HEADERS frame. The enumerator should already have a current value. - /// - public bool ContinueEncodeHeaders(Http2HeadersEnumerator headersEnumerator, Span buffer, out int length) - { - return EncodeHeadersCore(headersEnumerator, buffer, throwIfNoneEncoded: true, out length); - } - - private bool EncodeStatusHeader(int statusCode, Span buffer, out int length) - { - switch (statusCode) - { - case 200: - case 204: - case 206: - case 304: - case 400: - case 404: - case 500: - // Status codes which exist in the HTTP/2 StaticTable. - return HPackEncoder.EncodeIndexedHeaderField(H2StaticTable.StatusIndex[statusCode], buffer, out length); - default: - const string name = ":status"; - var value = StatusCodes.ToStatusString(statusCode); - return EncodeHeader(buffer, H2StaticTable.Status200, name, value, out length); - } - } - - private bool EncodeHeadersCore(Http2HeadersEnumerator headersEnumerator, Span buffer, bool throwIfNoneEncoded, out int length) - { - var currentLength = 0; - do - { - if (!EncodeHeader( - buffer.Slice(currentLength), - headersEnumerator.HPackStaticTableId, - headersEnumerator.Current.Key, - headersEnumerator.Current.Value, - out var headerLength)) - { - // If the header wasn't written, and no headers have been written, then the header is too large. - // Throw an error to avoid an infinite loop of attempting to write large header. - if (currentLength == 0 && throwIfNoneEncoded) - { - throw new HPackEncodingException(SR.net_http_hpack_encode_failure); - } - - length = currentLength; - return false; - } - - currentLength += headerLength; - } - while (headersEnumerator.MoveNext()); - - length = currentLength; return true; } - private bool EncodeHeader(Span buffer, int staticTableIndex, string name, string value, out int bytesWritten) + public bool EncodeHeader(Span buffer, int staticTableIndex, HeaderEncodingHint encodingHint, string name, string value, out int bytesWritten) { + Debug.Assert(!_pendingTableSizeUpdate, "Dynamic table size update should be encoded before headers."); + // Never index sensitive value. - if (IsSensitive(staticTableIndex, name)) + if (encodingHint == HeaderEncodingHint.NeverIndex) { var index = ResolveDynamicTableIndex(staticTableIndex, name); return index == -1 - ? HPackEncoder.EncodeLiteralHeaderFieldNeverIndexingNewName(name, value, buffer, out bytesWritten) - : HPackEncoder.EncodeLiteralHeaderFieldNeverIndexing(index, value, buffer, out bytesWritten); + ? EncodeLiteralHeaderFieldNeverIndexingNewName(name, value, buffer, out bytesWritten) + : EncodeLiteralHeaderFieldNeverIndexing(index, value, buffer, out bytesWritten); } // No dynamic table. Only use the static table. - if (_disableDynamicCompression || _maxHeaderTableSize == 0 || IsNotDynamicallyIndexed(staticTableIndex)) + if (_disableDynamicCompression || _maxHeaderTableSize == 0 || encodingHint == HeaderEncodingHint.IgnoreIndex) { return staticTableIndex == -1 - ? HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) - : HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(staticTableIndex, value, buffer, out bytesWritten); + ? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) + : EncodeLiteralHeaderFieldWithoutIndexing(staticTableIndex, value, buffer, out bytesWritten); } // Header is greater than the maximum table size. @@ -195,35 +90,13 @@ private bool EncodeHeader(Span buffer, int staticTableIndex, string name, var index = ResolveDynamicTableIndex(staticTableIndex, name); return index == -1 - ? HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) - : HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexing(index, value, buffer, out bytesWritten); + ? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) + : EncodeLiteralHeaderFieldWithoutIndexing(index, value, buffer, out bytesWritten); } return EncodeDynamicHeader(buffer, staticTableIndex, name, value, out bytesWritten); } - private bool IsSensitive(int staticTableIndex, string name) - { - // Set-Cookie could contain sensitive data. - if (staticTableIndex == H2StaticTable.SetCookie) - { - return true; - } - if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - return false; - } - - private bool IsNotDynamicallyIndexed(int staticTableIndex) - { - // Content-Length is added to static content. Content length is different for each - // file, and is unlikely to be reused because of browser caching. - return staticTableIndex == H2StaticTable.ContentLength; - } - private int ResolveDynamicTableIndex(int staticTableIndex, string name) { if (staticTableIndex != -1) @@ -242,7 +115,7 @@ private bool EncodeDynamicHeader(Span buffer, int staticTableIndex, string { // Already exists in dynamic table. Write index. var index = CalculateDynamicTableIndex(headerField.Index); - return HPackEncoder.EncodeIndexedHeaderField(index, buffer, out bytesWritten); + return EncodeIndexedHeaderField(index, buffer, out bytesWritten); } else { @@ -251,8 +124,8 @@ private bool EncodeDynamicHeader(Span buffer, int staticTableIndex, string var index = ResolveDynamicTableIndex(staticTableIndex, name); var success = index == -1 - ? HPackEncoder.EncodeLiteralHeaderFieldIndexingNewName(name, value, buffer, out bytesWritten) - : HPackEncoder.EncodeLiteralHeaderFieldIndexing(index, value, buffer, out bytesWritten); + ? EncodeLiteralHeaderFieldIndexingNewName(name, value, buffer, out bytesWritten) + : EncodeLiteralHeaderFieldIndexing(index, value, buffer, out bytesWritten); if (success) { @@ -275,12 +148,14 @@ private void EnsureCapacity(uint headerSize) while (_maxHeaderTableSize - _headerTableSize < headerSize) { var removed = RemoveHeaderEntry(); + Debug.Assert(removed != null); + // Removed entries are tracked to be reused. PushRemovedEntry(removed); } } - private HPackHeaderEntry GetEntry(string name, string value) + private HPackHeaderEntry? GetEntry(string name, string value) { if (_headerTableSize == 0) { @@ -350,7 +225,7 @@ private void PushRemovedEntry(HPackHeaderEntry removed) _removed = removed; } - private HPackHeaderEntry PopRemovedEntry() + private HPackHeaderEntry? PopRemovedEntry() { if (_removed != null) { @@ -365,7 +240,7 @@ private HPackHeaderEntry PopRemovedEntry() /// /// Remove the oldest entry. /// - private HPackHeaderEntry RemoveHeaderEntry() + private HPackHeaderEntry? RemoveHeaderEntry() { if (_headerTableSize == 0) { @@ -404,4 +279,11 @@ private int CalculateBucketIndex(int hash) return hash & _hashMask; } } + + internal enum HeaderEncodingHint + { + Index, + IgnoreIndex, + NeverIndex + } } diff --git a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs index 3540918d99de..97cdea1c50f7 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackEncoder.cs @@ -8,7 +8,7 @@ namespace System.Net.Http.HPack { - internal static class HPackEncoder + internal partial class HPackEncoder { // Things we should add: // * Huffman encoding diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackHeaderEntry.cs b/src/Shared/runtime/Http2/Hpack/HPackHeaderEntry.cs similarity index 95% rename from src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackHeaderEntry.cs rename to src/Shared/runtime/Http2/Hpack/HPackHeaderEntry.cs index f6e39e4f788e..ba387e654305 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPack/HPackHeaderEntry.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackHeaderEntry.cs @@ -3,9 +3,8 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; -using System.Net.Http.HPack; -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack +namespace System.Net.Http.HPack { [DebuggerDisplay("Name = {Name} Value = {Value}")] internal class HPackHeaderEntry diff --git a/src/Shared/runtime/Http2/Hpack/StatusCodes.cs b/src/Shared/runtime/Http2/Hpack/StatusCodes.cs index b701fa79f41a..c9f726f36c81 100644 --- a/src/Shared/runtime/Http2/Hpack/StatusCodes.cs +++ b/src/Shared/runtime/Http2/Hpack/StatusCodes.cs @@ -216,5 +216,144 @@ public static ReadOnlySpan ToStatusBytes(int statusCode) } } + + public static string ToStatusString(int statusCode) + { + switch (statusCode) + { + case (int)HttpStatusCode.Continue: + return "100"; + case (int)HttpStatusCode.SwitchingProtocols: + return "101"; + case (int)HttpStatusCode.Processing: + return "102"; + + case (int)HttpStatusCode.OK: + return "200"; + case (int)HttpStatusCode.Created: + return "201"; + case (int)HttpStatusCode.Accepted: + return "202"; + case (int)HttpStatusCode.NonAuthoritativeInformation: + return "203"; + case (int)HttpStatusCode.NoContent: + return "204"; + case (int)HttpStatusCode.ResetContent: + return "205"; + case (int)HttpStatusCode.PartialContent: + return "206"; + case (int)HttpStatusCode.MultiStatus: + return "207"; + case (int)HttpStatusCode.AlreadyReported: + return "208"; + case (int)HttpStatusCode.IMUsed: + return "226"; + + case (int)HttpStatusCode.MultipleChoices: + return "300"; + case (int)HttpStatusCode.MovedPermanently: + return "301"; + case (int)HttpStatusCode.Found: + return "302"; + case (int)HttpStatusCode.SeeOther: + return "303"; + case (int)HttpStatusCode.NotModified: + return "304"; + case (int)HttpStatusCode.UseProxy: + return "305"; + case (int)HttpStatusCode.Unused: + return "306"; + case (int)HttpStatusCode.TemporaryRedirect: + return "307"; + case (int)HttpStatusCode.PermanentRedirect: + return "308"; + + case (int)HttpStatusCode.BadRequest: + return "400"; + case (int)HttpStatusCode.Unauthorized: + return "401"; + case (int)HttpStatusCode.PaymentRequired: + return "402"; + case (int)HttpStatusCode.Forbidden: + return "403"; + case (int)HttpStatusCode.NotFound: + return "404"; + case (int)HttpStatusCode.MethodNotAllowed: + return "405"; + case (int)HttpStatusCode.NotAcceptable: + return "406"; + case (int)HttpStatusCode.ProxyAuthenticationRequired: + return "407"; + case (int)HttpStatusCode.RequestTimeout: + return "408"; + case (int)HttpStatusCode.Conflict: + return "409"; + case (int)HttpStatusCode.Gone: + return "410"; + case (int)HttpStatusCode.LengthRequired: + return "411"; + case (int)HttpStatusCode.PreconditionFailed: + return "412"; + case (int)HttpStatusCode.RequestEntityTooLarge: + return "413"; + case (int)HttpStatusCode.RequestUriTooLong: + return "414"; + case (int)HttpStatusCode.UnsupportedMediaType: + return "415"; + case (int)HttpStatusCode.RequestedRangeNotSatisfiable: + return "416"; + case (int)HttpStatusCode.ExpectationFailed: + return "417"; + case (int)418: + return "418"; + case (int)419: + return "419"; + case (int)HttpStatusCode.MisdirectedRequest: + return "421"; + case (int)HttpStatusCode.UnprocessableEntity: + return "422"; + case (int)HttpStatusCode.Locked: + return "423"; + case (int)HttpStatusCode.FailedDependency: + return "424"; + case (int)HttpStatusCode.UpgradeRequired: + return "426"; + case (int)HttpStatusCode.PreconditionRequired: + return "428"; + case (int)HttpStatusCode.TooManyRequests: + return "429"; + case (int)HttpStatusCode.RequestHeaderFieldsTooLarge: + return "431"; + case (int)HttpStatusCode.UnavailableForLegalReasons: + return "451"; + + case (int)HttpStatusCode.InternalServerError: + return "500"; + case (int)HttpStatusCode.NotImplemented: + return "501"; + case (int)HttpStatusCode.BadGateway: + return "502"; + case (int)HttpStatusCode.ServiceUnavailable: + return "503"; + case (int)HttpStatusCode.GatewayTimeout: + return "504"; + case (int)HttpStatusCode.HttpVersionNotSupported: + return "505"; + case (int)HttpStatusCode.VariantAlsoNegotiates: + return "506"; + case (int)HttpStatusCode.InsufficientStorage: + return "507"; + case (int)HttpStatusCode.LoopDetected: + return "508"; + case (int)HttpStatusCode.NotExtended: + return "510"; + case (int)HttpStatusCode.NetworkAuthenticationRequired: + return "511"; + + default: + return statusCode.ToString(CultureInfo.InvariantCulture); + + } + } } } From 6b4255f32359a5b448ed7ee93888f239f52f1249 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 29 Mar 2020 12:31:49 +1300 Subject: [PATCH 03/14] Fix build --- src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 6d5c519017f3..5b89520f25ef 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -15,7 +15,6 @@ 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; From 9d07253274456d1e5c913c536ac3f731c26c2d19 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 29 Mar 2020 12:35:23 +1300 Subject: [PATCH 04/14] Rename enable setting --- src/Servers/Kestrel/Core/src/Http2Limits.cs | 8 ++++++++ .../Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs | 2 +- src/Servers/Kestrel/Core/src/KestrelServerOptions.cs | 8 -------- .../Http2/Http2ConnectionTests.cs | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Http2Limits.cs b/src/Servers/Kestrel/Core/src/Http2Limits.cs index 713159f66a66..9dd2e077838d 100644 --- a/src/Servers/Kestrel/Core/src/Http2Limits.cs +++ b/src/Servers/Kestrel/Core/src/Http2Limits.cs @@ -141,5 +141,13 @@ public int InitialStreamWindowSize _initialStreamWindowSize = value; } } + + /// + /// Gets or sets a value that controls whether dynamic compression of response headers is enabled. + /// + /// + /// Defaults to true. + /// + public bool EnableResponseHeaderCompression { get; set; } = true; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 5b89520f25ef..bc401e38db85 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -71,7 +71,7 @@ public Http2FrameWriter( _outgoingFrame = new Http2Frame(); _headerEncodingBuffer = new byte[_maxFrameSize]; - _hpackEncoder = new HPackEncoder(serviceContext.ServerOptions.DisableResponseDynamicHeaderCompression); + _hpackEncoder = new HPackEncoder(!serviceContext.ServerOptions.Limits.Http2.EnableResponseHeaderCompression); } public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 6ae930bb3b56..71def30d7399 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -55,14 +55,6 @@ public class KestrelServerOptions /// public bool DisableStringReuse { get; set; } = false; - /// - /// Gets or sets a value that controls whether dynamic compression of response headers is enabled across HTTP/2 and HTTP/3 requests. - /// - /// - /// Defaults to false. - /// - public bool DisableResponseDynamicHeaderCompression { get; set; } = false; - /// /// Enables the Listen options callback to resolve and use services registered by the application during startup. /// Typically initialized by UseKestrel()"/>. diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index f7295c2298d8..b17de29c96af 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -1936,7 +1936,7 @@ await ExpectAsync(Http2FrameType.HEADERS, [Fact] public async Task HEADERS_DisableDynamicHeaderCompression_HeadersNotCompressed() { - _serviceContext.ServerOptions.DisableResponseDynamicHeaderCompression = true; + _serviceContext.ServerOptions.Limits.Http2.EnableResponseHeaderCompression = false; await InitializeConnectionAsync(_noopApplication); From 337129bb4fcf5876ae4b62c327121172739caccd Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 29 Mar 2020 12:39:14 +1300 Subject: [PATCH 05/14] Improve comment --- src/Servers/Kestrel/Core/src/Http2Limits.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Servers/Kestrel/Core/src/Http2Limits.cs b/src/Servers/Kestrel/Core/src/Http2Limits.cs index 9dd2e077838d..738761058577 100644 --- a/src/Servers/Kestrel/Core/src/Http2Limits.cs +++ b/src/Servers/Kestrel/Core/src/Http2Limits.cs @@ -144,6 +144,8 @@ public int InitialStreamWindowSize /// /// Gets or sets a value that controls whether dynamic compression of response headers is enabled. + /// For more information about security considerations of HPack dynamic header compression, visit + /// https://tools.ietf.org/html/rfc7541#section-7. /// /// /// Defaults to true. From 78b376aed33411e16b788cb38b0d784aa75597ed Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 29 Mar 2020 12:45:38 +1300 Subject: [PATCH 06/14] Rename --- .../Core/test/Http2HPackEncoderTests.cs | 6 ++--- ...ckHeaderEntry.cs => EncoderHeaderEntry.cs} | 12 +++++----- .../Http2/Hpack/HPackEncoder.Dynamic.cs | 22 +++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) rename src/Shared/runtime/Http2/Hpack/{HPackHeaderEntry.cs => EncoderHeaderEntry.cs} (87%) diff --git a/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs index 6322c1a62459..c9dcdb00ecaa 100644 --- a/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs +++ b/src/Servers/Kestrel/Core/test/Http2HPackEncoderTests.cs @@ -452,7 +452,7 @@ private static Http2HeadersEnumerator GetHeadersEnumerator(IEnumerable= 0) @@ -462,9 +462,9 @@ private HPackHeaderEntry GetHeaderEntry(HPackEncoder encoder, int index) return entry; } - private List GetHeaderEntries(HPackEncoder encoder) + private List GetHeaderEntries(HPackEncoder encoder) { - var headers = new List(); + var headers = new List(); var entry = encoder.Head; while (entry.Before != encoder.Head) diff --git a/src/Shared/runtime/Http2/Hpack/HPackHeaderEntry.cs b/src/Shared/runtime/Http2/Hpack/EncoderHeaderEntry.cs similarity index 87% rename from src/Shared/runtime/Http2/Hpack/HPackHeaderEntry.cs rename to src/Shared/runtime/Http2/Hpack/EncoderHeaderEntry.cs index ba387e654305..75a0aebde240 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackHeaderEntry.cs +++ b/src/Shared/runtime/Http2/Hpack/EncoderHeaderEntry.cs @@ -7,27 +7,27 @@ namespace System.Net.Http.HPack { [DebuggerDisplay("Name = {Name} Value = {Value}")] - internal class HPackHeaderEntry + internal class EncoderHeaderEntry { // Header name and value public string Name; public string Value; // Chained list of headers in the same bucket - public HPackHeaderEntry Next; + public EncoderHeaderEntry Next; public int Hash; // Compute dynamic table index public int Index; // Doubly linked list - public HPackHeaderEntry Before; - public HPackHeaderEntry After; + public EncoderHeaderEntry Before; + public EncoderHeaderEntry After; /// /// Initialize header values. An entry will be reinitialized when reused. /// - public void Initialize(int hash, string name, string value, int index, HPackHeaderEntry next) + public void Initialize(int hash, string name, string value, int index, EncoderHeaderEntry next) { Debug.Assert(name != null); Debug.Assert(value != null); @@ -62,7 +62,7 @@ public void Remove() /// /// Add before an entry in the linked list. /// - public void AddBefore(HPackHeaderEntry existingEntry) + public void AddBefore(EncoderHeaderEntry existingEntry) { After = existingEntry; Before = existingEntry.Before; diff --git a/src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs b/src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs index 7f080dabf173..a2064f58351b 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs @@ -13,25 +13,25 @@ internal partial class HPackEncoder public const int DefaultHeaderTableSize = 4096; // Internal for testing - internal readonly HPackHeaderEntry Head; + internal readonly EncoderHeaderEntry Head; private readonly bool _disableDynamicCompression; - private readonly HPackHeaderEntry[] _headerBuckets; + private readonly EncoderHeaderEntry[] _headerBuckets; private readonly byte _hashMask; private uint _headerTableSize; private uint _maxHeaderTableSize; private bool _pendingTableSizeUpdate; - private HPackHeaderEntry? _removed; + private EncoderHeaderEntry? _removed; public HPackEncoder(bool disableDynamicCompression = false, uint maxHeaderTableSize = DefaultHeaderTableSize) { _disableDynamicCompression = disableDynamicCompression; _maxHeaderTableSize = maxHeaderTableSize; - Head = new HPackHeaderEntry(); + Head = new EncoderHeaderEntry(); Head.Initialize(-1, string.Empty, string.Empty, int.MaxValue, null); // Bucket count balances memory usage and the expected low number of headers (constrained by the header table size). // Performance with different bucket counts hasn't been measured in detail. - _headerBuckets = new HPackHeaderEntry[16]; + _headerBuckets = new EncoderHeaderEntry[16]; _hashMask = (byte)(_headerBuckets.Length - 1); Head.Before = Head.After = Head; } @@ -155,7 +155,7 @@ private void EnsureCapacity(uint headerSize) } } - private HPackHeaderEntry? GetEntry(string name, string value) + private EncoderHeaderEntry? GetEntry(string name, string value) { if (_headerTableSize == 0) { @@ -209,14 +209,14 @@ private void AddHeaderEntry(string name, string value, uint headerSize) var bucketIndex = CalculateBucketIndex(hash); var oldEntry = _headerBuckets[bucketIndex]; // Attempt to reuse removed entry - var newEntry = PopRemovedEntry() ?? new HPackHeaderEntry(); + var newEntry = PopRemovedEntry() ?? new EncoderHeaderEntry(); newEntry.Initialize(hash, name, value, Head.Before.Index - 1, oldEntry); _headerBuckets[bucketIndex] = newEntry; newEntry.AddBefore(Head); _headerTableSize += headerSize; } - private void PushRemovedEntry(HPackHeaderEntry removed) + private void PushRemovedEntry(EncoderHeaderEntry removed) { if (_removed != null) { @@ -225,7 +225,7 @@ private void PushRemovedEntry(HPackHeaderEntry removed) _removed = removed; } - private HPackHeaderEntry? PopRemovedEntry() + private EncoderHeaderEntry? PopRemovedEntry() { if (_removed != null) { @@ -240,7 +240,7 @@ private void PushRemovedEntry(HPackHeaderEntry removed) /// /// Remove the oldest entry. /// - private HPackHeaderEntry? RemoveHeaderEntry() + private EncoderHeaderEntry? RemoveHeaderEntry() { if (_headerTableSize == 0) { @@ -253,7 +253,7 @@ private void PushRemovedEntry(HPackHeaderEntry removed) var e = prev; while (e != null) { - HPackHeaderEntry next = e.Next; + EncoderHeaderEntry next = e.Next; if (e == eldest) { if (prev == eldest) From 7a6e298baebf184e76895e3ca034745c8cc0f72e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 29 Mar 2020 12:57:09 +1300 Subject: [PATCH 07/14] Fix build --- .../ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d242bfc4dfa9..f77f1e9514cb 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 @@ -70,6 +70,7 @@ internal BadHttpRequestException() { } public partial class Http2Limits { public Http2Limits() { } + public bool EnableResponseHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public int HeaderTableSize { get { throw null; } set { } } public int InitialConnectionWindowSize { get { throw null; } set { } } public int InitialStreamWindowSize { get { throw null; } set { } } @@ -130,7 +131,6 @@ public KestrelServerOptions() { } public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } - public bool DisableResponseDynamicHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public bool DisableStringReuse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public bool EnableAltSvc { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } } From 18ca707f17b7cd8feecdb685e65e4981622fe9f9 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 29 Mar 2020 18:50:37 +1300 Subject: [PATCH 08/14] Rename setting --- .../Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs | 2 +- src/Servers/Kestrel/Core/src/Http2Limits.cs | 6 +++--- .../Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs | 2 +- .../InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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 f77f1e9514cb..544e5f3562e5 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 @@ -70,7 +70,7 @@ internal BadHttpRequestException() { } public partial class Http2Limits { public Http2Limits() { } - public bool EnableResponseHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public bool AllowResponseHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public int HeaderTableSize { get { throw null; } set { } } public int InitialConnectionWindowSize { get { throw null; } set { } } public int InitialStreamWindowSize { get { throw null; } set { } } diff --git a/src/Servers/Kestrel/Core/src/Http2Limits.cs b/src/Servers/Kestrel/Core/src/Http2Limits.cs index 738761058577..ad545f0fb1ea 100644 --- a/src/Servers/Kestrel/Core/src/Http2Limits.cs +++ b/src/Servers/Kestrel/Core/src/Http2Limits.cs @@ -143,13 +143,13 @@ public int InitialStreamWindowSize } /// - /// Gets or sets a value that controls whether dynamic compression of response headers is enabled. - /// For more information about security considerations of HPack dynamic header compression, visit + /// Gets or sets a value that controls whether dynamic compression of response headers is allowed. + /// For more information about the security considerations of HPack dynamic header compression, visit /// https://tools.ietf.org/html/rfc7541#section-7. /// /// /// Defaults to true. /// - public bool EnableResponseHeaderCompression { get; set; } = true; + public bool AllowResponseHeaderCompression { get; set; } = true; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index bc401e38db85..c4cb1d4c845e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -71,7 +71,7 @@ public Http2FrameWriter( _outgoingFrame = new Http2Frame(); _headerEncodingBuffer = new byte[_maxFrameSize]; - _hpackEncoder = new HPackEncoder(!serviceContext.ServerOptions.Limits.Http2.EnableResponseHeaderCompression); + _hpackEncoder = new HPackEncoder(!serviceContext.ServerOptions.Limits.Http2.AllowResponseHeaderCompression); } public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index b17de29c96af..077af2480458 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -1936,7 +1936,7 @@ await ExpectAsync(Http2FrameType.HEADERS, [Fact] public async Task HEADERS_DisableDynamicHeaderCompression_HeadersNotCompressed() { - _serviceContext.ServerOptions.Limits.Http2.EnableResponseHeaderCompression = false; + _serviceContext.ServerOptions.Limits.Http2.AllowResponseHeaderCompression = false; await InitializeConnectionAsync(_noopApplication); From bacb43cf97ac16297d681a1a3935d591339fb937 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 1 Apr 2020 15:24:25 +1300 Subject: [PATCH 09/14] Move shareable code out of runtime. --- ...soft.AspNetCore.Server.Kestrel.Core.csproj | 3 +- .../Http2 => }/Hpack/EncoderHeaderEntry.cs | 0 .../Http2 => }/Hpack/HPackEncoder.Dynamic.cs | 56 ++++--- src/Shared/Hpack/README.md | 3 + src/Shared/Hpack/StatusCodes.cs | 151 ++++++++++++++++++ src/Shared/runtime/Http2/Hpack/StatusCodes.cs | 141 +--------------- 6 files changed, 188 insertions(+), 166 deletions(-) rename src/Shared/{runtime/Http2 => }/Hpack/EncoderHeaderEntry.cs (100%) rename src/Shared/{runtime/Http2 => }/Hpack/HPackEncoder.Dynamic.cs (83%) create mode 100644 src/Shared/Hpack/README.md create mode 100644 src/Shared/Hpack/StatusCodes.cs 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 a38f17f12e7a..73f77bb7730c 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. @@ -19,6 +19,7 @@ + diff --git a/src/Shared/runtime/Http2/Hpack/EncoderHeaderEntry.cs b/src/Shared/Hpack/EncoderHeaderEntry.cs similarity index 100% rename from src/Shared/runtime/Http2/Hpack/EncoderHeaderEntry.cs rename to src/Shared/Hpack/EncoderHeaderEntry.cs diff --git a/src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs b/src/Shared/Hpack/HPackEncoder.Dynamic.cs similarity index 83% rename from src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs rename to src/Shared/Hpack/HPackEncoder.Dynamic.cs index a2064f58351b..de591f4e9e73 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackEncoder.Dynamic.cs +++ b/src/Shared/Hpack/HPackEncoder.Dynamic.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. #nullable enable -using System.Collections.Generic; using System.Diagnostics; namespace System.Net.Http.HPack @@ -41,6 +40,8 @@ public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) if (_maxHeaderTableSize != maxHeaderTableSize) { _maxHeaderTableSize = maxHeaderTableSize; + + // Dynamic table size update will be written next HEADERS frame _pendingTableSizeUpdate = true; // Check capacity and remove entries that exceed the new capacity @@ -50,6 +51,7 @@ public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) public bool EnsureDynamicTableSizeUpdate(Span buffer, out int length) { + // Check if there is a table size update that should be encoded if (_pendingTableSizeUpdate) { bool success = EncodeDynamicTableSizeUpdate((int)_maxHeaderTableSize, buffer, out length); @@ -68,7 +70,7 @@ public bool EncodeHeader(Span buffer, int staticTableIndex, HeaderEncoding // Never index sensitive value. if (encodingHint == HeaderEncodingHint.NeverIndex) { - var index = ResolveDynamicTableIndex(staticTableIndex, name); + int index = ResolveDynamicTableIndex(staticTableIndex, name); return index == -1 ? EncodeLiteralHeaderFieldNeverIndexingNewName(name, value, buffer, out bytesWritten) @@ -87,7 +89,7 @@ public bool EncodeHeader(Span buffer, int staticTableIndex, HeaderEncoding // Don't attempt to add dynamic header as all existing dynamic headers will be removed. if (HeaderField.GetLength(name.Length, value.Length) > _maxHeaderTableSize) { - var index = ResolveDynamicTableIndex(staticTableIndex, name); + int index = ResolveDynamicTableIndex(staticTableIndex, name); return index == -1 ? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten) @@ -110,20 +112,20 @@ private int ResolveDynamicTableIndex(int staticTableIndex, string name) private bool EncodeDynamicHeader(Span buffer, int staticTableIndex, string name, string value, out int bytesWritten) { - var headerField = GetEntry(name, value); + EncoderHeaderEntry? headerField = GetEntry(name, value); if (headerField != null) { // Already exists in dynamic table. Write index. - var index = CalculateDynamicTableIndex(headerField.Index); + int index = CalculateDynamicTableIndex(headerField.Index); return EncodeIndexedHeaderField(index, buffer, out bytesWritten); } else { // Doesn't exist in dynamic table. Add new entry to dynamic table. - var headerSize = (uint)HeaderField.GetLength(name.Length, value.Length); + uint headerSize = (uint)HeaderField.GetLength(name.Length, value.Length); - var index = ResolveDynamicTableIndex(staticTableIndex, name); - var success = index == -1 + int index = ResolveDynamicTableIndex(staticTableIndex, name); + bool success = index == -1 ? EncodeLiteralHeaderFieldIndexingNewName(name, value, buffer, out bytesWritten) : EncodeLiteralHeaderFieldIndexing(index, value, buffer, out bytesWritten); @@ -147,7 +149,7 @@ private void EnsureCapacity(uint headerSize) while (_maxHeaderTableSize - _headerTableSize < headerSize) { - var removed = RemoveHeaderEntry(); + EncoderHeaderEntry? removed = RemoveHeaderEntry(); Debug.Assert(removed != null); // Removed entries are tracked to be reused. @@ -161,9 +163,9 @@ private void EnsureCapacity(uint headerSize) { return null; } - var hash = name.GetHashCode(); - var bucketIndex = CalculateBucketIndex(hash); - for (var e = _headerBuckets[bucketIndex]; e != null; e = e.Next) + int hash = name.GetHashCode(); + int bucketIndex = CalculateBucketIndex(hash); + for (EncoderHeaderEntry? e = _headerBuckets[bucketIndex]; e != null; e = e.Next) { // We've already looked up entries based on a hash of the name. // Compare value before name as it is more likely to be different. @@ -183,9 +185,9 @@ private int CalculateDynamicTableIndex(string name) { return -1; } - var hash = name.GetHashCode(); - var bucketIndex = CalculateBucketIndex(hash); - for (var e = _headerBuckets[bucketIndex]; e != null; e = e.Next) + int hash = name.GetHashCode(); + int bucketIndex = CalculateBucketIndex(hash); + for (EncoderHeaderEntry? e = _headerBuckets[bucketIndex]; e != null; e = e.Next) { if (e.Hash == hash && string.Equals(name, e.Name, StringComparison.Ordinal)) { @@ -205,11 +207,11 @@ private void AddHeaderEntry(string name, string value, uint headerSize) Debug.Assert(headerSize <= _maxHeaderTableSize, "Header is bigger than dynamic table size."); Debug.Assert(headerSize <= _maxHeaderTableSize - _headerTableSize, "Not enough room in dynamic table."); - var hash = name.GetHashCode(); - var bucketIndex = CalculateBucketIndex(hash); - var oldEntry = _headerBuckets[bucketIndex]; + int hash = name.GetHashCode(); + int bucketIndex = CalculateBucketIndex(hash); + EncoderHeaderEntry? oldEntry = _headerBuckets[bucketIndex]; // Attempt to reuse removed entry - var newEntry = PopRemovedEntry() ?? new EncoderHeaderEntry(); + EncoderHeaderEntry? newEntry = PopRemovedEntry() ?? new EncoderHeaderEntry(); newEntry.Initialize(hash, name, value, Head.Before.Index - 1, oldEntry); _headerBuckets[bucketIndex] = newEntry; newEntry.AddBefore(Head); @@ -229,7 +231,7 @@ private void PushRemovedEntry(EncoderHeaderEntry removed) { if (_removed != null) { - var removed = _removed; + EncoderHeaderEntry? removed = _removed; _removed = _removed.Next; return removed; } @@ -246,11 +248,11 @@ private void PushRemovedEntry(EncoderHeaderEntry removed) { return null; } - var eldest = Head.After; - var hash = eldest.Hash; - var bucketIndex = CalculateBucketIndex(hash); - var prev = _headerBuckets[bucketIndex]; - var e = prev; + EncoderHeaderEntry? eldest = Head.After; + int hash = eldest.Hash; + int bucketIndex = CalculateBucketIndex(hash); + EncoderHeaderEntry? prev = _headerBuckets[bucketIndex]; + EncoderHeaderEntry? e = prev; while (e != null) { EncoderHeaderEntry next = e.Next; @@ -280,6 +282,10 @@ private int CalculateBucketIndex(int hash) } } + /// + /// Hint for how the header should be encoded as HPack. This value can be overriden. + /// For example, a header that is larger than the dynamic table won't be indexed. + /// internal enum HeaderEncodingHint { Index, diff --git a/src/Shared/Hpack/README.md b/src/Shared/Hpack/README.md new file mode 100644 index 000000000000..d18485cceac6 --- /dev/null +++ b/src/Shared/Hpack/README.md @@ -0,0 +1,3 @@ +HPack dynamic compression. These files are kept separate to help avoid ASP.NET Core dependencies being added to them. + +Runtime currently doesn't implement HPack dynamic compression. These files will move into runtime shareable code in the future when support is added to runtime. \ No newline at end of file diff --git a/src/Shared/Hpack/StatusCodes.cs b/src/Shared/Hpack/StatusCodes.cs new file mode 100644 index 000000000000..eb67205586f0 --- /dev/null +++ b/src/Shared/Hpack/StatusCodes.cs @@ -0,0 +1,151 @@ +// 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.Globalization; +using System.Text; + +namespace System.Net.Http.HPack +{ + internal static partial class StatusCodes + { + public static string ToStatusString(int statusCode) + { + switch (statusCode) + { + case (int)HttpStatusCode.Continue: + return "100"; + case (int)HttpStatusCode.SwitchingProtocols: + return "101"; + case (int)HttpStatusCode.Processing: + return "102"; + + case (int)HttpStatusCode.OK: + return "200"; + case (int)HttpStatusCode.Created: + return "201"; + case (int)HttpStatusCode.Accepted: + return "202"; + case (int)HttpStatusCode.NonAuthoritativeInformation: + return "203"; + case (int)HttpStatusCode.NoContent: + return "204"; + case (int)HttpStatusCode.ResetContent: + return "205"; + case (int)HttpStatusCode.PartialContent: + return "206"; + case (int)HttpStatusCode.MultiStatus: + return "207"; + case (int)HttpStatusCode.AlreadyReported: + return "208"; + case (int)HttpStatusCode.IMUsed: + return "226"; + + case (int)HttpStatusCode.MultipleChoices: + return "300"; + case (int)HttpStatusCode.MovedPermanently: + return "301"; + case (int)HttpStatusCode.Found: + return "302"; + case (int)HttpStatusCode.SeeOther: + return "303"; + case (int)HttpStatusCode.NotModified: + return "304"; + case (int)HttpStatusCode.UseProxy: + return "305"; + case (int)HttpStatusCode.Unused: + return "306"; + case (int)HttpStatusCode.TemporaryRedirect: + return "307"; + case (int)HttpStatusCode.PermanentRedirect: + return "308"; + + case (int)HttpStatusCode.BadRequest: + return "400"; + case (int)HttpStatusCode.Unauthorized: + return "401"; + case (int)HttpStatusCode.PaymentRequired: + return "402"; + case (int)HttpStatusCode.Forbidden: + return "403"; + case (int)HttpStatusCode.NotFound: + return "404"; + case (int)HttpStatusCode.MethodNotAllowed: + return "405"; + case (int)HttpStatusCode.NotAcceptable: + return "406"; + case (int)HttpStatusCode.ProxyAuthenticationRequired: + return "407"; + case (int)HttpStatusCode.RequestTimeout: + return "408"; + case (int)HttpStatusCode.Conflict: + return "409"; + case (int)HttpStatusCode.Gone: + return "410"; + case (int)HttpStatusCode.LengthRequired: + return "411"; + case (int)HttpStatusCode.PreconditionFailed: + return "412"; + case (int)HttpStatusCode.RequestEntityTooLarge: + return "413"; + case (int)HttpStatusCode.RequestUriTooLong: + return "414"; + case (int)HttpStatusCode.UnsupportedMediaType: + return "415"; + case (int)HttpStatusCode.RequestedRangeNotSatisfiable: + return "416"; + case (int)HttpStatusCode.ExpectationFailed: + return "417"; + case (int)418: + return "418"; + case (int)419: + return "419"; + case (int)HttpStatusCode.MisdirectedRequest: + return "421"; + case (int)HttpStatusCode.UnprocessableEntity: + return "422"; + case (int)HttpStatusCode.Locked: + return "423"; + case (int)HttpStatusCode.FailedDependency: + return "424"; + case (int)HttpStatusCode.UpgradeRequired: + return "426"; + case (int)HttpStatusCode.PreconditionRequired: + return "428"; + case (int)HttpStatusCode.TooManyRequests: + return "429"; + case (int)HttpStatusCode.RequestHeaderFieldsTooLarge: + return "431"; + case (int)HttpStatusCode.UnavailableForLegalReasons: + return "451"; + + case (int)HttpStatusCode.InternalServerError: + return "500"; + case (int)HttpStatusCode.NotImplemented: + return "501"; + case (int)HttpStatusCode.BadGateway: + return "502"; + case (int)HttpStatusCode.ServiceUnavailable: + return "503"; + case (int)HttpStatusCode.GatewayTimeout: + return "504"; + case (int)HttpStatusCode.HttpVersionNotSupported: + return "505"; + case (int)HttpStatusCode.VariantAlsoNegotiates: + return "506"; + case (int)HttpStatusCode.InsufficientStorage: + return "507"; + case (int)HttpStatusCode.LoopDetected: + return "508"; + case (int)HttpStatusCode.NotExtended: + return "510"; + case (int)HttpStatusCode.NetworkAuthenticationRequired: + return "511"; + + default: + return statusCode.ToString(CultureInfo.InvariantCulture); + + } + } + } +} diff --git a/src/Shared/runtime/Http2/Hpack/StatusCodes.cs b/src/Shared/runtime/Http2/Hpack/StatusCodes.cs index c9f726f36c81..01c42abbc524 100644 --- a/src/Shared/runtime/Http2/Hpack/StatusCodes.cs +++ b/src/Shared/runtime/Http2/Hpack/StatusCodes.cs @@ -7,7 +7,7 @@ namespace System.Net.Http.HPack { - internal static class StatusCodes + internal static partial class StatusCodes { // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static @@ -216,144 +216,5 @@ public static ReadOnlySpan ToStatusBytes(int statusCode) } } - - public static string ToStatusString(int statusCode) - { - switch (statusCode) - { - case (int)HttpStatusCode.Continue: - return "100"; - case (int)HttpStatusCode.SwitchingProtocols: - return "101"; - case (int)HttpStatusCode.Processing: - return "102"; - - case (int)HttpStatusCode.OK: - return "200"; - case (int)HttpStatusCode.Created: - return "201"; - case (int)HttpStatusCode.Accepted: - return "202"; - case (int)HttpStatusCode.NonAuthoritativeInformation: - return "203"; - case (int)HttpStatusCode.NoContent: - return "204"; - case (int)HttpStatusCode.ResetContent: - return "205"; - case (int)HttpStatusCode.PartialContent: - return "206"; - case (int)HttpStatusCode.MultiStatus: - return "207"; - case (int)HttpStatusCode.AlreadyReported: - return "208"; - case (int)HttpStatusCode.IMUsed: - return "226"; - - case (int)HttpStatusCode.MultipleChoices: - return "300"; - case (int)HttpStatusCode.MovedPermanently: - return "301"; - case (int)HttpStatusCode.Found: - return "302"; - case (int)HttpStatusCode.SeeOther: - return "303"; - case (int)HttpStatusCode.NotModified: - return "304"; - case (int)HttpStatusCode.UseProxy: - return "305"; - case (int)HttpStatusCode.Unused: - return "306"; - case (int)HttpStatusCode.TemporaryRedirect: - return "307"; - case (int)HttpStatusCode.PermanentRedirect: - return "308"; - - case (int)HttpStatusCode.BadRequest: - return "400"; - case (int)HttpStatusCode.Unauthorized: - return "401"; - case (int)HttpStatusCode.PaymentRequired: - return "402"; - case (int)HttpStatusCode.Forbidden: - return "403"; - case (int)HttpStatusCode.NotFound: - return "404"; - case (int)HttpStatusCode.MethodNotAllowed: - return "405"; - case (int)HttpStatusCode.NotAcceptable: - return "406"; - case (int)HttpStatusCode.ProxyAuthenticationRequired: - return "407"; - case (int)HttpStatusCode.RequestTimeout: - return "408"; - case (int)HttpStatusCode.Conflict: - return "409"; - case (int)HttpStatusCode.Gone: - return "410"; - case (int)HttpStatusCode.LengthRequired: - return "411"; - case (int)HttpStatusCode.PreconditionFailed: - return "412"; - case (int)HttpStatusCode.RequestEntityTooLarge: - return "413"; - case (int)HttpStatusCode.RequestUriTooLong: - return "414"; - case (int)HttpStatusCode.UnsupportedMediaType: - return "415"; - case (int)HttpStatusCode.RequestedRangeNotSatisfiable: - return "416"; - case (int)HttpStatusCode.ExpectationFailed: - return "417"; - case (int)418: - return "418"; - case (int)419: - return "419"; - case (int)HttpStatusCode.MisdirectedRequest: - return "421"; - case (int)HttpStatusCode.UnprocessableEntity: - return "422"; - case (int)HttpStatusCode.Locked: - return "423"; - case (int)HttpStatusCode.FailedDependency: - return "424"; - case (int)HttpStatusCode.UpgradeRequired: - return "426"; - case (int)HttpStatusCode.PreconditionRequired: - return "428"; - case (int)HttpStatusCode.TooManyRequests: - return "429"; - case (int)HttpStatusCode.RequestHeaderFieldsTooLarge: - return "431"; - case (int)HttpStatusCode.UnavailableForLegalReasons: - return "451"; - - case (int)HttpStatusCode.InternalServerError: - return "500"; - case (int)HttpStatusCode.NotImplemented: - return "501"; - case (int)HttpStatusCode.BadGateway: - return "502"; - case (int)HttpStatusCode.ServiceUnavailable: - return "503"; - case (int)HttpStatusCode.GatewayTimeout: - return "504"; - case (int)HttpStatusCode.HttpVersionNotSupported: - return "505"; - case (int)HttpStatusCode.VariantAlsoNegotiates: - return "506"; - case (int)HttpStatusCode.InsufficientStorage: - return "507"; - case (int)HttpStatusCode.LoopDetected: - return "508"; - case (int)HttpStatusCode.NotExtended: - return "510"; - case (int)HttpStatusCode.NetworkAuthenticationRequired: - return "511"; - - default: - return statusCode.ToString(CultureInfo.InvariantCulture); - - } - } } } From 6795a2484b80b4b903304f6cbeff701914b7c093 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 1 Apr 2020 15:51:17 +1300 Subject: [PATCH 10/14] Add end-to-end test --- .../Http2/Http2ConnectionTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 077af2480458..83032ea5d180 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -14,10 +14,12 @@ using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Moq; using Xunit; @@ -1933,6 +1935,79 @@ await ExpectAsync(Http2FrameType.HEADERS, await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); } + [Fact] + public async Task HEADERS_ResponseSetsIgnoreIndexAndNeverIndexValues_HeadersParsed() + { + await InitializeConnectionAsync(c => + { + c.Response.ContentLength = 0; + c.Response.Headers[HeaderNames.SetCookie] = "SetCookie!"; + c.Response.Headers[HeaderNames.ContentDisposition] = "ContentDisposition!"; + + return Task.CompletedTask; + }); + + await StartStreamAsync(1, _browserRequestHeaders, endStream: true); + + var frame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 90, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + var payload = frame.Payload; + + var handler = new TestHttpHeadersHandler(); + + var hpackDecoder = new HPackDecoder(); + hpackDecoder.Decode(new ReadOnlySequence(payload), endHeaders: true, handler); + hpackDecoder.CompleteDecode(); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + Assert.Equal("200", handler.Headers[":status"]); + Assert.Equal("SetCookie!", handler.Headers[HeaderNames.SetCookie]); + Assert.Equal("ContentDisposition!", handler.Headers[HeaderNames.ContentDisposition]); + Assert.Equal("0", handler.Headers[HeaderNames.ContentLength]); + } + + private class TestHttpHeadersHandler : IHttpHeadersHandler + { + public readonly Dictionary Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) + { + var nameString = Encoding.ASCII.GetString(name); + var valueString = Encoding.ASCII.GetString(value); + + if (Headers.TryGetValue(nameString, out var values)) + { + var l = values.ToList(); + l.Add(valueString); + + Headers[nameString] = new StringValues(l.ToArray()); + } + else + { + Headers[nameString] = new StringValues(valueString); + } + } + + public void OnHeadersComplete(bool endStream) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index) + { + throw new NotImplementedException(); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + throw new NotImplementedException(); + } + } + [Fact] public async Task HEADERS_DisableDynamicHeaderCompression_HeadersNotCompressed() { From 2517f0e72f099778cebf9905ede2905e615552e2 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 2 Apr 2020 09:48:31 +1300 Subject: [PATCH 11/14] PR feedback --- src/Servers/Kestrel/Core/src/Http2Limits.cs | 10 ---------- .../Core/src/Internal/Http2/Http2FrameWriter.cs | 2 +- src/Servers/Kestrel/Core/src/KestrelServerOptions.cs | 10 ++++++++++ .../Http2/Http2ConnectionTests.cs | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Http2Limits.cs b/src/Servers/Kestrel/Core/src/Http2Limits.cs index ad545f0fb1ea..713159f66a66 100644 --- a/src/Servers/Kestrel/Core/src/Http2Limits.cs +++ b/src/Servers/Kestrel/Core/src/Http2Limits.cs @@ -141,15 +141,5 @@ public int InitialStreamWindowSize _initialStreamWindowSize = value; } } - - /// - /// Gets or sets a value that controls whether dynamic compression of response headers is allowed. - /// For more information about the security considerations of HPack dynamic header compression, visit - /// https://tools.ietf.org/html/rfc7541#section-7. - /// - /// - /// Defaults to true. - /// - public bool AllowResponseHeaderCompression { get; set; } = true; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index c4cb1d4c845e..1746dbc77d52 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -71,7 +71,7 @@ public Http2FrameWriter( _outgoingFrame = new Http2Frame(); _headerEncodingBuffer = new byte[_maxFrameSize]; - _hpackEncoder = new HPackEncoder(!serviceContext.ServerOptions.Limits.Http2.AllowResponseHeaderCompression); + _hpackEncoder = new HPackEncoder(!serviceContext.ServerOptions.AllowResponseHeaderCompression); } public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index 71def30d7399..c849b84caf1c 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -38,6 +38,16 @@ public class KestrelServerOptions /// public bool AddServerHeader { get; set; } = true; + /// + /// Gets or sets a value that controls whether dynamic compression of response headers is allowed. + /// For more information about the security considerations of HPack dynamic header compression, visit + /// https://tools.ietf.org/html/rfc7541#section-7. + /// + /// + /// Defaults to true. + /// + public bool AllowResponseHeaderCompression { get; set; } = true; + /// /// Gets or sets a value that controls whether synchronous IO is allowed for the and /// diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 83032ea5d180..8a4153a98858 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -2011,7 +2011,7 @@ public void OnStaticIndexedHeader(int index, ReadOnlySpan value) [Fact] public async Task HEADERS_DisableDynamicHeaderCompression_HeadersNotCompressed() { - _serviceContext.ServerOptions.Limits.Http2.AllowResponseHeaderCompression = false; + _serviceContext.ServerOptions.AllowResponseHeaderCompression = false; await InitializeConnectionAsync(_noopApplication); From 7bd38a64517b0ed865732f4a2c6e4950cdd3c772 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 2 Apr 2020 10:02:31 +1300 Subject: [PATCH 12/14] Fix build --- .../ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 544e5f3562e5..cfbd7ab1e2dc 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 @@ -70,7 +70,6 @@ internal BadHttpRequestException() { } public partial class Http2Limits { public Http2Limits() { } - public bool AllowResponseHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public int HeaderTableSize { get { throw null; } set { } } public int InitialConnectionWindowSize { get { throw null; } set { } } public int InitialStreamWindowSize { get { throw null; } set { } } @@ -128,6 +127,7 @@ public partial class KestrelServerOptions { public KestrelServerOptions() { } public bool AddServerHeader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } + public bool AllowResponseHeaderCompression { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public System.IServiceProvider ApplicationServices { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader ConfigurationLoader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } From 5385c0740b9f944be54d14373f02d0384b9593ed Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 2 Apr 2020 10:07:02 +1300 Subject: [PATCH 13/14] Improve test --- .../Http2/Http2ConnectionTests.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 8a4153a98858..8184c5f04045 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -1954,20 +1954,35 @@ await InitializeConnectionAsync(c => withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); - var payload = frame.Payload; - var handler = new TestHttpHeadersHandler(); var hpackDecoder = new HPackDecoder(); - hpackDecoder.Decode(new ReadOnlySequence(payload), endHeaders: true, handler); + hpackDecoder.Decode(new ReadOnlySequence(frame.Payload), endHeaders: true, handler); hpackDecoder.CompleteDecode(); - await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + Assert.Equal("200", handler.Headers[":status"]); + Assert.Equal("SetCookie!", handler.Headers[HeaderNames.SetCookie]); + Assert.Equal("ContentDisposition!", handler.Headers[HeaderNames.ContentDisposition]); + Assert.Equal("0", handler.Headers[HeaderNames.ContentLength]); + + await StartStreamAsync(3, _browserRequestHeaders, endStream: true); + + frame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 60, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 3); + + handler = new TestHttpHeadersHandler(); + + hpackDecoder.Decode(new ReadOnlySequence(frame.Payload), endHeaders: true, handler); + hpackDecoder.CompleteDecode(); Assert.Equal("200", handler.Headers[":status"]); Assert.Equal("SetCookie!", handler.Headers[HeaderNames.SetCookie]); Assert.Equal("ContentDisposition!", handler.Headers[HeaderNames.ContentDisposition]); Assert.Equal("0", handler.Headers[HeaderNames.ContentLength]); + + await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); } private class TestHttpHeadersHandler : IHttpHeadersHandler From 2217e8ed606f8c20cb3364cad928b2248150e3ee Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 2 Apr 2020 10:44:34 +1300 Subject: [PATCH 14/14] Fix tests and PR feedback --- .../Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs | 2 +- .../Http2/Http2ConnectionTests.cs | 2 +- src/Shared/Hpack/HPackEncoder.Dynamic.cs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 1746dbc77d52..ca428eae8a5c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -71,7 +71,7 @@ public Http2FrameWriter( _outgoingFrame = new Http2Frame(); _headerEncodingBuffer = new byte[_maxFrameSize]; - _hpackEncoder = new HPackEncoder(!serviceContext.ServerOptions.AllowResponseHeaderCompression); + _hpackEncoder = new HPackEncoder(serviceContext.ServerOptions.AllowResponseHeaderCompression); } public void UpdateMaxHeaderTableSize(uint maxHeaderTableSize) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 8184c5f04045..1a7bd27c2445 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -103,7 +103,7 @@ await ExpectAsync(Http2FrameType.HEADERS, await StartStreamAsync(5, GetHeaders(responseBodySize: 3), endStream: true); await ExpectAsync(Http2FrameType.HEADERS, - withLength: 33, + withLength: 2, withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS, withStreamId: 5); diff --git a/src/Shared/Hpack/HPackEncoder.Dynamic.cs b/src/Shared/Hpack/HPackEncoder.Dynamic.cs index de591f4e9e73..f8e7f4c93dfb 100644 --- a/src/Shared/Hpack/HPackEncoder.Dynamic.cs +++ b/src/Shared/Hpack/HPackEncoder.Dynamic.cs @@ -14,7 +14,7 @@ internal partial class HPackEncoder // Internal for testing internal readonly EncoderHeaderEntry Head; - private readonly bool _disableDynamicCompression; + private readonly bool _allowDynamicCompression; private readonly EncoderHeaderEntry[] _headerBuckets; private readonly byte _hashMask; private uint _headerTableSize; @@ -22,9 +22,9 @@ internal partial class HPackEncoder private bool _pendingTableSizeUpdate; private EncoderHeaderEntry? _removed; - public HPackEncoder(bool disableDynamicCompression = false, uint maxHeaderTableSize = DefaultHeaderTableSize) + public HPackEncoder(bool allowDynamicCompression = true, uint maxHeaderTableSize = DefaultHeaderTableSize) { - _disableDynamicCompression = disableDynamicCompression; + _allowDynamicCompression = allowDynamicCompression; _maxHeaderTableSize = maxHeaderTableSize; Head = new EncoderHeaderEntry(); Head.Initialize(-1, string.Empty, string.Empty, int.MaxValue, null); @@ -78,7 +78,7 @@ public bool EncodeHeader(Span buffer, int staticTableIndex, HeaderEncoding } // No dynamic table. Only use the static table. - if (_disableDynamicCompression || _maxHeaderTableSize == 0 || encodingHint == HeaderEncodingHint.IgnoreIndex) + if (!_allowDynamicCompression || _maxHeaderTableSize == 0 || encodingHint == HeaderEncodingHint.IgnoreIndex) { return staticTableIndex == -1 ? EncodeLiteralHeaderFieldWithoutIndexingNewName(name, value, buffer, out bytesWritten)