diff --git a/src/Components/Components/src/Routing/TemplateParser.cs b/src/Components/Components/src/Routing/TemplateParser.cs index b9e95872ebe3..778e5b55b6e4 100644 --- a/src/Components/Components/src/Routing/TemplateParser.cs +++ b/src/Components/Components/src/Routing/TemplateParser.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; + namespace Microsoft.AspNetCore.Components.Routing; // This implementation is temporary, in the future we'll want to have @@ -18,8 +20,7 @@ namespace Microsoft.AspNetCore.Components.Routing; // * Catch-all parameters (Like /blog/{*slug}) internal sealed class TemplateParser { - public static readonly char[] InvalidParameterNameCharacters = - new char[] { '{', '}', '=', '.' }; + private static readonly IndexOfAnyValues _invalidParameterNameCharacters = IndexOfAnyValues.Create("{}=."); internal static RouteTemplate ParseTemplate(string template) { @@ -70,9 +71,10 @@ internal static RouteTemplate ParseTemplate(string template) $"Invalid template '{template}'. Empty parameter name in segment '{segment}' is not allowed."); } - var invalidCharacter = segment.IndexOfAny(InvalidParameterNameCharacters, 1, segment.Length - 2); - if (invalidCharacter != -1) + var invalidCharacter = segment.AsSpan(1, segment.Length - 2).IndexOfAny(_invalidParameterNameCharacters); + if (invalidCharacter >= 0) { + invalidCharacter++; // accommodate the slice above throw new InvalidOperationException( $"Invalid template '{template}'. The character '{segment[invalidCharacter]}' in parameter segment '{segment}' is not allowed."); } diff --git a/src/Http/Routing/src/Patterns/RoutePatternFactory.cs b/src/Http/Routing/src/Patterns/RoutePatternFactory.cs index edfd3a004fc3..d3d6c13748cc 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternFactory.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternFactory.cs @@ -807,7 +807,7 @@ public static RoutePatternParameterPart ParameterPart(string parameterName) throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + if (parameterName.AsSpan().IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } @@ -833,7 +833,7 @@ public static RoutePatternParameterPart ParameterPart(string parameterName, obje throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + if (parameterName.AsSpan().IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } @@ -863,7 +863,7 @@ public static RoutePatternParameterPart ParameterPart( throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + if (parameterName.AsSpan().IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } @@ -900,7 +900,7 @@ public static RoutePatternParameterPart ParameterPart( throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + if (parameterName.AsSpan().IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } @@ -942,7 +942,7 @@ public static RoutePatternParameterPart ParameterPart( throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + if (parameterName.AsSpan().IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } diff --git a/src/Http/Routing/src/Patterns/RoutePatternParser.cs b/src/Http/Routing/src/Patterns/RoutePatternParser.cs index 78b3c1c8855e..8781147a4f63 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternParser.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternParser.cs @@ -3,6 +3,7 @@ #nullable disable +using System.Buffers; using System.Diagnostics; namespace Microsoft.AspNetCore.Routing.Patterns; @@ -13,17 +14,9 @@ internal static class RoutePatternParser private const char OpenBrace = '{'; private const char CloseBrace = '}'; private const char QuestionMark = '?'; - private const char Asterisk = '*'; private const string PeriodString = "."; - internal static readonly char[] InvalidParameterNameChars = new char[] - { - Separator, - OpenBrace, - CloseBrace, - QuestionMark, - Asterisk - }; + internal static readonly IndexOfAnyValues InvalidParameterNameChars = IndexOfAnyValues.Create("/{}?*"); public static RoutePattern Parse(string pattern) { @@ -431,7 +424,7 @@ private static bool IsSegmentValid(Context context, List parts private static bool IsValidParameterName(Context context, string parameterName) { - if (parameterName.Length == 0 || parameterName.IndexOfAny(InvalidParameterNameChars) >= 0) + if (parameterName.Length == 0 || parameterName.AsSpan().IndexOfAny(InvalidParameterNameChars) >= 0) { context.Error = Resources.FormatTemplateRoute_InvalidParameterName(parameterName); return false; diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs index ea9d14ee13d7..6c880d3da21d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelServerImpl.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; @@ -89,8 +88,6 @@ internal KestrelServerImpl( Features.Set(_serverAddresses); _transportManager = new TransportManager(_transportFactories, _multiplexedTransportFactories, ServiceContext); - - HttpCharacters.Initialize(); } private static ServiceContext CreateServiceContext(IOptions options, ILoggerFactory loggerFactory, DiagnosticSource? diagnosticSource) diff --git a/src/Shared/HttpSys/RequestProcessing/HeaderCollection.cs b/src/Shared/HttpSys/RequestProcessing/HeaderCollection.cs index c5be1f489fc3..2bdde0e7084d 100644 --- a/src/Shared/HttpSys/RequestProcessing/HeaderCollection.cs +++ b/src/Shared/HttpSys/RequestProcessing/HeaderCollection.cs @@ -272,10 +272,12 @@ public static void ValidateHeaderCharacters(string headerCharacters) { if (headerCharacters != null) { - var invalid = HttpCharacters.IndexOfInvalidFieldValueCharExtended(headerCharacters); - if (invalid >= 0) + var invalidIndex = HttpCharacters.IndexOfInvalidFieldValueCharExtended(headerCharacters); + if (invalidIndex >= 0) { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Invalid control character in header: 0x{0:X2}", headerCharacters[invalid])); + Throw(headerCharacters, invalidIndex); + static void Throw(string headerCharacters, int invalidIndex) + => throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Invalid control character in header: 0x{0:X2}", headerCharacters[invalidIndex])); } } } diff --git a/src/Shared/ServerInfrastructure/HttpCharacters.cs b/src/Shared/ServerInfrastructure/HttpCharacters.cs index 9892ed6fdbb7..799c240a5bc5 100644 --- a/src/Shared/ServerInfrastructure/HttpCharacters.cs +++ b/src/Shared/ServerInfrastructure/HttpCharacters.cs @@ -1,224 +1,55 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; +using System.Buffers; namespace Microsoft.AspNetCore.Http; internal static class HttpCharacters { - private const int _tableSize = 128; - private static readonly bool[] _alphaNumeric = InitializeAlphaNumeric(); - private static readonly bool[] _authority = InitializeAuthority(); - private static readonly bool[] _token = InitializeToken(); - private static readonly bool[] _host = InitializeHost(); - private static readonly bool[] _fieldValue = InitializeFieldValue(); + // ALPHA and DIGIT https://tools.ietf.org/html/rfc5234#appendix-B.1 + private const string AlphaNumeric = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - internal static void Initialize() - { - // Access _alphaNumeric to initialize static fields - var _ = _alphaNumeric; - } + // Authority https://tools.ietf.org/html/rfc3986#section-3.2 + // Examples: + // microsoft.com + // hostname:8080 + // [::]:8080 + // [fe80::] + // 127.0.0.1 + // user@host.com + // user:password@host.com + private static readonly IndexOfAnyValues _allowedAuthorityBytes = IndexOfAnyValues.Create(":.-[]@0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"u8); - private static bool[] InitializeAlphaNumeric() - { - // ALPHA and DIGIT https://tools.ietf.org/html/rfc5234#appendix-B.1 - var alphaNumeric = new bool[_tableSize]; - for (var c = '0'; c <= '9'; c++) - { - alphaNumeric[c] = true; - } - for (var c = 'A'; c <= 'Z'; c++) - { - alphaNumeric[c] = true; - } - for (var c = 'a'; c <= 'z'; c++) - { - alphaNumeric[c] = true; - } - return alphaNumeric; - } + // Matches Http.Sys + // Matches RFC 3986 except "*" / "+" / "," / ";" / "=" and "%" HEXDIG HEXDIG which are not allowed by Http.Sys + private static readonly IndexOfAnyValues _allowedHostChars = IndexOfAnyValues.Create("!$&'()-._~" + AlphaNumeric); - private static bool[] InitializeAuthority() - { - // Authority https://tools.ietf.org/html/rfc3986#section-3.2 - // Examples: - // microsoft.com - // hostname:8080 - // [::]:8080 - // [fe80::] - // 127.0.0.1 - // user@host.com - // user:password@host.com - var authority = new bool[_tableSize]; - Array.Copy(_alphaNumeric, authority, _tableSize); - authority[':'] = true; - authority['.'] = true; - authority['-'] = true; - authority['['] = true; - authority[']'] = true; - authority['@'] = true; - return authority; - } + // tchar https://tools.ietf.org/html/rfc7230#appendix-B + private static readonly IndexOfAnyValues _allowedTokenChars = IndexOfAnyValues.Create("!#$%&'*+-.^_`|~" + AlphaNumeric); + private static readonly IndexOfAnyValues _allowedTokenBytes = IndexOfAnyValues.Create("!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"u8); - private static bool[] InitializeToken() - { - // tchar https://tools.ietf.org/html/rfc7230#appendix-B - var token = new bool[_tableSize]; - Array.Copy(_alphaNumeric, token, _tableSize); - token['!'] = true; - token['#'] = true; - token['$'] = true; - token['%'] = true; - token['&'] = true; - token['\''] = true; - token['*'] = true; - token['+'] = true; - token['-'] = true; - token['.'] = true; - token['^'] = true; - token['_'] = true; - token['`'] = true; - token['|'] = true; - token['~'] = true; - return token; - } + // field-value https://tools.ietf.org/html/rfc7230#section-3.2 + // HTAB, [VCHAR, SP] + private static readonly IndexOfAnyValues _allowedFieldChars = IndexOfAnyValues.Create("\t !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + AlphaNumeric); - private static bool[] InitializeHost() - { - // Matches Http.Sys - // Matches RFC 3986 except "*" / "+" / "," / ";" / "=" and "%" HEXDIG HEXDIG which are not allowed by Http.Sys - var host = new bool[_tableSize]; - Array.Copy(_alphaNumeric, host, _tableSize); - host['!'] = true; - host['$'] = true; - host['&'] = true; - host['\''] = true; - host['('] = true; - host[')'] = true; - host['-'] = true; - host['.'] = true; - host['_'] = true; - host['~'] = true; - return host; - } + // Values are [0x00, 0x1F] without 0x09 (HTAB) and with 0x7F. + private static readonly IndexOfAnyValues _invalidFieldChars = IndexOfAnyValues.Create( + "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u000A\u000B\u000C\u000D\u000E\u000F\u0010" + + "\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u007F"); - private static bool[] InitializeFieldValue() - { - // field-value https://tools.ietf.org/html/rfc7230#section-3.2 - var fieldValue = new bool[_tableSize]; + public static bool ContainsInvalidAuthorityChar(ReadOnlySpan span) => span.IndexOfAnyExcept(_allowedAuthorityBytes) >= 0; - fieldValue[0x9] = true; // HTAB + public static int IndexOfInvalidHostChar(ReadOnlySpan span) => span.IndexOfAnyExcept(_allowedHostChars); - for (var c = 0x20; c <= 0x7e; c++) // VCHAR and SP - { - fieldValue[c] = true; - } - return fieldValue; - } + public static int IndexOfInvalidTokenChar(ReadOnlySpan span) => span.IndexOfAnyExcept(_allowedTokenChars); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ContainsInvalidAuthorityChar(Span s) - { - var authority = _authority; - - for (var i = 0; i < s.Length; i++) - { - var c = s[i]; - if (c >= (uint)authority.Length || !authority[c]) - { - return true; - } - } - - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int IndexOfInvalidHostChar(string s) - { - var host = _host; - - for (var i = 0; i < s.Length; i++) - { - var c = s[i]; - if (c >= (uint)host.Length || !host[c]) - { - return i; - } - } - - return -1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int IndexOfInvalidTokenChar(string s) - { - var token = _token; - - for (var i = 0; i < s.Length; i++) - { - var c = s[i]; - if (c >= (uint)token.Length || !token[c]) - { - return i; - } - } - - return -1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int IndexOfInvalidTokenChar(ReadOnlySpan span) - { - var token = _token; - - for (var i = 0; i < span.Length; i++) - { - var c = span[i]; - if (c >= (uint)token.Length || !token[c]) - { - return i; - } - } - - return -1; - } + public static int IndexOfInvalidTokenChar(ReadOnlySpan span) => span.IndexOfAnyExcept(_allowedTokenBytes); // Follows field-value rules in https://tools.ietf.org/html/rfc7230#section-3.2 // Disallows characters > 0x7E. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int IndexOfInvalidFieldValueChar(string s) - { - var fieldValue = _fieldValue; - - for (var i = 0; i < s.Length; i++) - { - var c = s[i]; - if (c >= (uint)fieldValue.Length || !fieldValue[c]) - { - return i; - } - } - - return -1; - } + public static int IndexOfInvalidFieldValueChar(ReadOnlySpan span) => span.IndexOfAnyExcept(_allowedFieldChars); // Follows field-value rules for chars <= 0x7F. Allows extended characters > 0x7F. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int IndexOfInvalidFieldValueCharExtended(string s) - { - var fieldValue = _fieldValue; - - for (var i = 0; i < s.Length; i++) - { - var c = s[i]; - if (c < (uint)fieldValue.Length && !fieldValue[c]) - { - return i; - } - } - - return -1; - } + public static int IndexOfInvalidFieldValueCharExtended(ReadOnlySpan span) => span.IndexOfAny(_invalidFieldChars); }