From cd024d48beea196325abe27e52b2737982e053f7 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 15 Apr 2020 13:13:37 -0400 Subject: [PATCH] Simplify KnownHeaders lookup in SocketsHttpHandler (#34974) * Simplify KnownHeaders lookup * Update src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs Co-Authored-By: David Shulman Co-authored-by: David Shulman --- .../src/System/Net/Http/Headers/KnownHeaders.cs | 227 +++++++++++---------- .../tests/UnitTests/Headers/KnownHeadersTest.cs | 139 +++++++++++++ .../UnitTests/System.Net.Http.Unit.Tests.csproj | 1 + 3 files changed, 255 insertions(+), 112 deletions(-) create mode 100644 src/libraries/System.Net.Http/tests/UnitTests/Headers/KnownHeadersTest.cs diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs index 654765e..0708e8e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/KnownHeaders.cs @@ -141,13 +141,19 @@ namespace System.Net.Http.Headers public char this[int index] => (char)_p[index]; } - // Find possible known header match via lookup on length and a distinguishing char for that length. - // Matching is case-insenstive. - // NOTE: Because of this, we do not preserve the case of the original header, - // whether from the wire or from the user explicitly setting a known header using a header name string. + /// + /// Find possible known header match via lookup on length and a distinguishing char for that length. + /// + /// + /// Matching is case-insensitive. Because of this, we do not preserve the case of the original header, + /// whether from the wire or from the user explicitly setting a known header using a header name string. + /// private static KnownHeader? GetCandidate(T key) where T : struct, IHeaderNameAccessor // Enforce struct for performance { + // Lookup is performed by first switching on the header name's length, and then switching + // on the most unique position in that length's string. + int length = key.Length; switch (length) { @@ -155,190 +161,187 @@ namespace System.Net.Http.Headers return TE; // TE case 3: - switch (key[0]) + switch (key[0] | 0x20) { - case 'A': case 'a': return Age; // [A]ge - case 'P': case 'p': return P3P; // [P]3P - case 'T': case 't': return TSV; // [T]SV - case 'V': case 'v': return Via; // [V]ia + case 'a': return Age; // [A]ge + case 'p': return P3P; // [P]3P + case 't': return TSV; // [T]SV + case 'v': return Via; // [V]ia } break; case 4: - switch (key[0]) + switch (key[0] | 0x20) { - case 'D': case 'd': return Date; // [D]ate - case 'E': case 'e': return ETag; // [E]Tag - case 'F': case 'f': return From; // [F]rom - case 'H': case 'h': return Host; // [H]ost - case 'L': case 'l': return Link; // [L]ink - case 'V': case 'v': return Vary; // [V]ary + case 'd': return Date; // [D]ate + case 'e': return ETag; // [E]Tag + case 'f': return From; // [F]rom + case 'h': return Host; // [H]ost + case 'l': return Link; // [L]ink + case 'v': return Vary; // [V]ary } break; case 5: - switch (key[0]) + switch (key[0] | 0x20) { - case 'A': case 'a': return Allow; // [A]llow - case 'R': case 'r': return Range; // [R]ange + case 'a': return Allow; // [A]llow + case 'r': return Range; // [R]ange } break; case 6: - switch (key[0]) + switch (key[0] | 0x20) { - case 'A': case 'a': return Accept; // [A]ccept - case 'C': case 'c': return Cookie; // [C]ookie - case 'E': case 'e': return Expect; // [E]xpect - case 'O': case 'o': return Origin; // [O]rigin - case 'P': case 'p': return Pragma; // [P]ragma - case 'S': case 's': return Server; // [S]erver + case 'a': return Accept; // [A]ccept + case 'c': return Cookie; // [C]ookie + case 'e': return Expect; // [E]xpect + case 'o': return Origin; // [O]rigin + case 'p': return Pragma; // [P]ragma + case 's': return Server; // [S]erver } break; case 7: - switch (key[0]) + switch (key[0] | 0x20) { case ':': return PseudoStatus; // [:]status - case 'A': case 'a': return AltSvc; // [A]lt-Svc - case 'C': case 'c': return Cookie2; // [C]ookie2 - case 'E': case 'e': return Expires; // [E]xpires - case 'R': case 'r': - switch (key[3]) + case 'a': return AltSvc; // [A]lt-Svc + case 'c': return Cookie2; // [C]ookie2 + case 'e': return Expires; // [E]xpires + case 'r': + switch (key[3] | 0x20) { - case 'E': case 'e': return Referer; // [R]ef[e]rer - case 'R': case 'r': return Refresh; // [R]ef[r]esh + case 'e': return Referer; // [R]ef[e]rer + case 'r': return Refresh; // [R]ef[r]esh } break; - case 'T': case 't': return Trailer; // [T]railer - case 'U': case 'u': return Upgrade; // [U]pgrade - case 'W': case 'w': return Warning; // [W]arning - case 'X': case 'x': return XCache; // [X]-Cache + case 't': return Trailer; // [T]railer + case 'u': return Upgrade; // [U]pgrade + case 'w': return Warning; // [W]arning + case 'x': return XCache; // [X]-Cache } break; case 8: - switch (key[3]) + switch (key[3] | 0x20) { - case 'M': case 'm': return IfMatch; // If-[M]atch - case 'R': case 'r': return IfRange; // If-[R]ange - case 'A': case 'a': return Location; // Loc[a]tion + case '-': return AltUsed; // Alt[-]Used + case 'a': return Location; // Loc[a]tion + case 'm': return IfMatch; // If-[M]atch + case 'r': return IfRange; // If-[R]ange } break; case 9: - switch (key[0]) - { - case 'E': case 'e': return ExpectCT; // [E]xpect-CT - } - break; + return ExpectCT; // Expect-CT case 10: - switch (key[0]) + switch (key[0] | 0x20) { - case 'C': case 'c': return Connection; // [C]onnection - case 'K': case 'k': return KeepAlive; // [K]eep-Alive - case 'S': case 's': return SetCookie; // [S]et-Cookie - case 'U': case 'u': return UserAgent; // [U]ser-Agent + case 'c': return Connection; // [C]onnection + case 'k': return KeepAlive; // [K]eep-Alive + case 's': return SetCookie; // [S]et-Cookie + case 'u': return UserAgent; // [U]ser-Agent } break; case 11: - switch (key[0]) + switch (key[0] | 0x20) { - case 'C': case 'c': return ContentMD5; // [C]ontent-MD5 - case 'R': case 'r': return RetryAfter; // [R]etry-After - case 'S': case 's': return SetCookie2; // [S]et-Cookie2 + case 'c': return ContentMD5; // [C]ontent-MD5 + case 'r': return RetryAfter; // [R]etry-After + case 's': return SetCookie2; // [S]et-Cookie2 } break; case 12: - switch (key[2]) + switch (key[2] | 0x20) { - case 'C': case 'c': return AcceptPatch; // Ac[c]ept-Patch - case 'N': case 'n': return ContentType; // Co[n]tent-Type - case 'X': case 'x': return MaxForwards; // Ma[x]-Forwards - case 'M': case 'm': return XMSEdgeRef; // X-[M]SEdge-Ref - case 'P': case 'p': return XPoweredBy; // X-[P]owered-By - case 'R': case 'r': return XRequestID; // X-[R]equest-ID + case 'c': return AcceptPatch; // Ac[c]ept-Patch + case 'm': return XMSEdgeRef; // X-[M]SEdge-Ref + case 'n': return ContentType; // Co[n]tent-Type + case 'p': return XPoweredBy; // X-[P]owered-By + case 'r': return XRequestID; // X-[R]equest-ID + case 'x': return MaxForwards; // Ma[x]-Forwards } break; case 13: - switch (key[12]) + switch (key[12] | 0x20) { - case 'S': case 's': return AcceptRanges; // Accept-Range[s] - case 'N': case 'n': return Authorization; // Authorizatio[n] - case 'L': case 'l': return CacheControl; // Cache-Contro[l] - case 'E': case 'e': return ContentRange; // Content-Rang[e] - case 'H': case 'h': return IfNoneMatch; // If-None-Matc[h] - case 'D': case 'd': return LastModified; // Last-Modifie[d] - case 'T': case 't': return ProxySupport; // Proxy-Suppor[t] - case 'G': case 'g': return ServerTiming; // Server-Timin[g] + case 'd': return LastModified; // Last-Modifie[d] + case 'e': return ContentRange; // Content-Rang[e] + case 'g': return ServerTiming; // Server-Timin[g] + case 'h': return IfNoneMatch; // If-None-Matc[h] + case 'l': return CacheControl; // Cache-Contro[l] + case 'n': return Authorization; // Authorizatio[n] + case 's': return AcceptRanges; // Accept-Range[s] + case 't': return ProxySupport; // Proxy-Suppor[t] } break; case 14: - switch (key[0]) + switch (key[0] | 0x20) { - case 'A': case 'a': return AcceptCharset; // [A]ccept-Charset - case 'C': case 'c': return ContentLength; // [C]ontent-Length + case 'a': return AcceptCharset; // [A]ccept-Charset + case 'c': return ContentLength; // [C]ontent-Length } break; case 15: - switch (key[7]) + switch (key[7] | 0x20) { case '-': return XFrameOptions; // X-Frame[-]Options - case 'M': case 'm': return XUACompatible; // X-UA-Co[m]patible - case 'E': case 'e': return AcceptEncoding; // Accept-[E]ncoding - case 'K': case 'k': return PublicKeyPins; // Public-[K]ey-Pins - case 'L': case 'l': return AcceptLanguage; // Accept-[L]anguage - case 'R': case 'r': return ReferrerPolicy; // Referre[r]-Policy + case 'e': return AcceptEncoding; // Accept-[E]ncoding + case 'k': return PublicKeyPins; // Public-[K]ey-Pins + case 'l': return AcceptLanguage; // Accept-[L]anguage + case 'm': return XUACompatible; // X-UA-Co[m]patible + case 'r': return ReferrerPolicy; // Referre[r]-Policy } break; case 16: - switch (key[11]) + switch (key[11] | 0x20) { - case 'O': case 'o': return ContentEncoding; // Content-Enc[o]ding - case 'G': case 'g': return ContentLanguage; // Content-Lan[g]uage - case 'A': case 'a': return ContentLocation; // Content-Loc[a]tion - case 'C': case 'c': - switch (key[0]) + case 'a': return ContentLocation; // Content-Loc[a]tion + case 'c': + switch (key[0] | 0x20) { - case 'P': case 'p': return ProxyConnection; // [P]roxy-Conne[c]tion - case 'X': case 'x': return XXssProtection; // [X]-XSS-Prote[c]tion + case 'p': return ProxyConnection; // [P]roxy-Conne[c]tion + case 'x': return XXssProtection; // [X]-XSS-Prote[c]tion } break; - case 'I': case 'i': return WWWAuthenticate; // WWW-Authent[i]cate - case 'R': case 'r': return XAspNetVersion; // X-AspNet-Ve[r]sion + case 'g': return ContentLanguage; // Content-Lan[g]uage + case 'i': return WWWAuthenticate; // WWW-Authent[i]cate + case 'o': return ContentEncoding; // Content-Enc[o]ding + case 'r': return XAspNetVersion; // X-AspNet-Ve[r]sion } break; case 17: - switch (key[0]) + switch (key[0] | 0x20) { - case 'I': case 'i': return IfModifiedSince; // [I]f-Modified-Since - case 'S': case 's': return SecWebSocketKey; // [S]ec-WebSocket-Key - case 'T': case 't': return TransferEncoding; // [T]ransfer-Encoding + case 'i': return IfModifiedSince; // [I]f-Modified-Since + case 's': return SecWebSocketKey; // [S]ec-WebSocket-Key + case 't': return TransferEncoding; // [T]ransfer-Encoding } break; case 18: - switch (key[0]) + switch (key[0] | 0x20) { - case 'P': case 'p': return ProxyAuthenticate; // [P]roxy-Authenticate - case 'X': case 'x': return XContentDuration; // [X]-Content-Duration + case 'p': return ProxyAuthenticate; // [P]roxy-Authenticate + case 'x': return XContentDuration; // [X]-Content-Duration } break; case 19: - switch (key[0]) + switch (key[0] | 0x20) { - case 'C': case 'c': return ContentDisposition; // [C]ontent-Disposition - case 'I': case 'i': return IfUnmodifiedSince; // [I]f-Unmodified-Since - case 'P': case 'p': return ProxyAuthorization; // [P]roxy-Authorization + case 'c': return ContentDisposition; // [C]ontent-Disposition + case 'i': return IfUnmodifiedSince; // [I]f-Unmodified-Since + case 'p': return ProxyAuthorization; // [P]roxy-Authorization } break; @@ -349,11 +352,11 @@ namespace System.Net.Http.Headers return SecWebSocketVersion; // Sec-WebSocket-Version case 22: - switch (key[0]) + switch (key[0] | 0x20) { - case 'A': case 'a': return AccessControlMaxAge; // [A]ccess-Control-Max-Age - case 'S': case 's': return SecWebSocketProtocol; // [S]ec-WebSocket-Protocol - case 'X': case 'x': return XContentTypeOptions; // [X]-Content-Type-Options + case 'a': return AccessControlMaxAge; // [A]ccess-Control-Max-Age + case 's': return SecWebSocketProtocol; // [S]ec-WebSocket-Protocol + case 'x': return XContentTypeOptions; // [X]-Content-Type-Options } break; @@ -364,10 +367,10 @@ namespace System.Net.Http.Headers return SecWebSocketExtensions; // Sec-WebSocket-Extensions case 25: - switch (key[0]) + switch (key[0] | 0x20) { - case 'S': case 's': return StrictTransportSecurity; // [S]trict-Transport-Security - case 'U': case 'u': return UpgradeInsecureRequests; // [U]pgrade-Insecure-Requests + case 's': return StrictTransportSecurity; // [S]trict-Transport-Security + case 'u': return UpgradeInsecureRequests; // [U]pgrade-Insecure-Requests } break; @@ -375,10 +378,10 @@ namespace System.Net.Http.Headers return AccessControlAllowOrigin; // Access-Control-Allow-Origin case 28: - switch (key[21]) + switch (key[21] | 0x20) { - case 'H': case 'h': return AccessControlAllowHeaders; // Access-Control-Allow-[H]eaders - case 'M': case 'm': return AccessControlAllowMethods; // Access-Control-Allow-[M]ethods + case 'h': return AccessControlAllowHeaders; // Access-Control-Allow-[H]eaders + case 'm': return AccessControlAllowMethods; // Access-Control-Allow-[M]ethods } break; diff --git a/src/libraries/System.Net.Http/tests/UnitTests/Headers/KnownHeadersTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/Headers/KnownHeadersTest.cs new file mode 100644 index 0000000..b2bd2be --- /dev/null +++ b/src/libraries/System.Net.Http/tests/UnitTests/Headers/KnownHeadersTest.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Net.Http.Headers; +using Xunit; + +namespace System.Net.Http.Tests +{ + public class KnownHeadersTest + { + [Theory] + [InlineData(":status")] + [InlineData("Accept")] + [InlineData("Accept-Charset")] + [InlineData("Accept-Encoding")] + [InlineData("Accept-Language")] + [InlineData("Accept-Patch")] + [InlineData("Accept-Ranges")] + [InlineData("Access-Control-Allow-Credentials")] + [InlineData("Access-Control-Allow-Headers")] + [InlineData("Access-Control-Allow-Methods")] + [InlineData("Access-Control-Allow-Origin")] + [InlineData("Access-Control-Expose-Headers")] + [InlineData("Access-Control-Max-Age")] + [InlineData("Age")] + [InlineData("Allow")] + [InlineData("Alt-Svc")] + [InlineData("Alt-Used")] + [InlineData("Authorization")] + [InlineData("Cache-Control")] + [InlineData("Connection")] + [InlineData("Content-Disposition")] + [InlineData("Content-Encoding")] + [InlineData("Content-Language")] + [InlineData("Content-Length")] + [InlineData("Content-Location")] + [InlineData("Content-MD5")] + [InlineData("Content-Range")] + [InlineData("Content-Security-Policy")] + [InlineData("Content-Type")] + [InlineData("Cookie")] + [InlineData("Cookie2")] + [InlineData("Date")] + [InlineData("ETag")] + [InlineData("Expect")] + [InlineData("Expect-CT")] + [InlineData("Expires")] + [InlineData("From")] + [InlineData("Host")] + [InlineData("If-Match")] + [InlineData("If-Modified-Since")] + [InlineData("If-None-Match")] + [InlineData("If-Range")] + [InlineData("If-Unmodified-Since")] + [InlineData("Keep-Alive")] + [InlineData("Last-Modified")] + [InlineData("Link")] + [InlineData("Location")] + [InlineData("Max-Forwards")] + [InlineData("Origin")] + [InlineData("P3P")] + [InlineData("Pragma")] + [InlineData("Proxy-Authenticate")] + [InlineData("Proxy-Authorization")] + [InlineData("Proxy-Connection")] + [InlineData("Proxy-Support")] + [InlineData("Public-Key-Pins")] + [InlineData("Range")] + [InlineData("Referer")] + [InlineData("Referrer-Policy")] + [InlineData("Refresh")] + [InlineData("Retry-After")] + [InlineData("Sec-WebSocket-Accept")] + [InlineData("Sec-WebSocket-Extensions")] + [InlineData("Sec-WebSocket-Key")] + [InlineData("Sec-WebSocket-Protocol")] + [InlineData("Sec-WebSocket-Version")] + [InlineData("Server")] + [InlineData("Server-Timing")] + [InlineData("Set-Cookie")] + [InlineData("Set-Cookie2")] + [InlineData("Strict-Transport-Security")] + [InlineData("TE")] + [InlineData("Trailer")] + [InlineData("Transfer-Encoding")] + [InlineData("TSV")] + [InlineData("Upgrade")] + [InlineData("Upgrade-Insecure-Requests")] + [InlineData("User-Agent")] + [InlineData("Vary")] + [InlineData("Via")] + [InlineData("Warning")] + [InlineData("WWW-Authenticate")] + [InlineData("X-AspNet-Version")] + [InlineData("X-Cache")] + [InlineData("X-Content-Duration")] + [InlineData("X-Content-Type-Options")] + [InlineData("X-Frame-Options")] + [InlineData("X-MSEdge-Ref")] + [InlineData("X-Powered-By")] + [InlineData("X-Request-ID")] + [InlineData("X-UA-Compatible")] + [InlineData("X-XSS-Protection")] + public void TryGetKnownHeader_Known_Found(string name) + { + foreach (string casedName in new[] { name, name.ToUpperInvariant(), name.ToLowerInvariant() }) + { + Validate(casedName, KnownHeaders.TryGetKnownHeader(casedName)); + Validate(casedName, KnownHeaders.TryGetKnownHeader(casedName.Select(c => (byte)c).ToArray())); + } + + static void Validate(string name, KnownHeader h) + { + Assert.NotNull(h); + Assert.Same(h, KnownHeaders.TryGetKnownHeader(name)); + + Assert.Same(h, h.Descriptor.KnownHeader); + Assert.Equal(name, h.Name, StringComparer.OrdinalIgnoreCase); + Assert.Equal(name, h.Descriptor.Name, StringComparer.OrdinalIgnoreCase); + } + } + + [Theory] + [InlineData("")] + [InlineData(" \t ")] + [InlineData("Something")] + [InlineData("X-Unknown")] + public void TryGetKnownHeader_Unknown_NotFound(string name) + { + foreach (string casedName in new[] { name, name.ToUpperInvariant(), name.ToLowerInvariant() }) + { + Assert.Null(KnownHeaders.TryGetKnownHeader(casedName)); + Assert.Null(KnownHeaders.TryGetKnownHeader(casedName.Select(c => (byte)c).ToArray())); + } + } + } +} diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index 33d33d6..f8dc22a 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -377,6 +377,7 @@ + -- 2.7.4