< 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
89%
Covered lines: 142
Uncovered lines: 16
Coverable lines: 158
Total lines: 327
Line coverage: 89.8%
Branch coverage
89%
Covered branches: 75
Total branches: 84
Branch coverage: 89.2%
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(...)75%121284%
GetHostLength(...)92.85%141491.3%
GetCommentLength(...)100%11100%
GetQuotedStringLength(...)100%11100%
GetQuotedPairLength(...)87.5%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)
 18248537        {
 18248538            Debug.Assert(input is not null);
 39
 18248540            ReadOnlySpan<char> slice = input.AsSpan(startIndex);
 41
 18248542            int index = slice.IndexOfAnyExcept(s_tokenChars);
 43
 18248544            return index < 0 ? slice.Length : index;
 18248545        }
 46
 47        internal static bool IsToken(ReadOnlySpan<char> input) =>
 9261148            !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)
 104281561        {
 104281562            Debug.Assert(input != null);
 63
 104281564            if (startIndex >= input.Length)
 1582065            {
 1582066                return 0;
 67            }
 68
 102699569            int current = startIndex;
 70
 71            char c;
 105847372            while (current < input.Length)
 105832873            {
 105832874                c = input[current];
 75
 105832876                if ((c == ' ') || (c == '\t'))
 3147877                {
 3147878                    current++;
 3147879                    continue;
 80                }
 81
 102685082                return current - startIndex;
 83            }
 84
 85            // All characters between startIndex and the end of the string are LWS characters.
 14586            return input.Length - startIndex;
 104281587        }
 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) =>
 9151792            value.AsSpan(startIndex).ContainsAny('\r', '\n', '\0');
 93
 94        internal static int GetNumberLength(string input, int startIndex, bool allowDecimal)
 75295        {
 75296            Debug.Assert(input != null);
 75297            Debug.Assert((startIndex >= 0) && (startIndex < input.Length));
 98
 75299            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.
 752104            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".
 752111            if (input[current] == '.')
 6112            {
 6113                return 0;
 114            }
 115
 1934116            while (current < input.Length)
 1848117            {
 1848118                c = input[current];
 1848119                if (char.IsAsciiDigit(c))
 1188120                {
 1188121                    current++;
 1188122                }
 660123                else if (!haveDot && (c == '.'))
 0124                {
 125                    // Note that value "1." is valid.
 0126                    haveDot = true;
 0127                    current++;
 0128                }
 129                else
 660130                {
 660131                    break;
 132                }
 1188133            }
 134
 746135            return current - startIndex;
 752136        }
 137
 138        internal static int GetHostLength(string input, int startIndex, bool allowToken)
 7306139        {
 7306140            Debug.Assert(input != null);
 7306141            Debug.Assert(startIndex >= 0);
 142
 7306143            if (startIndex >= input.Length)
 0144            {
 0145                return 0;
 146            }
 147
 7306148            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.
 7306153            int index = slice.IndexOfAny(s_hostDelimiterChars);
 7306154            if (index >= 0)
 3272155            {
 3272156                if (index == 0)
 46157                {
 46158                    return 0;
 159                }
 160
 3226161                if (slice[index] == '/')
 6162                {
 6163                    return 0; // Host header must not contain paths.
 164                }
 165
 3220166                slice = slice.Slice(0, index);
 3220167            }
 168
 7254169            if ((allowToken && IsToken(slice)) || IsValidHostName(slice))
 7082170            {
 7082171                return slice.Length;
 172            }
 173
 172174            return 0;
 7306175        }
 176
 177        internal static HttpParseResult GetCommentLength(string input, int startIndex, out int length)
 718178        {
 718179            return GetExpressionLength(input, startIndex, '(', ')', true, 1, out length);
 718180        }
 181
 182        internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length)
 4029183        {
 4029184            return GetExpressionLength(input, startIndex, '"', '"', false, 1, out length);
 4029185        }
 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)
 41692190        {
 41692191            Debug.Assert(input != null);
 41692192            Debug.Assert((startIndex >= 0) && (startIndex < input.Length));
 193
 41692194            length = 0;
 195
 41692196            if (input[startIndex] != '\\')
 39249197            {
 39249198                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
 2443203            if ((startIndex + 2 > input.Length) || (input[startIndex + 1] is > (char)127 or '\r' or '\n' or '\0'))
 1025204            {
 1025205                return HttpParseResult.InvalidFormat;
 206            }
 207
 208            // It doesn't matter what the char next to '\' is so we can skip along.
 1418209            length = 2;
 1418210            return HttpParseResult.Parsed;
 41692211        }
 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)
 6063226        {
 6063227            Debug.Assert(input != null);
 6063228            Debug.Assert((startIndex >= 0) && (startIndex < input.Length));
 229
 6063230            length = 0;
 231
 6063232            if (input[startIndex] != openChar)
 312233            {
 312234                return HttpParseResult.NotParsed;
 235            }
 236
 5751237            int current = startIndex + 1; // Start parsing with the character next to the first open-char.
 45430238            while (current < input.Length)
 45100239            {
 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;
 45100243                if ((current + 2 < input.Length) &&
 45100244                    (GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed))
 1418245                {
 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).
 1418249                    current += quotedPairLength;
 1418250                    continue;
 251                }
 252
 43682253                char c = input[current];
 254
 43682255                if (c == '\r' || c == '\n' || c == '\0')
 342256                {
 342257                    return HttpParseResult.InvalidFormat;
 258                }
 259
 260                // If we support nested expressions and we find an open-char, then parse the nested expressions.
 43340261                if (supportsNesting && (c == openChar))
 1370262                {
 263                    // Check if we exceeded the number of nested calls.
 1370264                    if (nestedCount > MaxNestedCount)
 54265                    {
 54266                        return HttpParseResult.InvalidFormat;
 267                    }
 268
 269                    int nestedLength;
 1316270                    HttpParseResult nestedResult = GetExpressionLength(input, current, openChar, closeChar,
 1316271                        supportsNesting, nestedCount + 1, out nestedLength);
 272
 1316273                    switch (nestedResult)
 274                    {
 275                        case HttpParseResult.Parsed:
 664276                            current += nestedLength; // Add the length of the nested expression and continue.
 664277                            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.
 652287                            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
 664295                    continue;
 296                }
 297
 41970298                if (input[current] == closeChar)
 4373299                {
 4373300                    length = current - startIndex + 1;
 4373301                    return HttpParseResult.Parsed;
 302                }
 37597303                current++;
 37597304            }
 305
 306            // We didn't find the final quote, therefore we have an invalid expression string.
 330307            return HttpParseResult.InvalidFormat;
 6063308        }
 309
 310        private static bool IsValidHostName(ReadOnlySpan<char> host)
 6954311        {
 6954312            if (host.ContainsAny(s_disallowedHostChars))
 148313            {
 148314                return false;
 315            }
 316
 317            // Using a trailing slash as Uri ignores trailing whitespace otherwise.
 6806318            if (!Uri.TryCreate($"http://{host}/", UriKind.Absolute, out _))
 24319            {
 24320                return false;
 321            }
 322
 6782323            Debug.Assert(!ContainsNewLineOrNull(host.ToString()));
 6782324            return true;
 6954325        }
 326    }
 327}