From 06247f6ff45e15b1b0aface676ab7b94394cd439 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Sat, 27 Mar 2021 12:57:08 +0000 Subject: [PATCH 1/6] Use interned headernames for known headers not in the preallocated block --- src/Http/Headers/src/HeaderNames.cs | 15 +- src/Http/Headers/src/PublicAPI.Unshipped.txt | 3 + .../Internal/Http/HttpHeaders.Generated.cs | 100 ++++++++ .../Core/src/Internal/Http/HttpHeaders.cs | 16 +- .../src/Internal/Http/HttpRequestHeaders.cs | 5 +- .../src/Internal/Http/HttpResponseHeaders.cs | 4 +- .../src/Internal/Http/HttpResponseTrailers.cs | 4 +- src/Servers/Kestrel/shared/KnownHeaders.cs | 233 ++++++++++-------- 8 files changed, 261 insertions(+), 119 deletions(-) diff --git a/src/Http/Headers/src/HeaderNames.cs b/src/Http/Headers/src/HeaderNames.cs index 1355909d6d85..1347cffebe18 100644 --- a/src/Http/Headers/src/HeaderNames.cs +++ b/src/Http/Headers/src/HeaderNames.cs @@ -65,7 +65,7 @@ public static class HeaderNames public static readonly string Authorization = "Authorization"; /// Gets the baggage HTTP header name. - public static readonly string Baggage = "baggage"; + public static readonly string Baggage = "Baggage"; /// Gets the Cache-Control HTTP header name. public static readonly string CacheControl = "Cache-Control"; @@ -166,6 +166,9 @@ public static class HeaderNames /// Gets the Last-Modified HTTP header name. public static readonly string LastModified = "Last-Modified"; + /// Gets the Link HTTP header name. + public static readonly string Link = "Link"; + /// Gets the Location HTTP header name. public static readonly string Location = "Location"; @@ -245,10 +248,10 @@ public static class HeaderNames public static readonly string Translate = "Translate"; /// Gets the traceparent HTTP header name. - public static readonly string TraceParent = "traceparent"; + public static readonly string TraceParent = "TraceParent"; /// Gets the tracestate HTTP header name. - public static readonly string TraceState = "tracestate"; + public static readonly string TraceState = "TraceState"; /// Gets the Upgrade HTTP header name. public static readonly string Upgrade = "Upgrade"; @@ -274,10 +277,16 @@ public static class HeaderNames /// Gets the WWW-Authenticate HTTP header name. public static readonly string WWWAuthenticate = "WWW-Authenticate"; + /// Gets the X-Content-Type-Options HTTP header name. + public static readonly string XContentTypeOptions = "X-Content-Type-Options"; + /// Gets the X-Frame-Options HTTP header name. public static readonly string XFrameOptions = "X-Frame-Options"; /// Gets the X-Requested-With HTTP header name. public static readonly string XRequestedWith = "X-Requested-With"; + + /// Gets the X-XSS-Protection HTTP header name. + public static readonly string XXssProtection = "X-XSS-Protection"; } } diff --git a/src/Http/Headers/src/PublicAPI.Unshipped.txt b/src/Http/Headers/src/PublicAPI.Unshipped.txt index be7e9574bc0a..d7014272fd7c 100644 --- a/src/Http/Headers/src/PublicAPI.Unshipped.txt +++ b/src/Http/Headers/src/PublicAPI.Unshipped.txt @@ -3,4 +3,7 @@ Microsoft.Net.Http.Headers.MediaTypeHeaderValue.MatchesMediaType(Microsoft.Extensions.Primitives.StringSegment otherMediaType) -> bool Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> void static readonly Microsoft.Net.Http.Headers.HeaderNames.Baggage -> string! +static readonly Microsoft.Net.Http.Headers.HeaderNames.Link -> string! static readonly Microsoft.Net.Http.Headers.HeaderNames.ProxyConnection -> string! +static readonly Microsoft.Net.Http.Headers.HeaderNames.XContentTypeOptions -> string! +static readonly Microsoft.Net.Http.Headers.HeaderNames.XXssProtection -> string! 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 d8ad519873de..e0f89a3b09a8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs @@ -99,6 +99,106 @@ internal enum KnownHeaderType WWWAuthenticate, } + internal partial class HttpHeaders + { + protected readonly static HashSet s_internedHeaderNames = new HashSet(93, StringComparer.OrdinalIgnoreCase) + { + HeaderNames.Accept, + HeaderNames.AcceptCharset, + HeaderNames.AcceptEncoding, + HeaderNames.AcceptLanguage, + HeaderNames.AcceptRanges, + HeaderNames.AccessControlAllowCredentials, + HeaderNames.AccessControlAllowHeaders, + HeaderNames.AccessControlAllowMethods, + HeaderNames.AccessControlAllowOrigin, + HeaderNames.AccessControlExposeHeaders, + HeaderNames.AccessControlMaxAge, + HeaderNames.AccessControlRequestHeaders, + HeaderNames.AccessControlRequestMethod, + HeaderNames.Age, + HeaderNames.Allow, + HeaderNames.AltSvc, + HeaderNames.Authority, + HeaderNames.Authorization, + HeaderNames.Baggage, + HeaderNames.CacheControl, + HeaderNames.Connection, + HeaderNames.ContentDisposition, + HeaderNames.ContentEncoding, + HeaderNames.ContentLanguage, + HeaderNames.ContentLength, + HeaderNames.ContentLocation, + HeaderNames.ContentMD5, + HeaderNames.ContentRange, + HeaderNames.ContentSecurityPolicy, + HeaderNames.ContentSecurityPolicyReportOnly, + HeaderNames.ContentType, + HeaderNames.CorrelationContext, + HeaderNames.Cookie, + HeaderNames.Date, + HeaderNames.DNT, + HeaderNames.ETag, + HeaderNames.Expires, + HeaderNames.Expect, + HeaderNames.From, + HeaderNames.GrpcAcceptEncoding, + HeaderNames.GrpcEncoding, + HeaderNames.GrpcMessage, + HeaderNames.GrpcStatus, + HeaderNames.GrpcTimeout, + HeaderNames.Host, + HeaderNames.KeepAlive, + HeaderNames.IfMatch, + HeaderNames.IfModifiedSince, + HeaderNames.IfNoneMatch, + HeaderNames.IfRange, + HeaderNames.IfUnmodifiedSince, + HeaderNames.LastModified, + HeaderNames.Link, + HeaderNames.Location, + HeaderNames.MaxForwards, + HeaderNames.Method, + HeaderNames.Origin, + HeaderNames.Path, + HeaderNames.Pragma, + HeaderNames.ProxyAuthenticate, + HeaderNames.ProxyAuthorization, + HeaderNames.ProxyConnection, + HeaderNames.Range, + HeaderNames.Referer, + HeaderNames.RetryAfter, + HeaderNames.RequestId, + HeaderNames.Scheme, + HeaderNames.SecWebSocketAccept, + HeaderNames.SecWebSocketKey, + HeaderNames.SecWebSocketProtocol, + HeaderNames.SecWebSocketVersion, + HeaderNames.Server, + HeaderNames.SetCookie, + HeaderNames.Status, + HeaderNames.StrictTransportSecurity, + HeaderNames.TE, + HeaderNames.Trailer, + HeaderNames.TransferEncoding, + HeaderNames.Translate, + HeaderNames.TraceParent, + HeaderNames.TraceState, + HeaderNames.Upgrade, + HeaderNames.UpgradeInsecureRequests, + HeaderNames.UserAgent, + HeaderNames.Vary, + HeaderNames.Via, + HeaderNames.Warning, + HeaderNames.WebSocketSubProtocols, + HeaderNames.WWWAuthenticate, + HeaderNames.XContentTypeOptions, + HeaderNames.XFrameOptions, + HeaderNames.XRequestedWith, + HeaderNames.XXssProtection, + }; + } + internal partial class HttpRequestHeaders { private HeaderReferences _headers; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs index e7912791bd00..8bbb7c6e696a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs @@ -15,13 +15,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { - internal abstract class HttpHeaders : IHeaderDictionary + internal abstract partial class HttpHeaders : IHeaderDictionary { protected long _bits = 0; protected long? _contentLength; protected bool _isReadOnly; protected Dictionary? MaybeUnknown; - protected Dictionary Unknown => MaybeUnknown ?? (MaybeUnknown = new Dictionary(StringComparer.OrdinalIgnoreCase)); + protected Dictionary Unknown => MaybeUnknown ??= new Dictionary(StringComparer.OrdinalIgnoreCase); public long? ContentLength { @@ -126,6 +126,18 @@ public void Reset() ClearFast(); } + protected static string GetInternedHeaderName(string name) + { + // Some headers can be very long lived; for example those on a WebSocket connection + // so we exchange these for the preallocated strings predefined in HeaderNames + if (s_internedHeaderNames.TryGetValue(name, out var internedName)) + { + return internedName; + } + + return name; + } + [MethodImpl(MethodImplOptions.NoInlining)] protected static StringValues AppendValue(StringValues existing, string append) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs index 05e017cbfea1..b8833b3051bc 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestHeaders.cs @@ -121,13 +121,13 @@ private void AppendContentLengthCustomEncoding(ReadOnlySpan value, Encodin [MethodImpl(MethodImplOptions.NoInlining)] private void SetValueUnknown(string key, StringValues value) { - Unknown[key] = value; + Unknown[GetInternedHeaderName(key)] = value; } [MethodImpl(MethodImplOptions.NoInlining)] private bool AddValueUnknown(string key, StringValues value) { - Unknown.Add(key, value); + Unknown.Add(GetInternedHeaderName(key), value); // Return true, above will throw and exit for false return true; } @@ -135,6 +135,7 @@ private bool AddValueUnknown(string key, StringValues value) [MethodImpl(MethodImplOptions.NoInlining)] private unsafe void AppendUnknownHeaders(string name, string valueString) { + name = GetInternedHeaderName(name); Unknown.TryGetValue(name, out var existing); Unknown[name] = AppendValue(existing, valueString); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs index efc4dc6bc8bc..bdefaf8e9861 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseHeaders.cs @@ -76,14 +76,14 @@ private static void ThrowInvalidContentLengthException(string value) private void SetValueUnknown(string key, StringValues value) { ValidateHeaderNameCharacters(key); - Unknown[key] = value; + Unknown[GetInternedHeaderName(key)] = value; } [MethodImpl(MethodImplOptions.NoInlining)] private bool AddValueUnknown(string key, StringValues value) { ValidateHeaderNameCharacters(key); - Unknown.Add(key, value); + Unknown.Add(GetInternedHeaderName(key), value); // Return true, above will throw and exit for false return true; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs index 49948877d6cb..0585c6eee318 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs @@ -25,14 +25,14 @@ protected override IEnumerator> GetEnumerator private void SetValueUnknown(string key, StringValues value) { ValidateHeaderNameCharacters(key); - Unknown[key] = value; + Unknown[GetInternedHeaderName(key)] = value; } [MethodImpl(MethodImplOptions.NoInlining)] private bool AddValueUnknown(string key, StringValues value) { ValidateHeaderNameCharacters(key); - Unknown.Add(key, value); + Unknown.Add(GetInternedHeaderName(key), value); // Return true, above will throw and exit for false return true; } diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index e6541998aa29..4d3984ec02e9 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -7,8 +7,11 @@ using System.Globalization; using System.Linq; using System.Net.Http.HPack; +using System.Reflection; using System.Text; +using Microsoft.Net.Http.Headers; + namespace CodeGenerator { public class KnownHeaders @@ -22,94 +25,94 @@ static KnownHeaders() { var requestPrimaryHeaders = new[] { - "Accept", - "Connection", - "Host", - "User-Agent" + HeaderNames.Accept, + HeaderNames.Connection, + HeaderNames.Host, + HeaderNames.UserAgent }; var responsePrimaryHeaders = new[] { - "Connection", - "Date", - "Content-Type", - "Server", - "Content-Length", + HeaderNames.Connection, + HeaderNames.Date, + HeaderNames.ContentType, + HeaderNames.Server, + HeaderNames.ContentLength, }; var commonHeaders = new[] { - "Cache-Control", - "Connection", - "Date", - "Grpc-Encoding", - "Keep-Alive", - "Pragma", - "Trailer", - "Transfer-Encoding", - "Upgrade", - "Via", - "Warning", - "Allow", - "Content-Type", - "Content-Encoding", - "Content-Language", - "Content-Location", - "Content-MD5", - "Content-Range", - "Expires", - "Last-Modified" + HeaderNames.CacheControl, + HeaderNames.Connection, + HeaderNames.Date, + HeaderNames.GrpcEncoding, + HeaderNames.KeepAlive, + HeaderNames.Pragma, + HeaderNames.Trailer, + HeaderNames.TransferEncoding, + HeaderNames.Upgrade, + HeaderNames.Via, + HeaderNames.Warning, + HeaderNames.Allow, + HeaderNames.ContentType, + HeaderNames.ContentEncoding, + HeaderNames.ContentLanguage, + HeaderNames.ContentLocation, + HeaderNames.ContentMD5, + HeaderNames.ContentRange, + HeaderNames.Expires, + HeaderNames.LastModified }; // http://www.w3.org/TR/cors/#syntax var corsRequestHeaders = new[] { - "Origin", - "Access-Control-Request-Method", - "Access-Control-Request-Headers", + HeaderNames.Origin, + HeaderNames.AccessControlRequestMethod, + HeaderNames.AccessControlRequestHeaders, }; var requestHeadersExistence = new[] { - "Connection", - "Transfer-Encoding", + HeaderNames.Connection, + HeaderNames.TransferEncoding, }; var requestHeadersCount = new[] { - "Host" + HeaderNames.Host }; RequestHeaders = commonHeaders.Concat(new[] { - ":authority", - ":method", - ":path", - ":scheme", - "Accept", - "Accept-Charset", - "Accept-Encoding", - "Accept-Language", - "Authorization", - "Cookie", - "Expect", - "From", - "Grpc-Accept-Encoding", - "Grpc-Timeout", - "Host", - "If-Match", - "If-Modified-Since", - "If-None-Match", - "If-Range", - "If-Unmodified-Since", - "Max-Forwards", - "Proxy-Authorization", - "Referer", - "Range", - "TE", - "Translate", - "User-Agent", - "DNT", - "Upgrade-Insecure-Requests", - "Request-Id", - "Correlation-Context", - "TraceParent", - "TraceState", - "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.DNT, + HeaderNames.UpgradeInsecureRequests, + HeaderNames.RequestId, + HeaderNames.CorrelationContext, + HeaderNames.TraceParent, + HeaderNames.TraceState, + HeaderNames.Baggage }) .Concat(corsRequestHeaders) .Select((header, index) => new KnownHeader @@ -122,50 +125,50 @@ static KnownHeaders() }) .Concat(new[] { new KnownHeader { - Name = "Content-Length", + Name = HeaderNames.ContentLength, Index = -1, - PrimaryHeader = requestPrimaryHeaders.Contains("Content-Length") + PrimaryHeader = requestPrimaryHeaders.Contains(HeaderNames.ContentLength) }}) .ToArray(); var responseHeadersExistence = new[] { - "Connection", - "Server", - "Date", - "Transfer-Encoding" + HeaderNames.Connection, + HeaderNames.Server, + HeaderNames.Date, + HeaderNames.TransferEncoding }; var enhancedHeaders = new[] { - "Connection", - "Server", - "Date", - "Transfer-Encoding" + HeaderNames.Connection, + HeaderNames.Server, + HeaderNames.Date, + HeaderNames.TransferEncoding }; // http://www.w3.org/TR/cors/#syntax var corsResponseHeaders = new[] { - "Access-Control-Allow-Credentials", - "Access-Control-Allow-Headers", - "Access-Control-Allow-Methods", - "Access-Control-Allow-Origin", - "Access-Control-Expose-Headers", - "Access-Control-Max-Age", + HeaderNames.AccessControlAllowCredentials, + HeaderNames.AccessControlAllowHeaders, + HeaderNames.AccessControlAllowMethods, + HeaderNames.AccessControlAllowOrigin, + HeaderNames.AccessControlExposeHeaders, + HeaderNames.AccessControlMaxAge, }; ResponseHeaders = commonHeaders.Concat(new[] { - "Accept-Ranges", - "Age", - "Alt-Svc", - "ETag", - "Location", - "Proxy-Authenticate", - "Proxy-Connection", - "Retry-After", - "Server", - "Set-Cookie", - "Vary", - "WWW-Authenticate", + HeaderNames.AcceptRanges, + HeaderNames.Age, + HeaderNames.AltSvc, + HeaderNames.ETag, + HeaderNames.Location, + HeaderNames.ProxyAuthenticate, + HeaderNames.ProxyConnection, + HeaderNames.RetryAfter, + HeaderNames.Server, + HeaderNames.SetCookie, + HeaderNames.Vary, + HeaderNames.WWWAuthenticate, }) .Concat(corsResponseHeaders) .Select((header, index) => new KnownHeader @@ -178,18 +181,18 @@ static KnownHeaders() }) .Concat(new[] { new KnownHeader { - Name = "Content-Length", + Name = HeaderNames.ContentLength, Index = 63, - EnhancedSetter = enhancedHeaders.Contains("Content-Length"), - PrimaryHeader = responsePrimaryHeaders.Contains("Content-Length") + EnhancedSetter = enhancedHeaders.Contains(HeaderNames.ContentLength), + PrimaryHeader = responsePrimaryHeaders.Contains(HeaderNames.ContentLength) }}) .ToArray(); ResponseTrailers = new[] { - "ETag", - "Grpc-Message", - "Grpc-Status" + HeaderNames.ETag, + HeaderNames.GrpcMessage, + HeaderNames.GrpcStatus } .Select((header, index) => new KnownHeader { @@ -203,11 +206,11 @@ static KnownHeaders() var invalidH2H3ResponseHeaders = new[] { - "Connection", - "Transfer-Encoding", - "Keep-Alive", - "Upgrade", - "Proxy-Connection" + HeaderNames.Connection, + HeaderNames.TransferEncoding, + HeaderNames.KeepAlive, + HeaderNames.Upgrade, + HeaderNames.ProxyConnection }; InvalidH2H3ResponseHeadersBits = ResponseHeaders @@ -732,6 +735,11 @@ internal enum KnownHeaderType Unknown,{Each(allHeaderNames, n => @" " + n + ",")} }} + + internal partial class HttpHeaders + {{ + {GetHeaderLookup()} + }} {Each(loops, loop => $@" internal partial class {loop.ClassName} {{{(loop.Bytes != null ? @@ -1200,6 +1208,15 @@ public bool MoveNext() ")}}}"; } + private static string GetHeaderLookup() + { + var headerNameFields = typeof(HeaderNames).GetFields(BindingFlags.Static | BindingFlags.Public); + return @$"protected readonly static HashSet s_internedHeaderNames = new HashSet({headerNameFields.Length}, StringComparer.OrdinalIgnoreCase) + {{{Each(headerNameFields, (f) => @" + HeaderNames." + f.Name + ",")} + }};"; + } + private static IEnumerable GroupHPack(KnownHeader[] headers) { var staticHeaders = new (int Index, HeaderField HeaderField)[H2StaticTable.Count]; From 3e8943e3a7d8980277240d4c1832a50888cda367 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Sat, 27 Mar 2021 13:45:30 +0000 Subject: [PATCH 2/6] Switch Connection header values for interned values --- .../src/Internal/Http/Http1MessageBody.cs | 2 +- .../Internal/Http/HttpHeaders.Generated.cs | 4 +- .../Core/src/Internal/Http/HttpHeaders.cs | 45 ++++++++++++++++++- .../Http/HttpProtocol.FeatureCollection.cs | 4 +- .../Core/src/Internal/Http/HttpProtocol.cs | 2 +- .../src/Internal/Http/HttpResponseTrailers.cs | 2 + .../Kestrel/Core/test/HttpHeadersTests.cs | 8 +++- src/Servers/Kestrel/shared/KnownHeaders.cs | 24 +++++----- 8 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index 4e2a87ec10c0..5bee9a25b90a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -128,7 +128,7 @@ public static MessageBody For( if (headers.HasConnection) { - var connectionOptions = HttpHeaders.ParseConnection(headers.HeaderConnection); + var connectionOptions = HttpHeaders.ParseConnection(headers); upgrade = (connectionOptions & ConnectionOptions.Upgrade) != 0; keepAlive = keepAlive || (connectionOptions & ConnectionOptions.KeepAlive) != 0; 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 e0f89a3b09a8..5d3fd31122e7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs @@ -225,7 +225,7 @@ public StringValues HeaderCacheControl _headers._CacheControl = value; } } - public StringValues HeaderConnection + public override StringValues HeaderConnection { get { @@ -8229,7 +8229,7 @@ public StringValues HeaderCacheControl _headers._CacheControl = value; } } - public StringValues HeaderConnection + public override StringValues HeaderConnection { get { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs index 8bbb7c6e696a..9b8760385640 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { @@ -40,6 +41,8 @@ public long? ContentLength } } + public abstract StringValues HeaderConnection { get; set; } + StringValues IHeaderDictionary.this[string key] { get @@ -288,7 +291,12 @@ public static void ValidateHeaderNameCharacters(string headerCharacters) } } - public static ConnectionOptions ParseConnection(StringValues connection) + private readonly static string KeepAlive = "keep-alive"; + private readonly static StringValues ConnectionValueKeepAlive = KeepAlive; + private readonly static StringValues ConnectionValueClose = "close"; + private readonly static StringValues ConnectionValueUpgrade = HeaderNames.Upgrade; + + public static ConnectionOptions ParseConnection(HttpHeaders headers) { // Keep-alive const ulong lowerCaseKeep = 0x0000_0020_0020_0020; // Don't lowercase hyphen @@ -301,9 +309,27 @@ public static ConnectionOptions ParseConnection(StringValues connection) // Close const ulong closeEnd = 0x0065_0073_006f_006c; // 4 chars "lose" + var connection = headers.HeaderConnection; + var connectionCount = connection.Count; + if (connectionCount == 0) + { + return ConnectionOptions.None; + } + var connectionOptions = ConnectionOptions.None; - var connectionCount = connection.Count; + if (connectionCount == 1) + { + // "keep-alive" is the only value that will be repeated over + // many requests on the same connection; on the first request + // we will have switched it for the readonly static value; + // so we can ptentially short-circuit parsing and use ReferenceEquals. + if (ReferenceEquals(connection.ToString(), KeepAlive)) + { + return ConnectionOptions.KeepAlive; + } + } + for (var i = 0; i < connectionCount; i++) { var value = connection[i].AsSpan(); @@ -432,6 +458,21 @@ public static ConnectionOptions ParseConnection(StringValues connection) } } + // If Connection is a single value, switch it for the interned value + // in case the connection is long lived + if (connectionOptions == ConnectionOptions.Upgrade) + { + headers.HeaderConnection = ConnectionValueUpgrade; + } + else if (connectionOptions == ConnectionOptions.KeepAlive) + { + headers.HeaderConnection = ConnectionValueKeepAlive; + } + else if (connectionOptions == ConnectionOptions.Close) + { + headers.HeaderConnection = ConnectionValueClose; + } + return connectionOptions; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs index 71e9ed2047a6..9c10f3f4afb5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs @@ -2,8 +2,6 @@ // 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.Diagnostics; using System.IO; using System.IO.Pipelines; using System.Net; @@ -309,7 +307,7 @@ async Task IHttpUpgradeFeature.UpgradeAsync() StatusCode = StatusCodes.Status101SwitchingProtocols; ReasonPhrase = "Switching Protocols"; - ResponseHeaders[HeaderNames.Connection] = "Upgrade"; + ResponseHeaders[HeaderNames.Connection] = HeaderNames.Upgrade; await FlushAsync(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index bf856a53c4c8..2f575865930b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -1122,7 +1122,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted) if (_keepAlive && hasConnection && - (HttpHeaders.ParseConnection(responseHeaders.HeaderConnection) & ConnectionOptions.KeepAlive) == 0) + (HttpHeaders.ParseConnection(responseHeaders) & ConnectionOptions.KeepAlive) == 0) { _keepAlive = false; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs index 0585c6eee318..3fb3cef5e385 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs @@ -37,6 +37,8 @@ private bool AddValueUnknown(string key, StringValues value) return true; } + public override StringValues HeaderConnection { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public partial struct Enumerator : IEnumerator> { private readonly HttpResponseTrailers _collection; diff --git a/src/Servers/Kestrel/Core/test/HttpHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpHeadersTests.cs index 092e2d47b75a..c13062318439 100644 --- a/src/Servers/Kestrel/Core/test/HttpHeadersTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpHeadersTests.cs @@ -135,7 +135,9 @@ public class HttpHeadersTests public void TestParseConnection(string connection, int intExpectedConnectionOptions) { var expectedConnectionOptions = (ConnectionOptions)intExpectedConnectionOptions; - var connectionOptions = HttpHeaders.ParseConnection(connection); + var requestHeaders = new HttpRequestHeaders(); + requestHeaders.HeaderConnection = connection; + var connectionOptions = HttpHeaders.ParseConnection(requestHeaders); Assert.Equal(expectedConnectionOptions, connectionOptions); } @@ -159,7 +161,9 @@ public void TestParseConnectionMultipleValues(string value1, string value2, int { var expectedConnectionOptions = (ConnectionOptions)intExpectedConnectionOptions; var connection = new StringValues(new[] { value1, value2 }); - var connectionOptions = HttpHeaders.ParseConnection(connection); + var requestHeaders = new HttpRequestHeaders(); + requestHeaders.HeaderConnection = connection; + var connectionOptions = HttpHeaders.ParseConnection(requestHeaders); Assert.Equal(expectedConnectionOptions, connectionOptions); } diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index 4d3984ec02e9..08885b29258e 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -283,7 +283,7 @@ static string AppendValue(bool returnTrue = false) => static string AppendHPackSwitchSection(HPackGroup group) { var header = group.Header; - if (header.Identifier == "ContentLength") + if (header.Name == HeaderNames.ContentLength) { return $@"if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector)) {{ @@ -323,7 +323,7 @@ static string AppendSwitchSection(int length, IList values) string GenerateIfBody(KnownHeader header, string extraIndent = "") { - if (header.Identifier == "ContentLength") + if (header.Name == HeaderNames.ContentLength) { return $@" {extraIndent}if (ReferenceEquals(EncodingSelector, KestrelServerOptions.DefaultRequestHeaderEncodingSelector)) @@ -755,8 +755,8 @@ internal partial class {loop.ClassName} {Each(loop.Headers.Where(header => header.FastCount), header => $@" public int {header.Identifier}Count => _headers._{header.Identifier}.Count;")} {Each(loop.Headers, header => $@" - public StringValues Header{header.Identifier} - {{{(header.Identifier == "ContentLength" ? $@" + public {(header.Name == HeaderNames.Connection ? "override " : "")}StringValues Header{header.Identifier} + {{{(header.Name == HeaderNames.ContentLength ? $@" get {{ StringValues value = default; @@ -806,7 +806,7 @@ protected override bool TryGetValueFast(string key, out StringValues value) case {byLength.Key}: {{{Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" if (ReferenceEquals(HeaderNames.{header.Identifier}, key)) - {{{(header.Identifier == "ContentLength" ? @" + {{{(header.Name == HeaderNames.ContentLength ? @" if (_contentLength.HasValue) { value = HeaderUtilities.FormatNonNegativeInt64(_contentLength.Value); @@ -822,7 +822,7 @@ protected override bool TryGetValueFast(string key, out StringValues value) }}")} {Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" if (HeaderNames.{header.Identifier}.Equals(key, StringComparison.OrdinalIgnoreCase)) - {{{(header.Identifier == "ContentLength" ? @" + {{{(header.Name == HeaderNames.ContentLength ? @" if (_contentLength.HasValue) { value = HeaderUtilities.FormatNonNegativeInt64(_contentLength.Value); @@ -851,7 +851,7 @@ protected override void SetValueFast(string key, StringValues value) case {byLength.Key}: {{{Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" if (ReferenceEquals(HeaderNames.{header.Identifier}, key)) - {{{(header.Identifier == "ContentLength" ? $@" + {{{(header.Name == HeaderNames.ContentLength ? $@" _contentLength = ParseContentLength(value.ToString());" : $@" {header.SetBit()}; _headers._{header.Identifier} = value;{(header.EnhancedSetter == false ? "" : $@" @@ -860,7 +860,7 @@ protected override void SetValueFast(string key, StringValues value) }}")} {Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" if (HeaderNames.{header.Identifier}.Equals(key, StringComparison.OrdinalIgnoreCase)) - {{{(header.Identifier == "ContentLength" ? $@" + {{{(header.Name == HeaderNames.ContentLength ? $@" _contentLength = ParseContentLength(value.ToString());" : $@" {header.SetBit()}; _headers._{header.Identifier} = value;{(header.EnhancedSetter == false ? "" : $@" @@ -882,7 +882,7 @@ protected override bool AddValueFast(string key, StringValues value) case {byLength.Key}: {{{Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" if (ReferenceEquals(HeaderNames.{header.Identifier}, key)) - {{{(header.Identifier == "ContentLength" ? $@" + {{{(header.Name == HeaderNames.ContentLength ? $@" if (!_contentLength.HasValue) {{ _contentLength = ParseContentLength(value); @@ -900,7 +900,7 @@ protected override bool AddValueFast(string key, StringValues value) }}")} {Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" if (HeaderNames.{header.Identifier}.Equals(key, StringComparison.OrdinalIgnoreCase)) - {{{(header.Identifier == "ContentLength" ? $@" + {{{(header.Name == HeaderNames.ContentLength ? $@" if (!_contentLength.HasValue) {{ _contentLength = ParseContentLength(value); @@ -930,7 +930,7 @@ protected override bool RemoveFast(string key) case {byLength.Key}: {{{Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" if (ReferenceEquals(HeaderNames.{header.Identifier}, key)) - {{{(header.Identifier == "ContentLength" ? @" + {{{(header.Name == HeaderNames.ContentLength ? @" if (_contentLength.HasValue) { _contentLength = null; @@ -948,7 +948,7 @@ protected override bool RemoveFast(string key) }}")} {Each(byLength.OrderBy(h => !h.PrimaryHeader), header => $@" if (HeaderNames.{header.Identifier}.Equals(key, StringComparison.OrdinalIgnoreCase)) - {{{(header.Identifier == "ContentLength" ? @" + {{{(header.Name == HeaderNames.ContentLength ? @" if (_contentLength.HasValue) { _contentLength = null; From e90f998a884a91e3e0250c3b56b872cf7fa4e6bb Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Sat, 27 Mar 2021 14:04:13 +0000 Subject: [PATCH 3/6] Use interned strings for websockets --- src/Middleware/WebSockets/src/Constants.cs | 7 +++--- .../WebSockets/src/HandshakeHelpers.cs | 22 +++++++++++++++---- .../WebSockets/src/WebSocketMiddleware.cs | 11 +++++----- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/Middleware/WebSockets/src/Constants.cs b/src/Middleware/WebSockets/src/Constants.cs index 6208d4914a92..3757d3a344ab 100644 --- a/src/Middleware/WebSockets/src/Constants.cs +++ b/src/Middleware/WebSockets/src/Constants.cs @@ -6,10 +6,9 @@ namespace Microsoft.AspNetCore.WebSockets internal static class Constants { public static class Headers - { - public const string UpgradeWebSocket = "websocket"; - public const string ConnectionUpgrade = "Upgrade"; - public const string SupportedVersion = "13"; + { + public readonly static string UpgradeWebSocket = "websocket"; + public readonly static string SupportedVersion = "13"; } } } diff --git a/src/Middleware/WebSockets/src/HandshakeHelpers.cs b/src/Middleware/WebSockets/src/HandshakeHelpers.cs index f18de60e0e30..4ecc685090c6 100644 --- a/src/Middleware/WebSockets/src/HandshakeHelpers.cs +++ b/src/Middleware/WebSockets/src/HandshakeHelpers.cs @@ -34,7 +34,7 @@ internal static class HandshakeHelpers }; // Verify Method, Upgrade, Connection, version, key, etc.. - public static bool CheckSupportedWebSocketRequest(string method, List> headers) + public static bool CheckSupportedWebSocketRequest(string method, List> interestingHeaders, IHeaderDictionary requestHeaders) { bool validUpgrade = false, validConnection = false, validKey = false, validVersion = false; @@ -43,11 +43,11 @@ public static bool CheckSupportedWebSocketRequest(string method, List>(); - foreach (string headerName in HandshakeHelpers.NeededHeaders) + var requestHeaders = _context.Request.Headers; + var interestingHeaders = new List>(); + foreach (var headerName in HandshakeHelpers.NeededHeaders) { - foreach (var value in _context.Request.Headers.GetCommaSeparatedValues(headerName)) + foreach (var value in requestHeaders.GetCommaSeparatedValues(headerName)) { - headers.Add(new KeyValuePair(headerName, value)); + interestingHeaders.Add(new KeyValuePair(headerName, value)); } } - _isWebSocketRequest = HandshakeHelpers.CheckSupportedWebSocketRequest(_context.Request.Method, headers); + _isWebSocketRequest = HandshakeHelpers.CheckSupportedWebSocketRequest(_context.Request.Method, interestingHeaders, requestHeaders); } } return _isWebSocketRequest.Value; From 607f067a14542d4eba421ed2ee68224d57328155 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Sun, 28 Mar 2021 21:55:40 +0100 Subject: [PATCH 4/6] Keep baggage, tracestate, and traceparent with previous casing --- src/Http/Headers/src/HeaderNames.cs | 6 +++--- src/Servers/Kestrel/shared/KnownHeaders.cs | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Http/Headers/src/HeaderNames.cs b/src/Http/Headers/src/HeaderNames.cs index 1347cffebe18..ca48a8ab0e82 100644 --- a/src/Http/Headers/src/HeaderNames.cs +++ b/src/Http/Headers/src/HeaderNames.cs @@ -65,7 +65,7 @@ public static class HeaderNames public static readonly string Authorization = "Authorization"; /// Gets the baggage HTTP header name. - public static readonly string Baggage = "Baggage"; + public static readonly string Baggage = "baggage"; /// Gets the Cache-Control HTTP header name. public static readonly string CacheControl = "Cache-Control"; @@ -248,10 +248,10 @@ public static class HeaderNames public static readonly string Translate = "Translate"; /// Gets the traceparent HTTP header name. - public static readonly string TraceParent = "TraceParent"; + public static readonly string TraceParent = "traceparent"; /// Gets the tracestate HTTP header name. - public static readonly string TraceState = "TraceState"; + public static readonly string TraceState = "tracestate"; /// Gets the Upgrade HTTP header name. public static readonly string Upgrade = "Upgrade"; diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index 08885b29258e..e584b44b4194 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -387,6 +387,14 @@ public class KnownHeader private string ResolveIdentifier(string name) { + // Check the 3 lowercase headers + switch (name) + { + case "baggage": return "Baggage"; + case "traceparent": return "TraceParent"; + case "tracestate": return "TraceState"; + } + var identifier = name.Replace("-", ""); // Pseudo headers start with a colon. A colon isn't valid in C# names so From 1322ea10a6c35fb846fdac83a4f6592831810e77 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 30 Mar 2021 01:48:33 +0100 Subject: [PATCH 5/6] Update src/Servers/Kestrel/shared/KnownHeaders.cs Co-authored-by: Chris Ross --- src/Servers/Kestrel/shared/KnownHeaders.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index e584b44b4194..0dd2cc9b4994 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -9,7 +9,6 @@ using System.Net.Http.HPack; using System.Reflection; using System.Text; - using Microsoft.Net.Http.Headers; namespace CodeGenerator From abe7107dce23e40d1a072a9d4a72007c39df932f Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Tue, 30 Mar 2021 02:13:48 +0100 Subject: [PATCH 6/6] Feedback --- .../Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs | 2 +- src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs | 2 +- src/Servers/Kestrel/shared/KnownHeaders.cs | 2 +- 3 files changed, 3 insertions(+), 3 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 5d3fd31122e7..6bba7c9af45a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.Generated.cs @@ -101,7 +101,7 @@ internal enum KnownHeaderType internal partial class HttpHeaders { - protected readonly static HashSet s_internedHeaderNames = new HashSet(93, StringComparer.OrdinalIgnoreCase) + private readonly static HashSet _internedHeaderNames = new HashSet(93, StringComparer.OrdinalIgnoreCase) { HeaderNames.Accept, HeaderNames.AcceptCharset, diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs index 9b8760385640..c823ff2ef829 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs @@ -133,7 +133,7 @@ protected static string GetInternedHeaderName(string name) { // Some headers can be very long lived; for example those on a WebSocket connection // so we exchange these for the preallocated strings predefined in HeaderNames - if (s_internedHeaderNames.TryGetValue(name, out var internedName)) + if (_internedHeaderNames.TryGetValue(name, out var internedName)) { return internedName; } diff --git a/src/Servers/Kestrel/shared/KnownHeaders.cs b/src/Servers/Kestrel/shared/KnownHeaders.cs index 0dd2cc9b4994..e8b1d010df78 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -1218,7 +1218,7 @@ public bool MoveNext() private static string GetHeaderLookup() { var headerNameFields = typeof(HeaderNames).GetFields(BindingFlags.Static | BindingFlags.Public); - return @$"protected readonly static HashSet s_internedHeaderNames = new HashSet({headerNameFields.Length}, StringComparer.OrdinalIgnoreCase) + return @$"private readonly static HashSet _internedHeaderNames = new HashSet({headerNameFields.Length}, StringComparer.OrdinalIgnoreCase) {{{Each(headerNameFields, (f) => @" HeaderNames." + f.Name + ",")} }};";