< Summary

Information
Class: System.Net.Http.HttpRuleParser
Assembly: System.Net.Http
File(s): D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\HttpRuleParser.cs
Line coverage
92%
Covered lines: 146
Uncovered lines: 12
Coverable lines: 158
Total lines: 327
Line coverage: 92.4%
Branch coverage
92%
Covered branches: 78
Total branches: 84
Branch coverage: 92.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%11100%
GetTokenLength(...)100%22100%
IsToken(...)100%11100%
IsToken(...)100%110%
GetTokenString(...)100%110%
GetWhitespaceLength(...)100%88100%
ContainsNewLineOrNull(...)100%11100%
GetNumberLength(...)91.66%1212100%
GetHostLength(...)92.85%141491.3%
GetCommentLength(...)100%11100%
GetQuotedStringLength(...)100%11100%
GetQuotedPairLength(...)93.75%1616100%
GetExpressionLength(...)89.28%282890.69%
IsValidHostName(...)100%44100%

File(s)

D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\HttpRuleParser.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Diagnostics;
 6using System.Text;
 7
 8namespace System.Net.Http
 9{
 10    internal static class HttpRuleParser
 11    {
 12        // token = 1*<any CHAR except CTLs or separators>
 13        // CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
 114        private static readonly SearchValues<char> s_tokenChars =
 115            SearchValues.Create("!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~");
 16
 117        private static readonly SearchValues<byte> s_tokenBytes =
 118            SearchValues.Create("!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"u8);
 19
 120        private static readonly SearchValues<char> s_hostDelimiterChars =
 121            SearchValues.Create("/ \t\r,");
 22
 23        // Characters such as '?' or '#' are interpreted as an end of the host part of the URI, so they will not be vali
 124        private static readonly SearchValues<char> s_disallowedHostChars =
 125            SearchValues.Create("/\\?#@");
 26
 27        private const int MaxNestedCount = 5;
 28
 29        internal const char CR = (char)13;
 30        internal const char LF = (char)10;
 31        internal const int MaxInt64Digits = 19;
 32        internal const int MaxInt32Digits = 10;
 33
 034        internal static Encoding DefaultHttpEncoding => Encoding.Latin1;
 35
 36        internal static int GetTokenLength(string input, int startIndex)
 123838037        {
 123838038            Debug.Assert(input is not null);
 39
 123838040            ReadOnlySpan<char> slice = input.AsSpan(startIndex);
 41
 123838042            int index = slice.IndexOfAnyExcept(s_tokenChars);
 43
 123838044            return index < 0 ? slice.Length : index;
 123838045        }
 46
 47        internal static bool IsToken(ReadOnlySpan<char> input) =>
 30772848            !input.ContainsAnyExcept(s_tokenChars);
 49
 50        internal static bool IsToken(ReadOnlySpan<byte> input) =>
 051            !input.ContainsAnyExcept(s_tokenBytes);
 52
 53        internal static string GetTokenString(ReadOnlySpan<byte> input)
 054        {
 055            Debug.Assert(IsToken(input));
 56
 057            return Encoding.ASCII.GetString(input);
 058        }
 59
 60        internal static int GetWhitespaceLength(string input, int startIndex)
 593821861        {
 593821862            Debug.Assert(input != null);
 63
 593821864            if (startIndex >= input.Length)
 5993465            {
 5993466                return 0;
 67            }
 68
 587828469            int current = startIndex;
 70
 71            char c;
 604477172            while (current < input.Length)
 604444173            {
 604444174                c = input[current];
 75
 604444176                if ((c == ' ') || (c == '\t'))
 16648777                {
 16648778                    current++;
 16648779                    continue;
 80                }
 81
 587795482                return current - startIndex;
 83            }
 84
 85            // All characters between startIndex and the end of the string are LWS characters.
 33086            return input.Length - startIndex;
 593821887        }
 88
 89        // See https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5:
 90        // "Field values containing CR, LF, or NUL characters are invalid and dangerous"
 91        internal static bool ContainsNewLineOrNull(string value, int startIndex = 0) =>
 25100392            value.AsSpan(startIndex).ContainsAny('\r', '\n', '\0');
 93
 94        internal static int GetNumberLength(string input, int startIndex, bool allowDecimal)
 11817995        {
 11817996            Debug.Assert(input != null);
 11817997            Debug.Assert((startIndex >= 0) && (startIndex < input.Length));
 98
 11817999            int current = startIndex;
 100            char c;
 101
 102            // If decimal values are not allowed, we pretend to have read the '.' character already. I.e. if a dot is
 103            // found in the string, parsing will be aborted.
 118179104            bool haveDot = !allowDecimal;
 105
 106            // The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the
 107            // form "0.123". Also, there are no negative values defined in the RFC. So we'll just parse non-negative
 108            // values.
 109            // The RFC only allows decimal dots not ',' characters as decimal separators. Therefore value "1,23" is
 110            // considered invalid and must be represented as "1.23".
 118179111            if (input[current] == '.')
 10112            {
 10113                return 0;
 114            }
 115
 205149116            while (current < input.Length)
 204762117            {
 204762118                c = input[current];
 204762119                if (char.IsAsciiDigit(c))
 86459120                {
 86459121                    current++;
 86459122                }
 118303123                else if (!haveDot && (c == '.'))
 521124                {
 125                    // Note that value "1." is valid.
 521126                    haveDot = true;
 521127                    current++;
 521128                }
 129                else
 117782130                {
 117782131                    break;
 132                }
 86980133            }
 134
 118169135            return current - startIndex;
 118179136        }
 137
 138        internal static int GetHostLength(string input, int startIndex, bool allowToken)
 109792139        {
 109792140            Debug.Assert(input != null);
 109792141            Debug.Assert(startIndex >= 0);
 142
 109792143            if (startIndex >= input.Length)
 0144            {
 0145                return 0;
 146            }
 147
 109792148            ReadOnlySpan<char> slice = input.AsSpan(startIndex);
 149
 150            // A 'host' is either a token (if 'allowToken' == true) or a valid host name as defined by the URI RFC.
 151            // So we first iterate through the string and search for path delimiters and whitespace. When found, stop
 152            // and try to use the substring as token or URI host name. If it works, we have a host name, otherwise not.
 109792153            int index = slice.IndexOfAny(s_hostDelimiterChars);
 109792154            if (index >= 0)
 54138155            {
 54138156                if (index == 0)
 34157                {
 34158                    return 0;
 159                }
 160
 54104161                if (slice[index] == '/')
 72162                {
 72163                    return 0; // Host header must not contain paths.
 164                }
 165
 54032166                slice = slice.Slice(0, index);
 54032167            }
 168
 109686169            if ((allowToken && IsToken(slice)) || IsValidHostName(slice))
 109106170            {
 109106171                return slice.Length;
 172            }
 173
 580174            return 0;
 109792175        }
 176
 177        internal static HttpParseResult GetCommentLength(string input, int startIndex, out int length)
 4772178        {
 4772179            return GetExpressionLength(input, startIndex, '(', ')', true, 1, out length);
 4772180        }
 181
 182        internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length)
 61485183        {
 61485184            return GetExpressionLength(input, startIndex, '"', '"', false, 1, out length);
 61485185        }
 186
 187        // quoted-pair = "\" CHAR
 188        // CHAR = <any US-ASCII character (octets 0 - 127)>
 189        internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length)
 1761283190        {
 1761283191            Debug.Assert(input != null);
 1761283192            Debug.Assert((startIndex >= 0) && (startIndex < input.Length));
 193
 1761283194            length = 0;
 195
 1761283196            if (input[startIndex] != '\\')
 1760069197            {
 1760069198                return HttpParseResult.NotParsed;
 199            }
 200
 201            // Quoted-char has 2 characters. Check whether there are 2 chars left ('\' + char)
 202            // If so, check whether the character is in the range 0-127 and not a new line. Otherwise, it's an invalid v
 1214203            if ((startIndex + 2 > input.Length) || (input[startIndex + 1] is > (char)127 or '\r' or '\n' or '\0'))
 897204            {
 897205                return HttpParseResult.InvalidFormat;
 206            }
 207
 208            // It doesn't matter what the char next to '\' is so we can skip along.
 317209            length = 2;
 317210            return HttpParseResult.Parsed;
 1761283211        }
 212
 213        // TEXT = <any OCTET except CTLs, but including LWS>
 214        // LWS = SP | HT
 215        // CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
 216        //
 217        // Since we don't really care about the content of a quoted string or comment, we're more tolerant and
 218        // allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment).
 219        //
 220        // 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like
 221        // "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested
 222        // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any)
 223        // is unusual.
 224        private static HttpParseResult GetExpressionLength(string input, int startIndex, char openChar,
 225            char closeChar, bool supportsNesting, int nestedCount, out int length)
 72181226        {
 72181227            Debug.Assert(input != null);
 72181228            Debug.Assert((startIndex >= 0) && (startIndex < input.Length));
 229
 72181230            length = 0;
 231
 72181232            if (input[startIndex] != openChar)
 346233            {
 346234                return HttpParseResult.NotParsed;
 235            }
 236
 71835237            int current = startIndex + 1; // Start parsing with the character next to the first open-char.
 1825091238            while (current < input.Length)
 1824803239            {
 240                // Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e.
 241                // quoted char + closing char). Otherwise the closing char may be considered part of the quoted char.
 242                int quotedPairLength;
 1824803243                if ((current + 2 < input.Length) &&
 1824803244                    (GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed))
 317245                {
 246                    // We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair,
 247                    // but we actually have a quoted-string: e.g. "\\u00FC" ('\' followed by a char >127 - quoted-pair o
 248                    // allows ASCII chars after '\'; qdtext allows both '\' and >127 chars).
 317249                    current += quotedPairLength;
 317250                    continue;
 251                }
 252
 1824486253                char c = input[current];
 254
 1824486255                if (c == '\r' || c == '\n' || c == '\0')
 3642256                {
 3642257                    return HttpParseResult.InvalidFormat;
 258                }
 259
 260                // If we support nested expressions and we find an open-char, then parse the nested expressions.
 1820844261                if (supportsNesting && (c == openChar))
 6112262                {
 263                    // Check if we exceeded the number of nested calls.
 6112264                    if (nestedCount > MaxNestedCount)
 188265                    {
 188266                        return HttpParseResult.InvalidFormat;
 267                    }
 268
 269                    int nestedLength;
 5924270                    HttpParseResult nestedResult = GetExpressionLength(input, current, openChar, closeChar,
 5924271                        supportsNesting, nestedCount + 1, out nestedLength);
 272
 5924273                    switch (nestedResult)
 274                    {
 275                        case HttpParseResult.Parsed:
 4208276                            current += nestedLength; // Add the length of the nested expression and continue.
 4208277                            break;
 278
 279                        case HttpParseResult.NotParsed:
 0280                            Debug.Fail("'NotParsed' is unexpected: We started nested expression " +
 0281                                "parsing, because we found the open-char. So either it's a valid nested " +
 0282                                "expression or it has invalid format.");
 283                            break;
 284
 285                        case HttpParseResult.InvalidFormat:
 286                            // If the nested expression is invalid, we can't continue, so we fail with invalid format.
 1716287                            return HttpParseResult.InvalidFormat;
 288
 289                        default:
 0290                            Debug.Fail("Unknown enum result: " + nestedResult);
 291                            break;
 292                    }
 293
 294                    // after nested call we continue with parsing
 4208295                    continue;
 296                }
 297
 1814732298                if (input[current] == closeChar)
 66001299                {
 66001300                    length = current - startIndex + 1;
 66001301                    return HttpParseResult.Parsed;
 302                }
 1748731303                current++;
 1748731304            }
 305
 306            // We didn't find the final quote, therefore we have an invalid expression string.
 288307            return HttpParseResult.InvalidFormat;
 72181308        }
 309
 310        private static bool IsValidHostName(ReadOnlySpan<char> host)
 103038311        {
 103038312            if (host.ContainsAny(s_disallowedHostChars))
 198313            {
 198314                return false;
 315            }
 316
 317            // Using a trailing slash as Uri ignores trailing whitespace otherwise.
 102840318            if (!Uri.TryCreate($"http://{host}/", UriKind.Absolute, out _))
 382319            {
 382320                return false;
 321            }
 322
 102458323            Debug.Assert(!ContainsNewLineOrNull(host.ToString()));
 102458324            return true;
 103038325        }
 326    }
 327}