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 aac89b93be..d80fe0b84c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs @@ -6617,7 +6617,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http ref StringValues values = ref Unsafe.AsRef(null); var flag = 0L; - // Does the name matched any "known" headers + // Does the name match any "known" headers switch (name.Length) { case 2: @@ -7070,6 +7070,251 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public unsafe bool TryHPackAppend(int index, ReadOnlySpan value) + { + ref StringValues values = ref Unsafe.AsRef(null); + var nameStr = string.Empty; + var flag = 0L; + + // Does the HPack static index match any "known" headers + switch (index) + { + case 1: + flag = 0x100000L; + values = ref _headers._Authority; + nameStr = HeaderNames.Authority; + break; + case 2: + case 3: + flag = 0x200000L; + values = ref _headers._Method; + nameStr = HeaderNames.Method; + break; + case 4: + case 5: + flag = 0x400000L; + values = ref _headers._Path; + nameStr = HeaderNames.Path; + break; + case 6: + case 7: + flag = 0x800000L; + values = ref _headers._Scheme; + nameStr = HeaderNames.Scheme; + break; + case 15: + flag = 0x2000000L; + values = ref _headers._AcceptCharset; + nameStr = HeaderNames.AcceptCharset; + break; + case 16: + flag = 0x4000000L; + values = ref _headers._AcceptEncoding; + nameStr = HeaderNames.AcceptEncoding; + break; + case 17: + flag = 0x8000000L; + values = ref _headers._AcceptLanguage; + nameStr = HeaderNames.AcceptLanguage; + break; + case 19: + flag = 0x1000000L; + values = ref _headers._Accept; + nameStr = HeaderNames.Accept; + break; + case 22: + flag = 0x800L; + values = ref _headers._Allow; + nameStr = HeaderNames.Allow; + break; + case 23: + flag = 0x10000000L; + values = ref _headers._Authorization; + nameStr = HeaderNames.Authorization; + break; + case 24: + flag = 0x1L; + values = ref _headers._CacheControl; + nameStr = HeaderNames.CacheControl; + break; + case 26: + flag = 0x2000L; + values = ref _headers._ContentEncoding; + nameStr = HeaderNames.ContentEncoding; + break; + case 27: + flag = 0x4000L; + values = ref _headers._ContentLanguage; + nameStr = HeaderNames.ContentLanguage; + break; + case 28: + if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector)) + { + AppendContentLength(value); + } + else + { + AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength)); + } + return true; + case 29: + flag = 0x8000L; + values = ref _headers._ContentLocation; + nameStr = HeaderNames.ContentLocation; + break; + case 30: + flag = 0x20000L; + values = ref _headers._ContentRange; + nameStr = HeaderNames.ContentRange; + break; + case 31: + flag = 0x1000L; + values = ref _headers._ContentType; + nameStr = HeaderNames.ContentType; + break; + case 32: + flag = 0x20000000L; + values = ref _headers._Cookie; + nameStr = HeaderNames.Cookie; + break; + case 33: + flag = 0x4L; + values = ref _headers._Date; + nameStr = HeaderNames.Date; + break; + case 35: + flag = 0x40000000L; + values = ref _headers._Expect; + nameStr = HeaderNames.Expect; + break; + case 36: + flag = 0x40000L; + values = ref _headers._Expires; + nameStr = HeaderNames.Expires; + break; + case 37: + flag = 0x80000000L; + values = ref _headers._From; + nameStr = HeaderNames.From; + break; + case 38: + flag = 0x400000000L; + values = ref _headers._Host; + nameStr = HeaderNames.Host; + break; + case 39: + flag = 0x800000000L; + values = ref _headers._IfMatch; + nameStr = HeaderNames.IfMatch; + break; + case 40: + flag = 0x1000000000L; + values = ref _headers._IfModifiedSince; + nameStr = HeaderNames.IfModifiedSince; + break; + case 41: + flag = 0x2000000000L; + values = ref _headers._IfNoneMatch; + nameStr = HeaderNames.IfNoneMatch; + break; + case 42: + flag = 0x4000000000L; + values = ref _headers._IfRange; + nameStr = HeaderNames.IfRange; + break; + case 43: + flag = 0x8000000000L; + values = ref _headers._IfUnmodifiedSince; + nameStr = HeaderNames.IfUnmodifiedSince; + break; + case 44: + flag = 0x80000L; + values = ref _headers._LastModified; + nameStr = HeaderNames.LastModified; + break; + case 47: + flag = 0x10000000000L; + values = ref _headers._MaxForwards; + nameStr = HeaderNames.MaxForwards; + break; + case 49: + flag = 0x20000000000L; + values = ref _headers._ProxyAuthorization; + nameStr = HeaderNames.ProxyAuthorization; + break; + case 50: + flag = 0x80000000000L; + values = ref _headers._Range; + nameStr = HeaderNames.Range; + break; + case 51: + flag = 0x40000000000L; + values = ref _headers._Referer; + nameStr = HeaderNames.Referer; + break; + case 57: + flag = 0x80L; + values = ref _headers._TransferEncoding; + nameStr = HeaderNames.TransferEncoding; + break; + case 58: + flag = 0x400000000000L; + values = ref _headers._UserAgent; + nameStr = HeaderNames.UserAgent; + break; + case 60: + flag = 0x200L; + values = ref _headers._Via; + nameStr = HeaderNames.Via; + break; + } + + if (flag != 0) + { + // Matched a known header + if ((_previousBits & flag) != 0) + { + // Had a previous string for this header, mark it as used so we don't clear it OnHeadersComplete or consider it if we get a second header + _previousBits ^= flag; + + // We will only reuse this header if there was only one previous header + if (values.Count == 1) + { + var previousValue = values.ToString(); + // Check lengths are the same, then if the bytes were converted to an ascii string if they would be the same. + // We do not consider Utf8 headers for reuse. + if (previousValue.Length == value.Length && + StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, value)) + { + // The previous string matches what the bytes would convert to, so we will just use that one. + _bits |= flag; + return true; + } + } + } + + // We didn't have a previous matching header value, or have already added a header, so get the string for this value. + var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector); + if ((_bits & flag) == 0) + { + // We didn't already have a header set, so add a new one. + _bits |= flag; + values = new StringValues(valueStr); + } + else + { + // We already had a header set, so concatenate the new one. + values = AppendValue(values, valueStr); + } + return true; + } + else + { + return false; + } + } + private struct HeaderReferences { public StringValues _CacheControl; @@ -13716,4 +13961,4 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } } -} +} \ No newline at end of file diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 13065f095e..cf0f2ddf81 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -513,27 +513,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public virtual void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { - _requestHeadersParsed++; - if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) - { - KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); - } + IncrementRequestHeadersCount(); HttpRequestHeaders.Append(name, value); } + public virtual void OnHeader(int index, ReadOnlySpan name, ReadOnlySpan value) + { + IncrementRequestHeadersCount(); + + // This method should be overriden in specific implementations and the base should be + // called to validate the header count. + } + public void OnTrailer(ReadOnlySpan name, ReadOnlySpan value) { - // Trailers still count towards the limit. + IncrementRequestHeadersCount(); + + string key = name.GetHeaderName(); + var valueStr = value.GetRequestHeaderString(key, HttpRequestHeaders.EncodingSelector); + RequestTrailers.Append(key, valueStr); + } + + private void IncrementRequestHeadersCount() + { _requestHeadersParsed++; if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount) { KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders); } - - string key = name.GetHeaderName(); - var valueStr = value.GetRequestHeaderString(key, HttpRequestHeaders.EncodingSelector); - RequestTrailers.Append(key, valueStr); } public void OnHeadersComplete() diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 14b95a93bd..3040c54107 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -1230,6 +1230,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // For now these either need to be connection errors or BadRequests. If we want to downgrade any of them to stream errors later then we need to // rework the flow so that the remaining headers are drained and the decompression state is maintained. public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) + { + OnHeaderCore(index: null, name, value); + } + + public void OnStaticIndexedHeader(int index) + { + Debug.Assert(index <= H2StaticTable.Count); + + ref readonly var entry = ref H2StaticTable.Get(index - 1); + OnHeaderCore(index, entry.Name, entry.Value); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + Debug.Assert(index <= H2StaticTable.Count); + + OnHeaderCore(index, H2StaticTable.Get(index - 1).Name, value); + } + + // We can't throw a Http2StreamErrorException here, it interrupts the header decompression state and may corrupt subsequent header frames on other streams. + // For now these either need to be connection errors or BadRequests. If we want to downgrade any of them to stream errors later then we need to + // rework the flow so that the remaining headers are drained and the decompression state is maintained. + private void OnHeaderCore(int? index, ReadOnlySpan name, ReadOnlySpan value) { // https://tools.ietf.org/html/rfc7540#section-6.5.2 // "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field."; @@ -1239,7 +1262,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 throw new Http2ConnectionErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http2ErrorCode.PROTOCOL_ERROR); } - ValidateHeader(name, value); + if (index != null) + { + ValidateStaticHeader(index.Value, value); + } + else + { + ValidateHeader(name, value); + } + try { if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) @@ -1250,7 +1281,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { // Throws BadRequest for header count limit breaches. // Throws InvalidOperation for bad encoding. - _currentHeadersStream.OnHeader(name, value); + if (index != null) + { + _currentHeadersStream.OnHeader(index.Value, name, value); + } + else + { + _currentHeadersStream.OnHeader(name, value); + } } } catch (Microsoft.AspNetCore.Http.BadHttpRequestException bre) @@ -1267,6 +1305,62 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 => _currentHeadersStream.OnHeadersComplete(); private void ValidateHeader(ReadOnlySpan name, ReadOnlySpan value) + { + // Clients will normally send pseudo headers as an indexed header. + // Because pseudo headers can still be sent by name we need to check for them. + UpdateHeaderParsingState(value, GetPseudoHeaderField(name)); + + if (IsConnectionSpecificHeaderField(name, value)) + { + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR); + } + + // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2 + // A request or response containing uppercase header field names MUST be treated as malformed (Section 8.1.2.6). + for (var i = 0; i < name.Length; i++) + { + if (((uint)name[i] - 65) <= (90 - 65)) + { + if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) + { + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR); + } + else + { + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR); + } + } + } + } + + private void ValidateStaticHeader(int index, ReadOnlySpan value) + { + var headerField = index switch + { + 1 => PseudoHeaderFields.Authority, + 2 => PseudoHeaderFields.Method, + 3 => PseudoHeaderFields.Method, + 4 => PseudoHeaderFields.Path, + 5 => PseudoHeaderFields.Path, + 6 => PseudoHeaderFields.Scheme, + 7 => PseudoHeaderFields.Scheme, + 8 => PseudoHeaderFields.Status, + 9 => PseudoHeaderFields.Status, + 10 => PseudoHeaderFields.Status, + 11 => PseudoHeaderFields.Status, + 12 => PseudoHeaderFields.Status, + 13 => PseudoHeaderFields.Status, + 14 => PseudoHeaderFields.Status, + _ => PseudoHeaderFields.None + }; + + UpdateHeaderParsingState(value, headerField); + + // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2 + // No need to validate if header name if it is specified using a static index. + } + + private void UpdateHeaderParsingState(ReadOnlySpan value, PseudoHeaderFields headerField) { // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1 /* @@ -1282,7 +1376,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 against several types of common attacks against HTTP; they are deliberately strict because being permissive can expose implementations to these vulnerabilities.*/ - if (IsPseudoHeaderField(name, out var headerField)) + if (headerField != PseudoHeaderFields.None) { if (_requestHeaderParsingState == RequestHeaderParsingState.Headers) { @@ -1332,65 +1426,38 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { _requestHeaderParsingState = RequestHeaderParsingState.Headers; } - - if (IsConnectionSpecificHeaderField(name, value)) - { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR); - } - - // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2 - // A request or response containing uppercase header field names MUST be treated as malformed (Section 8.1.2.6). - for (var i = 0; i < name.Length; i++) - { - if (name[i] >= 65 && name[i] <= 90) - { - if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) - { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR); - } - else - { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR); - } - } - } } - private bool IsPseudoHeaderField(ReadOnlySpan name, out PseudoHeaderFields headerField) + private PseudoHeaderFields GetPseudoHeaderField(ReadOnlySpan name) { - headerField = PseudoHeaderFields.None; - if (name.IsEmpty || name[0] != (byte)':') { - return false; + return PseudoHeaderFields.None; } - - if (name.SequenceEqual(PathBytes)) + else if (name.SequenceEqual(PathBytes)) { - headerField = PseudoHeaderFields.Path; + return PseudoHeaderFields.Path; } else if (name.SequenceEqual(MethodBytes)) { - headerField = PseudoHeaderFields.Method; + return PseudoHeaderFields.Method; } else if (name.SequenceEqual(SchemeBytes)) { - headerField = PseudoHeaderFields.Scheme; + return PseudoHeaderFields.Scheme; } else if (name.SequenceEqual(StatusBytes)) { - headerField = PseudoHeaderFields.Status; + return PseudoHeaderFields.Status; } else if (name.SequenceEqual(AuthorityBytes)) { - headerField = PseudoHeaderFields.Authority; + return PseudoHeaderFields.Authority; } else { - headerField = PseudoHeaderFields.Unknown; + return PseudoHeaderFields.Unknown; } - - return true; } private static bool IsConnectionSpecificHeaderField(ReadOnlySpan name, ReadOnlySpan value) @@ -1468,16 +1535,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - public void OnStaticIndexedHeader(int index) - { - throw new NotImplementedException(); - } - - public void OnStaticIndexedHeader(int index, ReadOnlySpan value) - { - throw new NotImplementedException(); - } - private class StreamCloseAwaitable : ICriticalNotifyCompletion { private static readonly Action _callbackCompleted = () => { }; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index b72a01a5e6..613683ab98 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs @@ -6,6 +6,7 @@ using System.Buffers; using System.Diagnostics; using System.IO; using System.IO.Pipelines; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -618,5 +619,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 EndStreamReceived = 2, Aborted = 4, } + + public override void OnHeader(int index, ReadOnlySpan name, ReadOnlySpan value) + { + base.OnHeader(index, name, value); + + // HPack append will return false if the index is not a known request header. + // For example, someone could send the index of "Server" (a response header) in the request. + // If that happens then fallback to using Append with the name bytes. + if (!HttpRequestHeaders.TryHPackAppend(index, value)) + { + AppendHeader(name, value); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void AppendHeader(ReadOnlySpan name, ReadOnlySpan value) + { + HttpRequestHeaders.Append(name, value); + } } } 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 5694b9d2b7..7e661ef59c 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. diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs index 3886a38b09..4f7b36feef 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmarkBase.cs @@ -50,10 +50,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance _connectionPair = DuplexPipe.CreateConnectionPair(options, options); _httpRequestHeaders = new HttpRequestHeaders(); - _httpRequestHeaders.Append(HeaderNames.Method, new StringValues("GET")); - _httpRequestHeaders.Append(HeaderNames.Path, new StringValues("/")); - _httpRequestHeaders.Append(HeaderNames.Scheme, new StringValues("http")); - _httpRequestHeaders.Append(HeaderNames.Authority, new StringValues("localhost:80")); + _httpRequestHeaders.HeaderMethod = new StringValues("GET"); + _httpRequestHeaders.HeaderPath = new StringValues("/"); + _httpRequestHeaders.HeaderScheme = new StringValues("http"); + _httpRequestHeaders.HeaderAuthority = new StringValues("localhost:80"); _headersBuffer = new byte[1024 * 16]; _hpackEncoder = new HPackEncoder(); diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index 06e7440dad..9e21aba6a8 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Net.Http.HPack; using System.Text; using Microsoft.Net.Http.Headers; @@ -219,6 +220,74 @@ namespace CodeGenerator break;")} }}"; + static string AppendHPackSwitch(IEnumerable values) => + $@"switch (index) + {{{Each(values, header => $@"{Each(header.HPackStaticTableIndexes, index => $@" + case {index}:")} + {AppendHPackSwitchSection(header)}")} + }}"; + + static string AppendValue(bool returnTrue = false) => + $@"// Matched a known header + if ((_previousBits & flag) != 0) + {{ + // Had a previous string for this header, mark it as used so we don't clear it OnHeadersComplete or consider it if we get a second header + _previousBits ^= flag; + + // We will only reuse this header if there was only one previous header + if (values.Count == 1) + {{ + var previousValue = values.ToString(); + // Check lengths are the same, then if the bytes were converted to an ascii string if they would be the same. + // We do not consider Utf8 headers for reuse. + if (previousValue.Length == value.Length && + StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, value)) + {{ + // The previous string matches what the bytes would convert to, so we will just use that one. + _bits |= flag; + return{(returnTrue ? " true" : "")}; + }} + }} + }} + + // We didn't have a previous matching header value, or have already added a header, so get the string for this value. + var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector); + if ((_bits & flag) == 0) + {{ + // We didn't already have a header set, so add a new one. + _bits |= flag; + values = new StringValues(valueStr); + }} + else + {{ + // We already had a header set, so concatenate the new one. + values = AppendValue(values, valueStr); + }}"; + + static string AppendHPackSwitchSection(HPackGroup group) + { + var header = group.Header; + if (header.Identifier == "ContentLength") + { + return $@"if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector)) + {{ + AppendContentLength(value); + }} + else + {{ + AppendContentLengthCustomEncoding(value, EncodingSelector(HeaderNames.ContentLength)); + }} + return true;"; + } + else + { + return $@"flag = {header.FlagBit()}; + values = ref _headers._{header.Identifier}; + nameStr = HeaderNames.{header.Identifier}; + break;"; + } + } + static string AppendSwitchSection(int length, IOrderedEnumerable values) { var useVarForFirstTerm = values.Count() > 1 && values.Select(h => h.FirstNameIgnoreCaseSegment()).Distinct().Count() == 1; @@ -1012,46 +1081,12 @@ $@" private void Clear(long bitsToClear) ref StringValues values = ref Unsafe.AsRef(null); var flag = 0L; - // Does the name matched any ""known"" headers + // Does the name match any ""known"" headers {AppendSwitch(loop.Headers.GroupBy(x => x.Name.Length).OrderBy(x => x.Key))} if (flag != 0) {{ - // Matched a known header - if ((_previousBits & flag) != 0) - {{ - // Had a previous string for this header, mark it as used so we don't clear it OnHeadersComplete or consider it if we get a second header - _previousBits ^= flag; - - // We will only reuse this header if there was only one previous header - if (values.Count == 1) - {{ - var previousValue = values.ToString(); - // Check lengths are the same, then if the bytes were converted to an ascii string if they would be the same. - // We do not consider Utf8 headers for reuse. - if (previousValue.Length == value.Length && - StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, value)) - {{ - // The previous string matches what the bytes would convert to, so we will just use that one. - _bits |= flag; - return; - }} - }} - }} - - // We didn't have a previous matching header value, or have already added a header, so get the string for this value. - var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector); - if ((_bits & flag) == 0) - {{ - // We didn't already have a header set, so add a new one. - _bits |= flag; - values = new StringValues(valueStr); - }} - else - {{ - // We already had a header set, so concatenate the new one. - values = AppendValue(values, valueStr); - }} + {AppendValue()} }} else {{ @@ -1062,6 +1097,27 @@ $@" private void Clear(long bitsToClear) var valueStr = value.GetRequestHeaderString(nameStr, EncodingSelector); AppendUnknownHeaders(nameStr, valueStr); }} + }} + + [MethodImpl(MethodImplOptions.AggressiveOptimization)] + public unsafe bool TryHPackAppend(int index, ReadOnlySpan value) + {{ + ref StringValues values = ref Unsafe.AsRef(null); + var nameStr = string.Empty; + var flag = 0L; + + // Does the HPack static index match any ""known"" headers + {AppendHPackSwitch(GroupHPack(loop.Headers))} + + if (flag != 0) + {{ + {AppendValue(returnTrue: true)} + return true; + }} + else + {{ + return false; + }} }}" : "")} private struct HeaderReferences @@ -1117,5 +1173,33 @@ $@" private void Clear(long bitsToClear) }} ")}}}"; } + + private class HPackGroup + { + public int[] HPackStaticTableIndexes { get; set; } + public KnownHeader Header { get; set; } + public string Name { get; set; } + } + + private static IEnumerable GroupHPack(KnownHeader[] headers) + { + var staticHeaders = new (int Index, HeaderField HeaderField)[H2StaticTable.Count]; + for (var i = 0; i < H2StaticTable.Count; i++) + { + staticHeaders[i] = (i + 1, H2StaticTable.Get(i)); + } + + var groupedHeaders = staticHeaders.GroupBy(h => Encoding.ASCII.GetString(h.HeaderField.Name)).Select(g => + { + return new HPackGroup + { + Name = g.Key, + Header = headers.SingleOrDefault(knownHeader => string.Equals(knownHeader.Name, g.Key, StringComparison.OrdinalIgnoreCase)), + HPackStaticTableIndexes = g.Select(h => h.Index).ToArray() + }; + }).Where(g => g.Header != null).ToList(); + + return groupedHeaders; + } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 03560f70f2..110575221c 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -2095,12 +2095,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public void OnStaticIndexedHeader(int index) { - throw new NotImplementedException(); + ref readonly var entry = ref H2StaticTable.Get(index - 1); + OnHeader(entry.Name, entry.Value); } public void OnStaticIndexedHeader(int index, ReadOnlySpan value) { - throw new NotImplementedException(); + OnHeader(H2StaticTable.Get(index - 1).Name, value); } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index adae3b99c0..7714d1e342 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -6,6 +6,7 @@ using System.Buffers; using System.Buffers.Binary; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.IO; using System.IO.Pipelines; @@ -410,6 +411,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests _decodedHeaders[nameStr] = value.GetRequestHeaderString(nameStr, _serviceContext.ServerOptions.RequestHeaderEncodingSelector); } + public void OnStaticIndexedHeader(int index) + { + Debug.Assert(index <= H2StaticTable.Count); + + ref readonly var entry = ref H2StaticTable.Get(index - 1); + ((IHttpHeadersHandler)this).OnHeader(entry.Name, entry.Value); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + Debug.Assert(index <= H2StaticTable.Count); + + ((IHttpHeadersHandler)this).OnHeader(H2StaticTable.Get(index - 1).Name, value); + } + void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { } protected void CreateConnection() @@ -1271,16 +1287,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests return bufferSize ?? 0; } - public void OnStaticIndexedHeader(int index) - { - throw new NotImplementedException(); - } - - public void OnStaticIndexedHeader(int index, ReadOnlySpan value) - { - throw new NotImplementedException(); - } - internal class Http2FrameWithPayload : Http2Frame { public Http2FrameWithPayload() : base() diff --git a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj index b79bf13aa7..211523c8df 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj +++ b/src/Servers/Kestrel/tools/CodeGenerator/CodeGenerator.csproj @@ -9,6 +9,8 @@ + + diff --git a/src/Servers/Kestrel/tools/CodeGenerator/Program.cs b/src/Servers/Kestrel/tools/CodeGenerator/Program.cs index 48b1fa605f..f009666d87 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/Program.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/Program.cs @@ -75,12 +75,11 @@ namespace CodeGenerator if (!string.Equals(content, existingContent)) { File.WriteAllText(path, content); + Console.WriteLine($"{path} updated."); } - - var existingHttp2Connection = File.Exists(path) ? File.ReadAllText(path) : ""; - if (!string.Equals(content, existingHttp2Connection)) + else { - File.WriteAllText(path, content); + Console.WriteLine($"{path} already up to date."); } } } diff --git a/src/Shared/Http2cat/Http2Utilities.cs b/src/Shared/Http2cat/Http2Utilities.cs index 556249d7b7..07c81d49c2 100644 --- a/src/Shared/Http2cat/Http2Utilities.cs +++ b/src/Shared/Http2cat/Http2Utilities.cs @@ -1022,12 +1022,13 @@ namespace Microsoft.AspNetCore.Http2Cat public void OnStaticIndexedHeader(int index) { - throw new NotImplementedException(); + ref readonly var entry = ref H2StaticTable.Get(index - 1); + ((IHttpHeadersHandler)this).OnHeader(entry.Name, entry.Value); } public void OnStaticIndexedHeader(int index, ReadOnlySpan value) { - throw new NotImplementedException(); + ((IHttpHeadersHandler)this).OnHeader(H2StaticTable.Get(index - 1).Name, value); } internal class Http2FrameWithPayload : Http2Frame diff --git a/src/Shared/runtime/Http2/Hpack/HPackDecoder.cs b/src/Shared/runtime/Http2/Hpack/HPackDecoder.cs index bb9b988e89..8899104eb1 100644 --- a/src/Shared/runtime/Http2/Hpack/HPackDecoder.cs +++ b/src/Shared/runtime/Http2/Hpack/HPackDecoder.cs @@ -92,6 +92,7 @@ namespace System.Net.Http.HPack private State _state = State.Ready; private byte[]? _headerName; + private int _headerStaticIndex; private int _stringIndex; private int _stringLength; private int _headerNameLength; @@ -492,23 +493,36 @@ namespace System.Net.Http.HPack private void ProcessHeaderValue(ReadOnlySpan data, IHttpHeadersHandler handler) { - ReadOnlySpan headerNameSpan = _headerNameRange == null - ? new Span(_headerName, 0, _headerNameLength) - : data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length); - ReadOnlySpan headerValueSpan = _headerValueRange == null - ? new Span(_headerValueOctets, 0, _headerValueLength) + ? _headerValueOctets.AsSpan(0, _headerValueLength) : data.Slice(_headerValueRange.GetValueOrDefault().start, _headerValueRange.GetValueOrDefault().length); - handler.OnHeader(headerNameSpan, headerValueSpan); + if (_headerStaticIndex > 0) + { + handler.OnStaticIndexedHeader(_headerStaticIndex, headerValueSpan); + if (_index) + { + _dynamicTable.Insert(H2StaticTable.Get(_headerStaticIndex - 1).Name, headerValueSpan); + } + } + else + { + ReadOnlySpan headerNameSpan = _headerNameRange == null + ? _headerName.AsSpan(0, _headerNameLength) + : data.Slice(_headerNameRange.GetValueOrDefault().start, _headerNameRange.GetValueOrDefault().length); + + handler.OnHeader(headerNameSpan, headerValueSpan); + + if (_index) + { + _dynamicTable.Insert(headerNameSpan, headerValueSpan); + } + } + + _headerStaticIndex = 0; _headerNameRange = null; _headerValueRange = null; - - if (_index) - { - _dynamicTable.Insert(headerNameSpan, headerValueSpan); - } } public void CompleteDecode() @@ -522,15 +536,30 @@ namespace System.Net.Http.HPack private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler) { - ref readonly HeaderField header = ref GetHeader(index); - handler.OnHeader(header.Name, header.Value); + if (index <= H2StaticTable.Count) + { + handler.OnStaticIndexedHeader(index); + } + else + { + ref readonly HeaderField header = ref GetDynamicHeader(index); + handler.OnHeader(header.Name, header.Value); + } + _state = State.Ready; } private void OnIndexedHeaderName(int index) { - _headerName = GetHeader(index).Name; - _headerNameLength = _headerName.Length; + if (index <= H2StaticTable.Count) + { + _headerStaticIndex = index; + } + else + { + _headerName = GetDynamicHeader(index).Name; + _headerNameLength = _headerName.Length; + } _state = State.HeaderValueLength; } @@ -615,13 +644,11 @@ namespace System.Net.Http.HPack return (b & HuffmanMask) != 0; } - private ref readonly HeaderField GetHeader(int index) + private ref readonly HeaderField GetDynamicHeader(int index) { try { - return ref index <= H2StaticTable.Count - ? ref H2StaticTable.Get(index - 1) - : ref _dynamicTable[index - H2StaticTable.Count - 1]; + return ref _dynamicTable[index - H2StaticTable.Count - 1]; } catch (IndexOutOfRangeException) { diff --git a/src/Shared/test/Shared.Tests/runtime/Http2/HPackDecoderTest.cs b/src/Shared/test/Shared.Tests/runtime/Http2/HPackDecoderTest.cs index 40221b8b8e..6c84993d3f 100644 --- a/src/Shared/test/Shared.Tests/runtime/Http2/HPackDecoderTest.cs +++ b/src/Shared/test/Shared.Tests/runtime/Http2/HPackDecoderTest.cs @@ -104,10 +104,27 @@ namespace System.Net.Http.Unit.Tests.HPack } [Fact] - public void DecodesIndexedHeaderField_StaticTable() + public void DecodesIndexedHeaderField_StaticTableWithValue() { _decoder.Decode(_indexedHeaderStatic, endHeaders: true, handler: _handler); Assert.Equal("GET", _handler.DecodedHeaders[":method"]); + + Assert.Equal(":method", _handler.DecodedStaticHeaders[H2StaticTable.MethodGet].Key); + Assert.Equal("GET", _handler.DecodedStaticHeaders[H2StaticTable.MethodGet].Value); + } + + [Fact] + public void DecodesIndexedHeaderField_StaticTableWithoutValue() + { + byte[] encoded = _literalHeaderFieldWithIndexingIndexedName + .Concat(_headerValue) + .ToArray(); + + _decoder.Decode(encoded, endHeaders: true, handler: _handler); + Assert.Equal(_headerValueString, _handler.DecodedHeaders[_userAgentString]); + + Assert.Equal(_userAgentString, _handler.DecodedStaticHeaders[H2StaticTable.UserAgent].Key); + Assert.Equal(_headerValueString, _handler.DecodedStaticHeaders[H2StaticTable.UserAgent].Value); } [Fact] @@ -707,6 +724,7 @@ namespace System.Net.Http.Unit.Tests.HPack public class TestHttpHeadersHandler : IHttpHeadersHandler { public Dictionary DecodedHeaders { get; } = new Dictionary(); + public Dictionary> DecodedStaticHeaders { get; } = new Dictionary>(); void IHttpHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan value) { @@ -718,14 +736,16 @@ namespace System.Net.Http.Unit.Tests.HPack void IHttpHeadersHandler.OnStaticIndexedHeader(int index) { - // Not yet implemented for HPACK. - throw new NotImplementedException(); + ref readonly HeaderField entry = ref H2StaticTable.Get(index - 1); + ((IHttpHeadersHandler)this).OnHeader(entry.Name, entry.Value); + DecodedStaticHeaders[index] = new KeyValuePair(Encoding.ASCII.GetString(entry.Name), Encoding.ASCII.GetString(entry.Value)); } void IHttpHeadersHandler.OnStaticIndexedHeader(int index, ReadOnlySpan value) { - // Not yet implemented for HPACK. - throw new NotImplementedException(); + byte[] name = H2StaticTable.Get(index - 1).Name; + ((IHttpHeadersHandler)this).OnHeader(name, value); + DecodedStaticHeaders[index] = new KeyValuePair(Encoding.ASCII.GetString(name), Encoding.ASCII.GetString(value)); } void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }