diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 5ec3b164b6..0266660792 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -34,6 +34,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive"); private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked"); private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName); + internal const string SchemeHttp = "http"; + internal const string SchemeHttps = "https"; protected BodyControl _bodyControl; private Stack, object>> _onStarting; @@ -385,7 +387,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http if (_scheme == null) { var tlsFeature = ConnectionFeatures?[typeof(ITlsConnectionFeature)]; - _scheme = tlsFeature != null ? "https" : "http"; + _scheme = tlsFeature != null ? SchemeHttps : SchemeHttp; } Scheme = _scheme; @@ -518,7 +520,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http HttpRequestHeaders.Append(name, value); } - public virtual void OnHeader(int index, ReadOnlySpan name, ReadOnlySpan value) + public virtual void OnHeader(int index, bool indexOnly, ReadOnlySpan name, ReadOnlySpan value) { IncrementRequestHeadersCount(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs index c914595bc9..bcb67975e5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/HPackHeaderWriter.cs @@ -151,13 +151,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 private static bool IsSensitive(int staticTableIndex, string name) { // Set-Cookie could contain sensitive data. - if (staticTableIndex == H2StaticTable.SetCookie) + switch (staticTableIndex) { - return true; - } - if (string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase)) - { - return true; + case H2StaticTable.SetCookie: + case H2StaticTable.ContentDisposition: + return true; + case -1: + // Content-Disposition currently isn't a known header so a + // static index probably won't be specified. + return string.Equals(name, "Content-Disposition", StringComparison.OrdinalIgnoreCase); } return false; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 3040c54107..b63f4a4e7c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -1226,12 +1226,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 } } - // We can't throw a Http2StreamErrorException here, it interrupts the header decompression state and may corrupt subsequent header frames on other streams. - // For now these either need to be connection errors or BadRequests. If we want to downgrade any of them to stream errors later then we need to - // rework the flow so that the remaining headers are drained and the decompression state is maintained. public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) { - OnHeaderCore(index: null, name, value); + OnHeaderCore(index: null, indexedValue: false, name, value); } public void OnStaticIndexedHeader(int index) @@ -1239,20 +1236,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 Debug.Assert(index <= H2StaticTable.Count); ref readonly var entry = ref H2StaticTable.Get(index - 1); - OnHeaderCore(index, entry.Name, entry.Value); + OnHeaderCore(index, indexedValue: true, entry.Name, entry.Value); } public void OnStaticIndexedHeader(int index, ReadOnlySpan value) { Debug.Assert(index <= H2StaticTable.Count); - OnHeaderCore(index, H2StaticTable.Get(index - 1).Name, value); + OnHeaderCore(index, indexedValue: false, 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) + private void OnHeaderCore(int? index, bool indexedValue, 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."; @@ -1283,7 +1280,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // Throws InvalidOperation for bad encoding. if (index != null) { - _currentHeadersStream.OnHeader(index.Value, name, value); + _currentHeadersStream.OnHeader(index.Value, indexedValue, name, value); } else { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs index 613683ab98..c3a90279f6 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.Net.Http.HPack; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -15,6 +16,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +using HttpMethods = Microsoft.AspNetCore.Http.HttpMethods; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 { @@ -205,7 +207,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 _httpVersion = Http.HttpVersion.Http2; - if (!TryValidateMethod()) + // Method could already have been set from :method static table index + if (Method == HttpMethod.None && !TryValidateMethod()) { return false; } @@ -237,7 +240,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 // - That said, we shouldn't allow arbitrary values or use them to populate Request.Scheme, right? // - For now we'll restrict it to http/s and require it match the transport. // - We'll need to find some concrete scenarios to warrant unblocking this. - if (!string.Equals(HttpRequestHeaders.HeaderScheme, Scheme, StringComparison.OrdinalIgnoreCase)) + var headerScheme = HttpRequestHeaders.HeaderScheme.ToString(); + if (!ReferenceEquals(headerScheme, Scheme) && + !string.Equals(headerScheme, Scheme, StringComparison.OrdinalIgnoreCase)) { ResetAndAbort(new ConnectionAbortedException( CoreStrings.FormatHttp2StreamErrorSchemeMismatch(HttpRequestHeaders.HeaderScheme, Scheme)), Http2ErrorCode.PROTOCOL_ERROR); @@ -620,9 +625,33 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 Aborted = 4, } - public override void OnHeader(int index, ReadOnlySpan name, ReadOnlySpan value) + public override void OnHeader(int index, bool indexedValue, ReadOnlySpan name, ReadOnlySpan value) { - base.OnHeader(index, name, value); + base.OnHeader(index, indexedValue, name, value); + + if (indexedValue) + { + // Special case setting headers when the value is indexed for performance. + switch (index) + { + case H2StaticTable.MethodGet: + HttpRequestHeaders.HeaderMethod = HttpMethods.Get; + Method = HttpMethod.Get; + _methodText = HttpMethods.Get; + return; + case H2StaticTable.MethodPost: + HttpRequestHeaders.HeaderMethod = HttpMethods.Post; + Method = HttpMethod.Post; + _methodText = HttpMethods.Post; + return; + case H2StaticTable.SchemeHttp: + HttpRequestHeaders.HeaderScheme = SchemeHttp; + return; + case H2StaticTable.SchemeHttps: + HttpRequestHeaders.HeaderScheme = SchemeHttps; + return; + } + } // 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. diff --git a/src/Servers/Kestrel/Core/src/ListenOptions.cs b/src/Servers/Kestrel/Core/src/ListenOptions.cs index bc90bb2fbd..fb049d102a 100644 --- a/src/Servers/Kestrel/Core/src/ListenOptions.cs +++ b/src/Servers/Kestrel/Core/src/ListenOptions.cs @@ -8,6 +8,7 @@ using System.Net.Sockets; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core { @@ -84,7 +85,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core { get { - return IsTls ? "https" : "http"; + return IsTls ? HttpProtocol.SchemeHttps : HttpProtocol.SchemeHttp; } } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/HPackHeaderWriterBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/HPackHeaderWriterBenchmark.cs new file mode 100644 index 0000000000..fcca5b58aa --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/HPackHeaderWriterBenchmark.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net.Http.HPack; +using System.Text; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class HPackHeaderWriterBenchmark + { + private Http2HeadersEnumerator _http2HeadersEnumerator; + private HPackEncoder _hpackEncoder; + private HttpResponseHeaders _knownResponseHeaders; + private HttpResponseHeaders _unknownResponseHeaders; + private byte[] _buffer; + + [GlobalSetup] + public void GlobalSetup() + { + _http2HeadersEnumerator = new Http2HeadersEnumerator(); + _hpackEncoder = new HPackEncoder(); + _buffer = new byte[1024 * 1024]; + + _knownResponseHeaders = new HttpResponseHeaders + { + HeaderServer = "Kestrel", + HeaderContentType = "application/json", + HeaderDate = "Date!", + HeaderContentLength = "0", + HeaderAcceptRanges = "Ranges!", + HeaderTransferEncoding = "Encoding!", + HeaderVia = "Via!", + HeaderVary = "Vary!", + HeaderWWWAuthenticate = "Authenticate!", + HeaderLastModified = "Modified!", + HeaderExpires = "Expires!", + HeaderAge = "Age!" + }; + + _unknownResponseHeaders = new HttpResponseHeaders(); + for (var i = 0; i < 10; i++) + { + _unknownResponseHeaders.Append("Unknown" + i, "Value" + i); + } + } + + [Benchmark] + public void BeginEncodeHeaders_KnownHeaders() + { + _http2HeadersEnumerator.Initialize(_knownResponseHeaders); + HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _); + } + + [Benchmark] + public void BeginEncodeHeaders_UnknownHeaders() + { + _http2HeadersEnumerator.Initialize(_unknownResponseHeaders); + HPackHeaderWriter.BeginEncodeHeaders(_hpackEncoder, _http2HeadersEnumerator, _buffer, out _); + } + } +}