From e30f4a80eb8633dfb7253a527b81666e6b7cf329 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 29 Nov 2021 15:47:47 +1300 Subject: [PATCH 1/3] HTTP/3: Check response header value and write just index when possible --- .../Internal/Http/HttpHeaders.Generated.cs | 173 +++++++++ .../Internal/Http3/Http3HeadersEnumerator.cs | 64 +--- .../src/Internal/Http3/QPackHeaderWriter.cs | 34 +- .../test/Http3/Http3HeadersEnumeratorTests.cs | 23 +- .../Core/test/Http3/Http3QPackEncoderTests.cs | 19 + src/Servers/Kestrel/shared/KnownHeaders.cs | 347 +++++++++++------- .../tools/CodeGenerator/CodeGenerator.csproj | 3 + 7 files changed, 447 insertions(+), 216 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs index eee8a7cb47b4..ef6ecb18a4b4 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs @@ -102,6 +102,179 @@ internal enum KnownHeaderType WWWAuthenticate, } + internal static class HttpHeadersCompression + { + internal static (int index, bool matchedValue) MatchKnownHeaderQPack(KnownHeaderType knownHeader, string value) + { + switch (knownHeader) + { + case KnownHeaderType.Age: + switch (value) + { + case "0": + return (2, true); + default: + return (2, false); + } + case KnownHeaderType.ContentLength: + switch (value) + { + case "0": + return (4, true); + default: + return (4, false); + } + case KnownHeaderType.Date: + return (6, false); + case KnownHeaderType.ETag: + return (7, false); + case KnownHeaderType.LastModified: + return (10, false); + case KnownHeaderType.Location: + return (12, false); + case KnownHeaderType.SetCookie: + return (14, false); + case KnownHeaderType.AcceptRanges: + switch (value) + { + case "bytes": + return (32, true); + default: + return (32, false); + } + case KnownHeaderType.AccessControlAllowHeaders: + switch (value) + { + case "cache-control": + return (33, true); + case "content-type": + return (34, true); + case "*": + return (75, true); + default: + return (33, false); + } + case KnownHeaderType.AccessControlAllowOrigin: + switch (value) + { + case "*": + return (35, true); + default: + return (35, false); + } + case KnownHeaderType.CacheControl: + switch (value) + { + case "max-age=0": + return (36, true); + case "max-age=2592000": + return (37, true); + case "max-age=604800": + return (38, true); + case "no-cache": + return (39, true); + case "no-store": + return (40, true); + case "public, max-age=31536000": + return (41, true); + default: + return (36, false); + } + case KnownHeaderType.ContentEncoding: + switch (value) + { + case "br": + return (42, true); + case "gzip": + return (43, true); + default: + return (42, false); + } + case KnownHeaderType.ContentType: + switch (value) + { + case "application/dns-message": + return (44, true); + case "application/javascript": + return (45, true); + case "application/json": + return (46, true); + case "application/x-www-form-urlencoded": + return (47, true); + case "image/gif": + return (48, true); + case "image/jpeg": + return (49, true); + case "image/png": + return (50, true); + case "text/css": + return (51, true); + case "text/html; charset=utf-8": + return (52, true); + case "text/plain": + return (53, true); + case "text/plain;charset=utf-8": + return (54, true); + default: + return (44, false); + } + case KnownHeaderType.Vary: + switch (value) + { + case "accept-encoding": + return (59, true); + case "origin": + return (60, true); + default: + return (59, false); + } + case KnownHeaderType.AccessControlAllowCredentials: + switch (value) + { + case "FALSE": + return (73, true); + case "TRUE": + return (74, true); + default: + return (73, false); + } + case KnownHeaderType.AccessControlAllowMethods: + switch (value) + { + case "get": + return (76, true); + case "get, post, options": + return (77, true); + case "options": + return (78, true); + default: + return (76, false); + } + case KnownHeaderType.AccessControlExposeHeaders: + switch (value) + { + case "content-length": + return (79, true); + default: + return (79, false); + } + case KnownHeaderType.AltSvc: + switch (value) + { + case "clear": + return (83, true); + default: + return (83, false); + } + case KnownHeaderType.Server: + return (92, false); + + default: + return (-1, false); + } + } + } + internal partial class HttpHeaders { private readonly static HashSet _internedHeaderNames = new HashSet(96, StringComparer.OrdinalIgnoreCase) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs index 8c65d7bd74e4..c4edf9d95abe 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3HeadersEnumerator.cs @@ -29,7 +29,7 @@ private enum HeadersType : byte public Func EncodingSelector { get; set; } = KestrelServerOptions.DefaultHeaderEncodingSelector; - public int QPackStaticTableId => GetResponseHeaderStaticTableId(_knownHeaderType); + public (int index, bool matchedValue) GetQPackStaticTableId() => HttpHeadersCompression.MatchKnownHeaderQPack(_knownHeaderType, Current.Value); public KeyValuePair Current { get; private set; } object IEnumerator.Current => Current; @@ -145,66 +145,4 @@ public void Reset() public void Dispose() { } - - internal static int GetResponseHeaderStaticTableId(KnownHeaderType responseHeaderType) - { - // Removed from this test are request-only headers, e.g. cookie. - // - // Not every header in the QPACK static table is known. - // These are missing from this test and the full header name is written. - // Missing: - // - link - // - location - // - strict-transport-security - // - x-content-type-options - // - x-xss-protection - // - content-security-policy - // - early-data - // - expect-ct - // - purpose - // - timing-allow-origin - // - x-forwarded-for - // - x-frame-options - switch (responseHeaderType) - { - case KnownHeaderType.Age: - return H3StaticTable.Age0; - case KnownHeaderType.ContentLength: - return H3StaticTable.ContentLength0; - case KnownHeaderType.Date: - return H3StaticTable.Date; - case KnownHeaderType.ETag: - return H3StaticTable.ETag; - case KnownHeaderType.LastModified: - return H3StaticTable.LastModified; - case KnownHeaderType.Location: - return H3StaticTable.Location; - case KnownHeaderType.SetCookie: - return H3StaticTable.SetCookie; - case KnownHeaderType.AcceptRanges: - return H3StaticTable.AcceptRangesBytes; - case KnownHeaderType.AccessControlAllowHeaders: - return H3StaticTable.AccessControlAllowHeadersCacheControl; - case KnownHeaderType.AccessControlAllowOrigin: - return H3StaticTable.AccessControlAllowOriginAny; - case KnownHeaderType.CacheControl: - return H3StaticTable.CacheControlMaxAge0; - case KnownHeaderType.ContentEncoding: - return H3StaticTable.ContentEncodingBr; - case KnownHeaderType.ContentType: - return H3StaticTable.ContentTypeApplicationDnsMessage; - case KnownHeaderType.Vary: - return H3StaticTable.VaryAcceptEncoding; - case KnownHeaderType.AccessControlAllowCredentials: - return H3StaticTable.AccessControlAllowCredentials; - case KnownHeaderType.AccessControlAllowMethods: - return H3StaticTable.AccessControlAllowMethodsGet; - case KnownHeaderType.AltSvc: - return H3StaticTable.AltSvcClear; - case KnownHeaderType.Server: - return H3StaticTable.Server; - default: - return -1; - } - } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs index 7474de78c3be..b47bcfeb6faf 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/QPackHeaderWriter.cs @@ -59,19 +59,39 @@ private static bool Encode(Http3HeadersEnumerator headersEnumerator, Span do { - var staticTableId = headersEnumerator.QPackStaticTableId; + // Match the current header to the QPACK static table. Possible outcomes: + // 1. Known header and value. Write index. + // 2. Known header with custom value. Write name index and full value. + // 3. Unknown header. Write full name and value. + var (staticTableId, matchedValue) = headersEnumerator.GetQPackStaticTableId(); var name = headersEnumerator.Current.Key; var value = headersEnumerator.Current.Value; - var valueEncoding = ReferenceEquals(headersEnumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) - ? null : headersEnumerator.EncodingSelector(name); - if (!EncodeHeader(buffer.Slice(length), staticTableId, name, value, valueEncoding, out var headerLength)) + int headerLength; + if (matchedValue) { - if (length == 0 && throwIfNoneEncoded) + if (!QPackEncoder.EncodeStaticIndexedHeaderField(staticTableId, buffer.Slice(length), out headerLength)) { - throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */); + if (length == 0 && throwIfNoneEncoded) + { + throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */); + } + return false; + } + } + else + { + var valueEncoding = ReferenceEquals(headersEnumerator.EncodingSelector, KestrelServerOptions.DefaultHeaderEncodingSelector) + ? null : headersEnumerator.EncodingSelector(name); + + if (!EncodeHeader(buffer.Slice(length), staticTableId, name, value, valueEncoding, out headerLength)) + { + if (length == 0 && throwIfNoneEncoded) + { + throw new QPackEncodingException("TODO sync with corefx" /* CoreStrings.HPackErrorNotEnoughBuffer */); + } + return false; } - return false; } // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.3 diff --git a/src/Servers/Kestrel/Core/test/Http3/Http3HeadersEnumeratorTests.cs b/src/Servers/Kestrel/Core/test/Http3/Http3HeadersEnumeratorTests.cs index e2f3b242fc54..4e0ebd7c314d 100644 --- a/src/Servers/Kestrel/Core/test/Http3/Http3HeadersEnumeratorTests.cs +++ b/src/Servers/Kestrel/Core/test/Http3/Http3HeadersEnumeratorTests.cs @@ -93,17 +93,20 @@ public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource() Assert.True(e.MoveNext()); Assert.Equal("Name1", e.Current.Key); Assert.Equal("Value1", e.Current.Value); - Assert.Equal(-1, e.QPackStaticTableId); + var (index, matchedValue) = e.GetQPackStaticTableId(); + Assert.Equal(-1, index); Assert.True(e.MoveNext()); Assert.Equal("Name2", e.Current.Key); Assert.Equal("Value2-1", e.Current.Value); - Assert.Equal(-1, e.QPackStaticTableId); + (index, matchedValue) = e.GetQPackStaticTableId(); + Assert.Equal(-1, index); Assert.True(e.MoveNext()); Assert.Equal("Name2", e.Current.Key); Assert.Equal("Value2-2", e.Current.Value); - Assert.Equal(-1, e.QPackStaticTableId); + (index, matchedValue) = e.GetQPackStaticTableId(); + Assert.Equal(-1, index); var responseTrailers = (IHeaderDictionary)new HttpResponseTrailers(); @@ -118,22 +121,26 @@ public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource() Assert.True(e.MoveNext()); Assert.Equal("Grpc-Status", e.Current.Key); Assert.Equal("1", e.Current.Value); - Assert.Equal(-1, e.QPackStaticTableId); + (index, matchedValue) = e.GetQPackStaticTableId(); + Assert.Equal(-1, index); Assert.True(e.MoveNext()); Assert.Equal("Name1", e.Current.Key); Assert.Equal("Value1", e.Current.Value); - Assert.Equal(-1, e.QPackStaticTableId); + (index, matchedValue) = e.GetQPackStaticTableId(); + Assert.Equal(-1, index); Assert.True(e.MoveNext()); Assert.Equal("Name2", e.Current.Key); Assert.Equal("Value2-1", e.Current.Value); - Assert.Equal(-1, e.QPackStaticTableId); + (index, matchedValue) = e.GetQPackStaticTableId(); + Assert.Equal(-1, index); Assert.True(e.MoveNext()); Assert.Equal("Name2", e.Current.Key); Assert.Equal("Value2-2", e.Current.Value); - Assert.Equal(-1, e.QPackStaticTableId); + (index, matchedValue) = e.GetQPackStaticTableId(); + Assert.Equal(-1, index); Assert.False(e.MoveNext()); } @@ -143,7 +150,7 @@ public void Initialize_ChangeHeadersSource_EnumeratorUsesNewSource() var headers = new List<(int HPackStaticTableId, string Name, string Value)>(); while (enumerator.MoveNext()) { - headers.Add(CreateHeaderResult(enumerator.QPackStaticTableId, enumerator.Current.Key, enumerator.Current.Value)); + headers.Add(CreateHeaderResult(enumerator.GetQPackStaticTableId().index, enumerator.Current.Key, enumerator.Current.Value)); } return headers.ToArray(); } diff --git a/src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs b/src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs index 10f888668257..159c59b47da8 100644 --- a/src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs +++ b/src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs @@ -73,6 +73,25 @@ public void BeginEncodeHeaders_StatusWithIndexedValue_ExpectedLength(int statusC Assert.True(length <= 2, "Indexed header should be encoded into 1 or 2 bytes"); } + [Fact] + public void BeginEncodeHeaders_StaticKeyAndValue_WriteIndex() + { + Span buffer = new byte[1024 * 16]; + + var headers = (IHeaderDictionary)new HttpResponseHeaders(); + headers.ContentType = "application/json"; + + var totalHeaderSize = 0; + var enumerator = new Http3HeadersEnumerator(); + enumerator.Initialize(headers); + + Assert.True(QPackHeaderWriter.BeginEncodeHeaders(enumerator, buffer, ref totalHeaderSize, out var length)); + + var result = buffer.Slice(2, length - 2).ToArray(); // trim prefix + var hex = BitConverter.ToString(result); + Assert.Equal("EE", hex); + } + [Fact] public void BeginEncodeHeaders_NonStaticKey_WriteFullNameAndFullValue() { diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index 079df9639367..fc6a5d84fa56 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Net.Http.HPack; +using System.Net.Http.QPack; using System.Reflection; using System.Text; using Microsoft.Net.Http.Headers; @@ -20,33 +21,33 @@ public class KnownHeaders public static readonly KnownHeader[] ResponseTrailers; public static readonly string[] InternalHeaderAccessors = new[] { - HeaderNames.Allow, - HeaderNames.AltSvc, - HeaderNames.TransferEncoding, - HeaderNames.ContentLength, - HeaderNames.Connection, - HeaderNames.Scheme, - HeaderNames.Path, - HeaderNames.Method, - HeaderNames.Authority, - HeaderNames.Host, - }; + HeaderNames.Allow, + HeaderNames.AltSvc, + HeaderNames.TransferEncoding, + HeaderNames.ContentLength, + HeaderNames.Connection, + HeaderNames.Scheme, + HeaderNames.Path, + HeaderNames.Method, + HeaderNames.Authority, + HeaderNames.Host, + }; public static readonly string[] DefinedHeaderNames = typeof(HeaderNames).GetFields(BindingFlags.Static | BindingFlags.Public).Select(h => h.Name).ToArray(); public static readonly string[] ObsoleteHeaderNames = new[] { - HeaderNames.DNT, - }; + HeaderNames.DNT, + }; public static readonly string[] PseudoHeaderNames = new[] { - "Authority", // :authority - "Method", // :method - "Path", // :path - "Scheme", // :scheme - "Status" // :status - }; + "Authority", // :authority + "Method", // :method + "Path", // :path + "Scheme", // :scheme + "Status" // :status + }; public static readonly string[] NonApiHeaders = ObsoleteHeaderNames @@ -65,85 +66,85 @@ static KnownHeaders() { var requestPrimaryHeaders = new[] { - HeaderNames.Accept, - HeaderNames.Connection, - HeaderNames.Host, - HeaderNames.UserAgent - }; + HeaderNames.Accept, + HeaderNames.Connection, + HeaderNames.Host, + HeaderNames.UserAgent + }; var responsePrimaryHeaders = new[] { - HeaderNames.Connection, - HeaderNames.Date, - HeaderNames.ContentType, - HeaderNames.Server, - HeaderNames.ContentLength, - }; + HeaderNames.Connection, + HeaderNames.Date, + HeaderNames.ContentType, + HeaderNames.Server, + HeaderNames.ContentLength, + }; var commonHeaders = new[] { - HeaderNames.CacheControl, - HeaderNames.Connection, - HeaderNames.Date, - HeaderNames.GrpcEncoding, - HeaderNames.KeepAlive, - HeaderNames.Pragma, - HeaderNames.TransferEncoding, - HeaderNames.Upgrade, - HeaderNames.Via, - HeaderNames.Warning, - HeaderNames.ContentType, - }; + HeaderNames.CacheControl, + HeaderNames.Connection, + HeaderNames.Date, + HeaderNames.GrpcEncoding, + HeaderNames.KeepAlive, + HeaderNames.Pragma, + HeaderNames.TransferEncoding, + HeaderNames.Upgrade, + HeaderNames.Via, + HeaderNames.Warning, + HeaderNames.ContentType, + }; // http://www.w3.org/TR/cors/#syntax var corsRequestHeaders = new[] { - HeaderNames.Origin, - HeaderNames.AccessControlRequestMethod, - HeaderNames.AccessControlRequestHeaders, - }; + HeaderNames.Origin, + HeaderNames.AccessControlRequestMethod, + HeaderNames.AccessControlRequestHeaders, + }; var requestHeadersExistence = new[] { - HeaderNames.Connection, - HeaderNames.TransferEncoding, - }; + HeaderNames.Connection, + HeaderNames.TransferEncoding, + }; var requestHeadersCount = new[] { - HeaderNames.Host - }; + HeaderNames.Host + }; RequestHeaders = commonHeaders.Concat(new[] { - HeaderNames.Authority, - HeaderNames.Method, - HeaderNames.Path, - HeaderNames.Scheme, - HeaderNames.Accept, - HeaderNames.AcceptCharset, - HeaderNames.AcceptEncoding, - HeaderNames.AcceptLanguage, - HeaderNames.Authorization, - HeaderNames.Cookie, - HeaderNames.Expect, - HeaderNames.From, - HeaderNames.GrpcAcceptEncoding, - HeaderNames.GrpcTimeout, - HeaderNames.Host, - HeaderNames.IfMatch, - HeaderNames.IfModifiedSince, - HeaderNames.IfNoneMatch, - HeaderNames.IfRange, - HeaderNames.IfUnmodifiedSince, - HeaderNames.MaxForwards, - HeaderNames.ProxyAuthorization, - HeaderNames.Referer, - HeaderNames.Range, - HeaderNames.TE, - HeaderNames.Translate, - HeaderNames.UserAgent, - HeaderNames.UpgradeInsecureRequests, - HeaderNames.RequestId, - HeaderNames.CorrelationContext, - HeaderNames.TraceParent, - HeaderNames.TraceState, - HeaderNames.Baggage, - }) + HeaderNames.Authority, + HeaderNames.Method, + HeaderNames.Path, + HeaderNames.Scheme, + HeaderNames.Accept, + HeaderNames.AcceptCharset, + HeaderNames.AcceptEncoding, + HeaderNames.AcceptLanguage, + HeaderNames.Authorization, + HeaderNames.Cookie, + HeaderNames.Expect, + HeaderNames.From, + HeaderNames.GrpcAcceptEncoding, + HeaderNames.GrpcTimeout, + HeaderNames.Host, + HeaderNames.IfMatch, + HeaderNames.IfModifiedSince, + HeaderNames.IfNoneMatch, + HeaderNames.IfRange, + HeaderNames.IfUnmodifiedSince, + HeaderNames.MaxForwards, + HeaderNames.ProxyAuthorization, + HeaderNames.Referer, + HeaderNames.Range, + HeaderNames.TE, + HeaderNames.Translate, + HeaderNames.UserAgent, + HeaderNames.UpgradeInsecureRequests, + HeaderNames.RequestId, + HeaderNames.CorrelationContext, + HeaderNames.TraceParent, + HeaderNames.TraceState, + HeaderNames.Baggage, + }) .Concat(corsRequestHeaders) .OrderBy(header => header) .OrderBy(header => !requestPrimaryHeaders.Contains(header)) @@ -165,54 +166,54 @@ static KnownHeaders() var responseHeadersExistence = new[] { - HeaderNames.Connection, - HeaderNames.Server, - HeaderNames.Date, - HeaderNames.TransferEncoding, - HeaderNames.AltSvc - }; + HeaderNames.Connection, + HeaderNames.Server, + HeaderNames.Date, + HeaderNames.TransferEncoding, + HeaderNames.AltSvc + }; var enhancedHeaders = new[] { - HeaderNames.Connection, - HeaderNames.Server, - HeaderNames.Date, - HeaderNames.TransferEncoding, - HeaderNames.AltSvc - }; + HeaderNames.Connection, + HeaderNames.Server, + HeaderNames.Date, + HeaderNames.TransferEncoding, + HeaderNames.AltSvc + }; // http://www.w3.org/TR/cors/#syntax var corsResponseHeaders = new[] { - HeaderNames.AccessControlAllowCredentials, - HeaderNames.AccessControlAllowHeaders, - HeaderNames.AccessControlAllowMethods, - HeaderNames.AccessControlAllowOrigin, - HeaderNames.AccessControlExposeHeaders, - HeaderNames.AccessControlMaxAge, - }; + HeaderNames.AccessControlAllowCredentials, + HeaderNames.AccessControlAllowHeaders, + HeaderNames.AccessControlAllowMethods, + HeaderNames.AccessControlAllowOrigin, + HeaderNames.AccessControlExposeHeaders, + HeaderNames.AccessControlMaxAge, + }; ResponseHeaders = commonHeaders.Concat(new[] { - HeaderNames.AcceptRanges, - HeaderNames.Age, - HeaderNames.Allow, - HeaderNames.AltSvc, - HeaderNames.ETag, - HeaderNames.Location, - HeaderNames.ProxyAuthenticate, - HeaderNames.ProxyConnection, - HeaderNames.RetryAfter, - HeaderNames.Server, - HeaderNames.SetCookie, - HeaderNames.Vary, - HeaderNames.Expires, - HeaderNames.WWWAuthenticate, - HeaderNames.ContentRange, - HeaderNames.ContentEncoding, - HeaderNames.ContentLanguage, - HeaderNames.ContentLocation, - HeaderNames.ContentMD5, - HeaderNames.LastModified, - HeaderNames.Trailer, - }) + HeaderNames.AcceptRanges, + HeaderNames.Age, + HeaderNames.Allow, + HeaderNames.AltSvc, + HeaderNames.ETag, + HeaderNames.Location, + HeaderNames.ProxyAuthenticate, + HeaderNames.ProxyConnection, + HeaderNames.RetryAfter, + HeaderNames.Server, + HeaderNames.SetCookie, + HeaderNames.Vary, + HeaderNames.Expires, + HeaderNames.WWWAuthenticate, + HeaderNames.ContentRange, + HeaderNames.ContentEncoding, + HeaderNames.ContentLanguage, + HeaderNames.ContentLocation, + HeaderNames.ContentMD5, + HeaderNames.LastModified, + HeaderNames.Trailer, + }) .Concat(corsResponseHeaders) .OrderBy(header => header) .OrderBy(header => !responsePrimaryHeaders.Contains(header)) @@ -235,10 +236,10 @@ static KnownHeaders() ResponseTrailers = new[] { - HeaderNames.ETag, - HeaderNames.GrpcMessage, - HeaderNames.GrpcStatus - } + HeaderNames.ETag, + HeaderNames.GrpcMessage, + HeaderNames.GrpcStatus + } .OrderBy(header => header) .OrderBy(header => !responsePrimaryHeaders.Contains(header)) .Select((header, index) => new KnownHeader @@ -253,12 +254,12 @@ static KnownHeaders() var invalidH2H3ResponseHeaders = new[] { - HeaderNames.Connection, - HeaderNames.TransferEncoding, - HeaderNames.KeepAlive, - HeaderNames.Upgrade, - HeaderNames.ProxyConnection - }; + HeaderNames.Connection, + HeaderNames.TransferEncoding, + HeaderNames.KeepAlive, + HeaderNames.Upgrade, + HeaderNames.ProxyConnection + }; InvalidH2H3ResponseHeadersBits = ResponseHeaders .Where(header => invalidH2H3ResponseHeaders.Contains(header.Name)) @@ -728,7 +729,6 @@ public string EqualIgnoreCaseBytesSecondTermOnwards() public static string GeneratedFile() { - var requestHeaders = RequestHeaders; Debug.Assert(requestHeaders.Length <= 64); Debug.Assert(requestHeaders.Max(x => x.Index) <= 62); @@ -805,6 +805,11 @@ internal enum KnownHeaderType " + n + ",")} }} + internal static class HttpHeadersCompression + {{ + {GetQPackStaticTableMatch()} + }} + internal partial class HttpHeaders {{ {GetHeaderLookup()} @@ -1383,9 +1388,68 @@ private static string GetHeaderLookup() }};"; } + private static string GetQPackStaticTableMatch() + { + var group = GroupQPack(ResponseHeaders); + + return @$"internal static (int index, bool matchedValue) MatchKnownHeaderQPack(KnownHeaderType knownHeader, string value) + {{ + switch (knownHeader) + {{ + {Each(group, (h) => @$"case KnownHeaderType.{h.Header.Identifier}: + {AppendQPackSwitch(h.QPackStaticTableFields.OrderBy(t => t.Index).ToList())} + ")} + default: + return (-1, false); + }} + }}"; + } + + private static string AppendQPackSwitch(IList<(int Index, System.Net.Http.QPack.HeaderField Field)> values) + { + if (values.Count == 1 && values[0].Field.Value.Length == 0) + { + // Skip check if the only value is empty string. Empty string wasn't chosen because it is common. + // Instead it is the default value when there isn't a common value for the header. + return $"return ({values[0].Index}, false);"; + } + else + { + // Use smallest index if there is no match. Smaller number is more likely to fit into a single byte. + return $@"switch (value) + {{{Each(values, value => $@" + case ""{Encoding.ASCII.GetString(value.Field.Value)}"": + return ({value.Index}, true);")} + default: + return ({values.Min(v => v.Index)}, false); + }}"; + } + } + + private static IEnumerable GroupQPack(KnownHeader[] headers) + { + var staticHeaders = new (int Index, System.Net.Http.QPack.HeaderField HeaderField)[H3StaticTable.Count]; + for (var i = 0; i < H3StaticTable.Count; i++) + { + staticHeaders[i] = (i, H3StaticTable.GetHeaderFieldAt(i)); + } + + var groupedHeaders = staticHeaders.GroupBy(h => Encoding.ASCII.GetString(h.HeaderField.Name)).Select(g => + { + return new QPackGroup + { + Name = g.Key, + Header = headers.SingleOrDefault(knownHeader => string.Equals(knownHeader.Name, g.Key, StringComparison.OrdinalIgnoreCase)), + QPackStaticTableFields = g.ToArray() + }; + }).Where(g => g.Header != null).ToList(); + + return groupedHeaders; + } + private static IEnumerable GroupHPack(KnownHeader[] headers) { - var staticHeaders = new (int Index, HeaderField HeaderField)[H2StaticTable.Count]; + var staticHeaders = new (int Index, System.Net.Http.HPack.HeaderField HeaderField)[H2StaticTable.Count]; for (var i = 0; i < H2StaticTable.Count; i++) { staticHeaders[i] = (i + 1, H2StaticTable.Get(i)); @@ -1404,6 +1468,13 @@ private static IEnumerable GroupHPack(KnownHeader[] headers) return groupedHeaders; } + private class QPackGroup + { + public (int Index, System.Net.Http.QPack.HeaderField Field)[] QPackStaticTableFields { get; set; } + public KnownHeader Header { get; set; } + public string Name { get; set; } + } + private class HPackGroup { public int[] HPackStaticTableIndexes { get; set; } diff --git a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj index ebfe9f77d71f..20e3d4d88047 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj +++ b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj @@ -12,6 +12,9 @@ + + + From 2eacb37d53cba6f217b89e2cf7ee560103cab5e4 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 30 Nov 2021 11:42:55 +1300 Subject: [PATCH 2/3] Fix merge --- src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs | 4 ++-- src/Servers/Kestrel/shared/KnownHeaders.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs b/src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs index 159c59b47da8..8cf1572daa30 100644 --- a/src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs +++ b/src/Servers/Kestrel/Core/test/Http3/Http3QPackEncoderTests.cs @@ -136,7 +136,7 @@ public void BeginEncodeHeaders_StaticKey_WriteStaticNameAndFullValue() Span buffer = new byte[1024 * 16]; var headers = (IHeaderDictionary)new HttpResponseHeaders(); - headers.ContentType = "application/json"; + headers.ContentType = "application/custom"; var totalHeaderSize = 0; var enumerator = new Http3HeadersEnumerator(); @@ -146,6 +146,6 @@ public void BeginEncodeHeaders_StaticKey_WriteStaticNameAndFullValue() var result = buffer.Slice(2, length - 2).ToArray(); var hex = BitConverter.ToString(result); - Assert.Equal("5F-1D-10-61-70-70-6C-69-63-61-74-69-6F-6E-2F-6A-73-6F-6E", hex); + Assert.Equal("5F-1D-12-61-70-70-6C-69-63-61-74-69-6F-6E-2F-63-75-73-74-6F-6D", hex); } } diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index fc6a5d84fa56..73a3e6e8310d 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -1431,7 +1431,7 @@ private static IEnumerable GroupQPack(KnownHeader[] headers) var staticHeaders = new (int Index, System.Net.Http.QPack.HeaderField HeaderField)[H3StaticTable.Count]; for (var i = 0; i < H3StaticTable.Count; i++) { - staticHeaders[i] = (i, H3StaticTable.GetHeaderFieldAt(i)); + staticHeaders[i] = (i, H3StaticTable.Get(i)); } var groupedHeaders = staticHeaders.GroupBy(h => Encoding.ASCII.GetString(h.HeaderField.Name)).Select(g => From 28a9cfdebbd388dab2f4983ad0bc682e91165078 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 30 Nov 2021 12:46:38 +1300 Subject: [PATCH 3/3] Fix case --- .../Kestrel/tools/CodeGenerator/CodeGenerator.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj index 20e3d4d88047..1fbcacd357f7 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj +++ b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj @@ -12,9 +12,9 @@ - - - + + +