diff --git a/src/Http/Headers/src/HeaderNames.cs b/src/Http/Headers/src/HeaderNames.cs index 1355909d6d85..ca48a8ab0e82 100644 --- a/src/Http/Headers/src/HeaderNames.cs +++ b/src/Http/Headers/src/HeaderNames.cs @@ -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"; @@ -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/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; 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 d8ad519873de..6bba7c9af45a 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 + { + private readonly static HashSet _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; @@ -125,7 +225,7 @@ public StringValues HeaderCacheControl _headers._CacheControl = value; } } - public StringValues HeaderConnection + public override StringValues HeaderConnection { get { @@ -8129,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 e7912791bd00..c823ff2ef829 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpHeaders.cs @@ -12,16 +12,17 @@ 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 { - 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 { @@ -40,6 +41,8 @@ public long? ContentLength } } + public abstract StringValues HeaderConnection { get; set; } + StringValues IHeaderDictionary.this[string key] { get @@ -126,6 +129,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 (_internedHeaderNames.TryGetValue(name, out var internedName)) + { + return internedName; + } + + return name; + } + [MethodImpl(MethodImplOptions.NoInlining)] protected static StringValues AppendValue(StringValues existing, string append) { @@ -276,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 @@ -289,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(); @@ -420,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/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..3fb3cef5e385 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseTrailers.cs @@ -25,18 +25,20 @@ 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; } + 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 e6541998aa29..e8b1d010df78 100644 --- a/src/Servers/Kestrel/shared/KnownHeaders.cs +++ b/src/Servers/Kestrel/shared/KnownHeaders.cs @@ -7,7 +7,9 @@ using System.Globalization; using System.Linq; using System.Net.Http.HPack; +using System.Reflection; using System.Text; +using Microsoft.Net.Http.Headers; namespace CodeGenerator { @@ -22,94 +24,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 +124,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 +180,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 +205,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 @@ -280,7 +282,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)) {{ @@ -320,7 +322,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)) @@ -384,6 +386,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 @@ -732,6 +742,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 ? @@ -747,8 +762,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; @@ -798,7 +813,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); @@ -814,7 +829,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); @@ -843,7 +858,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 ? "" : $@" @@ -852,7 +867,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 ? "" : $@" @@ -874,7 +889,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); @@ -892,7 +907,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); @@ -922,7 +937,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; @@ -940,7 +955,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; @@ -1200,6 +1215,15 @@ public bool MoveNext() ")}}}"; } + private static string GetHeaderLookup() + { + var headerNameFields = typeof(HeaderNames).GetFields(BindingFlags.Static | BindingFlags.Public); + return @$"private readonly static HashSet _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];