File: Internal\Infrastructure\HttpUtilities.cs
Web Access
Project: src\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core)
// 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;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
 
internal static partial class HttpUtilities
{
    public const string HttpUriScheme = "http://";
    public const string HttpsUriScheme = "https://";
 
    // readonly primitive statics can be Jit'd to consts https://github.com/dotnet/coreclr/issues/1079
    private static readonly ulong _httpSchemeLong = GetAsciiStringAsLong(HttpUriScheme + "\0");
    private static readonly ulong _httpsSchemeLong = GetAsciiStringAsLong(HttpsUriScheme);
 
    private const uint _httpGetMethodInt = 542393671; // GetAsciiStringAsInt("GET "); const results in better codegen
 
    private const ulong _http10VersionLong = 3471766442030158920; // GetAsciiStringAsLong("HTTP/1.0"); const results in better codegen
    private const ulong _http11VersionLong = 3543824036068086856; // GetAsciiStringAsLong("HTTP/1.1"); const results in better codegen
 
    private static readonly UTF8Encoding DefaultRequestHeaderEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void SetKnownMethod(ulong mask, ulong knownMethodUlong, HttpMethod knownMethod, int length)
    {
        _knownMethods[GetKnownMethodIndex(knownMethodUlong)] = new Tuple<ulong, ulong, HttpMethod, int>(mask, knownMethodUlong, knownMethod, length);
    }
 
    private static void FillKnownMethodsGaps()
    {
        var knownMethods = _knownMethods;
        var length = knownMethods.Length;
        var invalidHttpMethod = new Tuple<ulong, ulong, HttpMethod, int>(_mask8Chars, 0ul, HttpMethod.Custom, 0);
        for (int i = 0; i < length; i++)
        {
            if (knownMethods[i] == null)
            {
                knownMethods[i] = invalidHttpMethod;
            }
        }
    }
 
    private static ulong GetAsciiStringAsLong(string str)
    {
        Debug.Assert(str.Length == 8, "String must be exactly 8 (ASCII) characters long.");
 
        Span<byte> bytes = stackalloc byte[8];
        OperationStatus operationStatus = Ascii.FromUtf16(str, bytes, out _);
 
        Debug.Assert(operationStatus == OperationStatus.Done);
 
        return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
    }
 
    private static uint GetAsciiStringAsInt(string str)
    {
        Debug.Assert(str.Length == 4, "String must be exactly 4 (ASCII) characters long.");
 
        Span<byte> bytes = stackalloc byte[4];
        OperationStatus operationStatus = Ascii.FromUtf16(str, bytes, out _);
 
        Debug.Assert(operationStatus == OperationStatus.Done);
 
        return BinaryPrimitives.ReadUInt32LittleEndian(bytes);
    }
 
    private static ulong GetMaskAsLong(ReadOnlySpan<byte> bytes)
    {
        Debug.Assert(bytes.Length == 8, "Mask must be exactly 8 bytes long.");
 
        return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
    }
 
    // The same as GetAsciiString but throws BadRequest for null character
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static string GetHeaderName(this ReadOnlySpan<byte> span)
    {
        if (span.IsEmpty)
        {
            return string.Empty;
        }
 
        var str = string.Create(span.Length, span, static (destination, source) =>
        {
            if (source.Contains((byte)0)
                || Ascii.ToUtf16(source, destination, out var written) != OperationStatus.Done)
            {
                KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName);
            }
            else
            {
                Debug.Assert(written == destination.Length);
            }
        });
 
        return str;
    }
 
    // Null checks must be done independently of this method (if required)
    public static string GetAsciiString(this Span<byte> span)
        => StringUtilities.GetAsciiString(span);
 
    // Null checks must be done independently of this method (if required)
    public static string GetAsciiOrUTF8String(this ReadOnlySpan<byte> span)
        => StringUtilities.GetAsciiOrUTF8String(span, DefaultRequestHeaderEncoding);
 
    public static string GetRequestHeaderString(this ReadOnlySpan<byte> span, string name, Func<string, Encoding?> encodingSelector, bool checkForNewlineChars)
    {
        string result;
        if (ReferenceEquals(KestrelServerOptions.DefaultHeaderEncodingSelector, encodingSelector))
        {
            result = span.GetAsciiOrUTF8String(DefaultRequestHeaderEncoding);
        }
        else
        {
            result = span.GetRequestHeaderStringWithoutDefaultEncodingCore(name, encodingSelector);
        }
 
        // New Line characters (CR, LF) are considered invalid at this point.
        // Null characters are also not allowed.
        var hasInvalidChar = checkForNewlineChars ?
            ((ReadOnlySpan<char>)result).ContainsAny('\r', '\n', '\0')
            : ((ReadOnlySpan<char>)result).Contains('\0');
 
        if (hasInvalidChar)
        {
            ThrowForInvalidCharacter(checkForNewlineChars);
        }
 
        return result;
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void ThrowForInvalidCharacter(bool checkForNewlines)
    {
        if (checkForNewlines)
        {
            throw new InvalidOperationException("Newline characters (CR/LF) or Null are not allowed in request headers.");
        }
        else
        {
            throw new InvalidOperationException("Null characters are not allowed in request headers.");
        }
    }
 
    private static string GetRequestHeaderStringWithoutDefaultEncodingCore(this ReadOnlySpan<byte> span, string name, Func<string, Encoding?> encodingSelector)
    {
        var encoding = encodingSelector(name);
 
        if (encoding is null)
        {
            return span.GetAsciiOrUTF8String(DefaultRequestHeaderEncoding);
        }
        if (ReferenceEquals(encoding, Encoding.Latin1))
        {
            return span.GetLatin1String();
        }
        try
        {
            return encoding.GetString(span);
        }
        catch (DecoderFallbackException ex)
        {
            throw new InvalidOperationException(ex.Message, ex);
        }
    }
 
    public static string GetAsciiStringEscaped(this ReadOnlySpan<byte> span, int maxChars)
    {
        var sb = new StringBuilder();
 
        for (var i = 0; i < Math.Min(span.Length, maxChars); i++)
        {
            var ch = span[i];
            sb.Append(ch < 0x20 || ch >= 0x7F ? $"\\x{ch:X2}" : ((char)ch).ToString());
        }
 
        if (span.Length > maxChars)
        {
            sb.Append("...");
        }
 
        return sb.ToString();
    }
 
    /// <summary>
    /// Checks that up to 8 bytes from <paramref name="span"/> correspond to a known HTTP method.
    /// </summary>
    /// <remarks>
    /// A "known HTTP method" can be an HTTP method name defined in the HTTP/1.1 RFC.
    /// Since all of those fit in at most 8 bytes, they can be optimally looked up by reading those bytes as a long. Once
    /// in that format, it can be checked against the known method.
    /// The Known Methods (CONNECT, DELETE, GET, HEAD, PATCH, POST, PUT, OPTIONS, TRACE) are all less than 8 bytes
    /// and will be compared with the required space. A mask is used if the Known method is less than 8 bytes.
    /// To optimize performance the GET method will be checked first.
    /// </remarks>
    /// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
    public static bool GetKnownMethod(this ReadOnlySpan<byte> span, out HttpMethod method, out int length)
    {
        method = GetKnownMethod(span, out length);
        return method != HttpMethod.Custom;
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static HttpMethod GetKnownMethod(this ReadOnlySpan<byte> span, out int methodLength)
    {
        methodLength = 0;
        if (sizeof(uint) <= span.Length)
        {
            if (BinaryPrimitives.ReadUInt32LittleEndian(span) == _httpGetMethodInt)
            {
                methodLength = 3;
                return HttpMethod.Get;
            }
            else if (sizeof(ulong) <= span.Length)
            {
                var value = BinaryPrimitives.ReadUInt64LittleEndian(span);
                var index = GetKnownMethodIndex(value);
                var knownMethods = _knownMethods;
                if ((uint)index < (uint)knownMethods.Length)
                {
                    var knownMethod = _knownMethods[index];
 
                    if (knownMethod != null && (value & knownMethod.Item1) == knownMethod.Item2)
                    {
                        methodLength = knownMethod.Item4;
                        return knownMethod.Item3;
                    }
                }
            }
        }
 
        return HttpMethod.Custom;
    }
 
    /// <summary>
    /// Parses string <paramref name="value"/> for a known HTTP method.
    /// </summary>
    /// <remarks>
    /// A "known HTTP method" can be an HTTP method name defined in the HTTP/1.1 RFC.
    /// The Known Methods (CONNECT, DELETE, GET, HEAD, PATCH, POST, PUT, OPTIONS, TRACE)
    /// </remarks>
    /// <returns><see cref="HttpMethod"/></returns>
    public static HttpMethod GetKnownMethod(string? value)
    {
        // A perfect hash is used to get an index into a lookup-table for know HTTP-methods.
        //
        // If value is not null or an empty string, then local function PerfectHash is called.
        // This perfect hashing is done by lookup up 'associatedValues' from a pre-generated lookup-table.
        // The generation of that table was done by GNU gperf tool.
        // Once we have that perfect hash we use another lookup-table to get the know HTTP-method if found
        // or return HttpMethod.Custom if not found.
        // 
        // Further info and how to call gperf see https://github.com/dotnet/aspnetcore/pull/44096
        //
        // Code here could be removed if Roslyn improvements from
        // https://github.com/dotnet/roslyn/issues/56374 are added.
 
        const int MinWordLength = 3;
        const int MaxWordLength = 7;
        const int MaxHashValue = 12;
 
        if (string.IsNullOrEmpty(value))
        {
            return HttpMethod.None;
        }
 
        if ((uint)(value.Length - MinWordLength) <= (MaxWordLength - MinWordLength))
        {
            var methodsLookup = Methods();
 
            Debug.Assert(WordListForPerfectHashOfMethods.Length == (MaxHashValue + 1) && methodsLookup.Length == (MaxHashValue + 1));
 
            var index = PerfectHash(value);
 
            if (index < (uint)WordListForPerfectHashOfMethods.Length
                && WordListForPerfectHashOfMethods[index] == value
                && index < (uint)methodsLookup.Length)
            {
                return methodsLookup[(int)index];
            }
        }
 
        return HttpMethod.Custom;
 
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static uint PerfectHash(ReadOnlySpan<char> str)
        {
            ReadOnlySpan<byte> associatedValues =
            [
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13,  5,  0, 13,
                13,  0,  0, 13, 13, 13, 13, 13, 13,  0,
                 5, 13, 13, 13,  0, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
                13, 13, 13, 13, 13, 13
            ];
 
            var c = MemoryMarshal.GetReference(str);
 
            Debug.Assert(char.IsAscii(c), "Must already be validated");
 
            return (uint)str.Length + associatedValues[c];
        }
 
        static ReadOnlySpan<HttpMethod> Methods() =>
        [
            HttpMethod.None,
            HttpMethod.None,
            HttpMethod.None,
            HttpMethod.Get,
            HttpMethod.Head,
            HttpMethod.Trace,
            HttpMethod.Delete,
            HttpMethod.Options,
            HttpMethod.Put,
            HttpMethod.Post,
            HttpMethod.Patch,
            HttpMethod.None,
            HttpMethod.Connect
        ];
    }
 
    private static readonly string[] WordListForPerfectHashOfMethods =
    {
        "",
        "",
        "",
        "GET",
        "HEAD",
        "TRACE",
        "DELETE",
        "OPTIONS",
        "PUT",
        "POST",
        "PATCH",
        "",
        "CONNECT"
    };
 
    /// <summary>
    /// Checks 9 bytes from <paramref name="span"/>  correspond to a known HTTP version.
    /// </summary>
    /// <remarks>
    /// A "known HTTP version" Is is either HTTP/1.0 or HTTP/1.1.
    /// Since those fit in 8 bytes, they can be optimally looked up by reading those bytes as a long. Once
    /// in that format, it can be checked against the known versions.
    /// The Known versions will be checked with the required '\r'.
    /// To optimize performance the HTTP/1.1 will be checked first.
    /// </remarks>
    /// <returns><c>true</c> if the input matches a known string, <c>false</c> otherwise.</returns>
    public static bool GetKnownVersion(this ReadOnlySpan<byte> span, out HttpVersion knownVersion, out byte length)
    {
        if (span.Length > sizeof(ulong) && span[sizeof(ulong)] == (byte)'\r')
        {
            knownVersion = GetKnownVersion(span);
            if (knownVersion != HttpVersion.Unknown)
            {
                length = sizeof(ulong);
                return true;
            }
        }
 
        knownVersion = HttpVersion.Unknown;
        length = 0;
        return false;
    }
 
    /// <summary>
    /// Checks 8 bytes from <paramref name="span"/>  correspond to a known HTTP version.
    /// </summary>
    /// <remarks>
    /// A "known HTTP version" Is is either HTTP/1.0 or HTTP/1.1.
    /// Since those fit in 8 bytes, they can be optimally looked up by reading those bytes as a long. Once
    /// in that format, it can be checked against the known versions.
    /// To optimize performance the HTTP/1.1 will be checked first.
    /// </remarks>
    /// <returns>the HTTP version if the input matches a known string, <c>Unknown</c> otherwise.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal static HttpVersion GetKnownVersion(this ReadOnlySpan<byte> span)
    {
        if (BinaryPrimitives.TryReadUInt64LittleEndian(span, out var version))
        {
            if (version == _http11VersionLong)
            {
                return HttpVersion.Http11;
            }
            else if (version == _http10VersionLong)
            {
                return HttpVersion.Http10;
            }
        }
        return HttpVersion.Unknown;
    }
 
    /// <summary>
    /// Checks 8 bytes from <paramref name="span"/> that correspond to 'http://' or 'https://'
    /// </summary>
    /// <param name="span">The span</param>
    /// <param name="knownScheme">A reference to the known scheme, if the input matches any</param>
    /// <returns>True when memory starts with known http or https schema</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool GetKnownHttpScheme(this Span<byte> span, out HttpScheme knownScheme)
    {
        if (BinaryPrimitives.TryReadUInt64LittleEndian(span, out var scheme))
        {
            if ((scheme & _mask7Chars) == _httpSchemeLong)
            {
                knownScheme = HttpScheme.Http;
                return true;
            }
 
            if (scheme == _httpsSchemeLong)
            {
                knownScheme = HttpScheme.Https;
                return true;
            }
        }
        knownScheme = HttpScheme.Unknown;
        return false;
    }
 
    public static string VersionToString(HttpVersion httpVersion)
    {
        switch (httpVersion)
        {
            case HttpVersion.Http10:
                return AspNetCore.Http.HttpProtocol.Http10;
            case HttpVersion.Http11:
                return AspNetCore.Http.HttpProtocol.Http11;
            case HttpVersion.Http2:
                return AspNetCore.Http.HttpProtocol.Http2;
            case HttpVersion.Http3:
                return AspNetCore.Http.HttpProtocol.Http3;
            default:
                Debug.Fail("Unexpected HttpVersion: " + httpVersion);
                return null;
        };
    }
 
    public static string? MethodToString(HttpMethod method)
    {
        var methodIndex = (int)method;
        var methodNames = _methodNames;
        if ((uint)methodIndex < (uint)methodNames.Length)
        {
            return methodNames[methodIndex];
        }
        return null;
    }
 
    public static string? SchemeToString(HttpScheme scheme)
    {
        return scheme switch
        {
            HttpScheme.Http => HttpUriScheme,
            HttpScheme.Https => HttpsUriScheme,
            _ => null,
        };
    }
 
    public static bool IsHostHeaderValid(string hostText)
    {
        if (string.IsNullOrEmpty(hostText))
        {
            // The spec allows empty values
            return true;
        }
 
        var firstChar = hostText[0];
        if (firstChar == '[')
        {
            // Tail call
            return IsIPv6HostValid(hostText);
        }
        else
        {
            if (firstChar == ':')
            {
                // Only a port
                return false;
            }
 
            var invalid = HttpCharacters.IndexOfInvalidHostChar(hostText);
            if (invalid >= 0)
            {
                // Tail call
                return IsHostPortValid(hostText, invalid);
            }
 
            return true;
        }
    }
 
    // The lead '[' was already checked
    private static bool IsIPv6HostValid(string hostText)
    {
        for (var i = 1; i < hostText.Length; i++)
        {
            var ch = hostText[i];
            if (ch == ']')
            {
                // [::1] is the shortest valid IPv6 host
                if (i < 4)
                {
                    return false;
                }
                else if (i + 1 < hostText.Length)
                {
                    // Tail call
                    return IsHostPortValid(hostText, i + 1);
                }
                return true;
            }
 
            if (!IsHex(ch) && ch != ':' && ch != '.')
            {
                return false;
            }
        }
 
        // Must contain a ']'
        return false;
    }
 
    private static bool IsHostPortValid(string hostText, int offset)
    {
        var firstChar = hostText[offset];
        offset++;
        if (firstChar != ':' || offset == hostText.Length)
        {
            // Must have at least one number after the colon if present.
            return false;
        }
 
        for (var i = offset; i < hostText.Length; i++)
        {
            if (!IsNumeric(hostText[i]))
            {
                return false;
            }
        }
 
        return true;
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsNumeric(char ch)
    {
        // '0' <= ch && ch <= '9'
        // (uint)(ch - '0') <= (uint)('9' - '0')
 
        // Subtract start of range '0'
        // Cast to uint to change negative numbers to large numbers
        // Check if less than 10 representing chars '0' - '9'
        return (uint)(ch - '0') < 10u;
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsHex(char ch)
    {
        return IsNumeric(ch)
            // || ('a' <= ch && ch <= 'f')
            // || ('A' <= ch && ch <= 'F');
 
            // Lowercase indiscriminately (or with 32)
            // Subtract start of range 'a'
            // Cast to uint to change negative numbers to large numbers
            // Check if less than 6 representing chars 'a' - 'f'
            || (uint)((ch | 32) - 'a') < 6u;
    }
 
    public static AltSvcHeader? GetEndpointAltSvc(System.Net.IPEndPoint endpoint, HttpProtocols protocols)
    {
        var hasHttp1OrHttp2 = protocols.HasFlag(HttpProtocols.Http1) || protocols.HasFlag(HttpProtocols.Http2);
        var hasHttp3 = protocols.HasFlag(HttpProtocols.Http3);
 
        if (hasHttp1OrHttp2 && hasHttp3)
        {
            // 86400 is a cache of 24 hours.
            // This is the default cache if none is specified with Alt-Svc, but it appears that all
            // popular HTTP/3 websites explicitly specifies a cache duration in the header.
            // Specify a value to be consistent.
            var text = "h3=\":" + endpoint.Port.ToString(CultureInfo.InvariantCulture) + "\"; ma=86400";
            var bytes = Encoding.ASCII.GetBytes($"\r\nAlt-Svc: " + text);
            return new AltSvcHeader(text, bytes);
        }
 
        return null;
    }
}
 
internal sealed class AltSvcHeader
{
    public string Value { get; }
    
    public byte[] RawBytes { get; }
 
    public AltSvcHeader(string value, byte[] rawBytes)
    {
        Value = value;
        RawBytes = rawBytes;
    }
}