< Summary

Information
Class: System.Net.Http.Headers.AltSvcHeaderParser
Assembly: System.Net.Http
File(s): D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\Headers\AltSvcHeaderParser.cs
Line coverage
33%
Covered lines: 101
Uncovered lines: 198
Coverable lines: 299
Total lines: 476
Line coverage: 33.7%
Branch coverage
33%
Covered branches: 48
Total branches: 145
Branch coverage: 33.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\Headers\AltSvcHeaderParser.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.Diagnostics;
 5using System.Diagnostics.CodeAnalysis;
 6using System.Text;
 7
 8namespace System.Net.Http.Headers
 9{
 10    /// <summary>
 11    /// Parses Alt-Svc header values, per RFC 7838 Section 3.
 12    /// </summary>
 13    internal sealed class AltSvcHeaderParser : BaseHeaderParser
 14    {
 15        internal const long DefaultMaxAgeTicks = 24 * TimeSpan.TicksPerHour;
 16
 217        public static AltSvcHeaderParser Parser { get; } = new AltSvcHeaderParser();
 18
 19        private AltSvcHeaderParser()
 120            : base(supportsMultipleValues: true)
 121        {
 122        }
 23
 24        protected override int GetParsedValueLength(string value, int startIndex, object? storeValue,
 25            out object? parsedValue)
 67226        {
 67227            Debug.Assert(startIndex >= 0);
 67228            Debug.Assert(startIndex < value.Length);
 29
 67230            if (string.IsNullOrEmpty(value))
 031            {
 032                parsedValue = null;
 033                return 0;
 34            }
 35
 67236            int idx = startIndex;
 37
 67238            if (!TryReadPercentEncodedAlpnProtocolName(value, idx, out string? alpnProtocolName, out int alpnProtocolNam
 33039            {
 33040                parsedValue = null;
 33041                return 0;
 42            }
 43
 34244            idx += alpnProtocolNameLength;
 45
 34246            if (alpnProtocolName == AltSvcHeaderValue.ClearString)
 047            {
 048                if (idx != value.Length)
 049                {
 50                    // Clear has no parameters and should be the only Alt-Svc value present, so there should be nothing 
 051                    parsedValue = null;
 052                    return 0;
 53                }
 54
 055                parsedValue = AltSvcHeaderValue.Clear;
 056                return idx - startIndex;
 57            }
 58
 59            // Make sure we have at least 2 characters and first one being an '='.
 34260            if (idx + 1 >= value.Length || value[idx++] != '=')
 30661            {
 30662                parsedValue = null;
 30663                return 0;
 64            }
 65
 3666            if (!TryReadQuotedAltAuthority(value, idx, out string? altAuthorityHost, out int altAuthorityPort, out int a
 3667            {
 3668                parsedValue = null;
 3669                return 0;
 70            }
 071            idx += altAuthorityLength;
 72
 73            // Parse parameters: *( OWS ";" OWS parameter )
 074            int? maxAge = null;
 075            bool persist = false;
 76
 077            while (idx < value.Length)
 078            {
 79                // Skip OWS before semicolon.
 080                while (idx < value.Length && IsOptionalWhiteSpace(value[idx])) ++idx;
 81
 082                if (idx == value.Length)
 083                {
 084                    parsedValue = null;
 085                    return 0;
 86                }
 87
 088                char ch = value[idx];
 89
 090                if (ch == ',')
 091                {
 92                    // Multi-value header: return this value; will get called again to parse the next.
 093                    break;
 94                }
 95
 096                if (ch != ';')
 097                {
 98                    // Expecting parameters starting with semicolon; fail out.
 099                    parsedValue = null;
 0100                    return 0;
 101                }
 102
 0103                ++idx;
 104
 105                // Skip OWS after semicolon / before value.
 0106                while (idx < value.Length && IsOptionalWhiteSpace(value[idx])) ++idx;
 107
 108                // Get the parameter key length.
 0109                int tokenLength = HttpRuleParser.GetTokenLength(value, idx);
 0110                if (tokenLength == 0)
 0111                {
 0112                    parsedValue = null;
 0113                    return 0;
 114                }
 115
 0116                if ((idx + tokenLength) >= value.Length || value[idx + tokenLength] != '=')
 0117                {
 0118                    parsedValue = null;
 0119                    return 0;
 120                }
 121
 0122                if (tokenLength == 2 && value[idx] == 'm' && value[idx + 1] == 'a')
 0123                {
 124                    // Parse "ma" (Max Age).
 125
 0126                    idx += 3; // Skip "ma="
 0127                    if (!TryReadTokenOrQuotedInt32(value, idx, out int maxAgeTmp, out int parameterLength))
 0128                    {
 0129                        parsedValue = null;
 0130                        return 0;
 131                    }
 132
 0133                    if (maxAge == null)
 0134                    {
 0135                        maxAge = maxAgeTmp;
 0136                    }
 137                    else
 0138                    {
 139                        // RFC makes it unclear what to do if a duplicate parameter is found. For now, take the minimum.
 0140                        maxAge = Math.Min(maxAge.GetValueOrDefault(), maxAgeTmp);
 0141                    }
 142
 0143                    idx += parameterLength;
 0144                }
 0145                else if (value.AsSpan(idx).StartsWith("persist="))
 0146                {
 0147                    idx += 8; // Skip "persist="
 0148                    if (TryReadTokenOrQuotedInt32(value, idx, out int persistInt, out int parameterLength))
 0149                    {
 0150                        persist = persistInt == 1;
 0151                    }
 0152                    else if (!TrySkipTokenOrQuoted(value, idx, out parameterLength))
 0153                    {
 154                        // Cold path: unsupported value, just skip the parameter.
 0155                        parsedValue = null;
 0156                        return 0;
 157                    }
 158
 0159                    idx += parameterLength;
 0160                }
 161                else
 0162                {
 163                    // Some unknown parameter. Skip it.
 164
 0165                    idx += tokenLength + 1;
 0166                    if (!TrySkipTokenOrQuoted(value, idx, out int parameterLength))
 0167                    {
 0168                        parsedValue = null;
 0169                        return 0;
 170                    }
 0171                    idx += parameterLength;
 0172                }
 0173            }
 174
 175            // If no "ma" parameter present, use the default.
 0176            TimeSpan maxAgeTimeSpan = TimeSpan.FromTicks(maxAge * TimeSpan.TicksPerSecond ?? DefaultMaxAgeTicks);
 177
 0178            parsedValue = new AltSvcHeaderValue(alpnProtocolName, altAuthorityHost, altAuthorityPort, maxAgeTimeSpan, pe
 0179            return idx - startIndex;
 672180        }
 181
 182        private static bool IsOptionalWhiteSpace(char ch)
 0183        {
 0184            return ch == ' ' || ch == '\t';
 0185        }
 186
 187        private static bool TryReadPercentEncodedAlpnProtocolName(string value, int startIndex, [NotNullWhen(true)] out 
 672188        {
 672189            int tokenLength = HttpRuleParser.GetTokenLength(value, startIndex);
 190
 672191            if (tokenLength == 0)
 12192            {
 12193                result = "";
 12194                readLength = 0;
 12195                return true;
 196            }
 197
 660198            ReadOnlySpan<char> span = value.AsSpan(startIndex, tokenLength);
 199
 660200            readLength = tokenLength;
 201
 202            // Special-case expected values to avoid allocating one-off strings.
 660203            switch (span.Length)
 204            {
 84205                case 2 when span is "h3":
 6206                    result = "h3";
 6207                    return true;
 208
 78209                case 2 when span is "h2":
 6210                    result = "h2";
 6211                    return true;
 212
 100213                case 3 when span is "h2c":
 0214                    result = "h2c";
 0215                    readLength = 3;
 0216                    return true;
 217
 12218                case 5 when span is "clear":
 0219                    result = "clear";
 0220                    return true;
 221
 14222                case 10 when span.StartsWith("http%2F1."):
 0223                    char ch = span[9];
 0224                    if (ch == '1')
 0225                    {
 0226                        result = "http/1.1";
 0227                        return true;
 228                    }
 0229                    if (ch == '0')
 0230                    {
 0231                        result = "http/1.0";
 0232                        return true;
 233                    }
 0234                    break;
 235            }
 236
 237            // Unrecognized ALPN protocol name. Percent-decode.
 648238            return TryReadUnknownPercentEncodedAlpnProtocolName(span, out result);
 672239        }
 240
 241        private static bool TryReadUnknownPercentEncodedAlpnProtocolName(ReadOnlySpan<char> value, [NotNullWhen(true)] o
 648242        {
 648243            int idx = value.IndexOf('%');
 244
 648245            if (idx < 0)
 188246            {
 188247                result = new string(value);
 188248                return true;
 249            }
 250
 460251            var builder = value.Length <= 128 ?
 460252                new ValueStringBuilder(stackalloc char[128]) :
 460253                new ValueStringBuilder(value.Length);
 254
 255            do
 2412256            {
 2412257                if (idx != 0)
 1058258                {
 1058259                    builder.Append(value.Slice(0, idx));
 1058260                }
 261
 2412262                if ((value.Length - idx) < 3 || !TryReadAlpnHexDigit(value[idx + 1], out int hi) || !TryReadAlpnHexDigit
 330263                {
 330264                    builder.Dispose();
 330265                    result = null;
 330266                    return false;
 267                }
 268
 2082269                builder.Append((char)((hi << 4) | lo));
 270
 2082271                value = value.Slice(idx + 3);
 2082272                idx = value.IndexOf('%');
 2082273            }
 2082274            while (idx != -1);
 275
 130276            if (value.Length != 0)
 84277            {
 84278                builder.Append(value);
 84279            }
 280
 130281            result = builder.ToString();
 130282            return !HttpRuleParser.ContainsNewLineOrNull(result);
 648283        }
 284
 285        /// <summary>
 286        /// Reads a hex nibble. Specialized for ALPN protocol names as they explicitly can not contain lower-case hex.
 287        /// </summary>
 288        private static bool TryReadAlpnHexDigit(char ch, out int nibble)
 4354289        {
 4354290            int result = HexConverter.FromUpperChar(ch);
 4354291            if (result == 0xFF)
 112292            {
 112293                nibble = 0;
 112294                return false;
 295            }
 296
 4242297            nibble = result;
 4242298            return true;
 4354299        }
 300
 301        private static bool TryReadQuotedAltAuthority(string value, int startIndex, out string? host, out int port, out 
 36302        {
 36303            if (HttpRuleParser.GetQuotedStringLength(value, startIndex, out int quotedLength) != HttpParseResult.Parsed)
 26304            {
 26305                goto parseError;
 306            }
 307
 10308            Debug.Assert(value[startIndex] == '"' && value[startIndex + quotedLength - 1] == '"', $"{nameof(HttpRulePars
 10309            ReadOnlySpan<char> quoted = value.AsSpan(startIndex + 1, quotedLength - 2);
 310
 10311            int idx = quoted.IndexOf(':');
 10312            if (idx == -1)
 10313            {
 10314                goto parseError;
 315            }
 316
 317            // Parse the port. Port comes at the end of the string, but do this first so we don't allocate a host string
 0318            if (!TryReadQuotedInt32Value(quoted.Slice(idx + 1), out port))
 0319            {
 0320                goto parseError;
 321            }
 322
 323            // Parse the optional host.
 0324            if (idx == 0)
 0325            {
 0326                host = null;
 0327            }
 0328            else if (!TryReadQuotedValue(quoted.Slice(0, idx), out host))
 0329            {
 0330                goto parseError;
 331            }
 332
 0333            readLength = quotedLength;
 0334            return true;
 335
 36336        parseError:
 36337            host = null;
 36338            port = 0;
 36339            readLength = 0;
 36340            return false;
 36341        }
 342
 343        private static bool TryReadQuotedValue(ReadOnlySpan<char> value, out string? result)
 0344        {
 0345            int idx = value.IndexOf('\\');
 346
 0347            if (idx == -1)
 0348            {
 349                // Hostnames shouldn't require quoted pairs, so this should be the hot path.
 0350                result = value.Length != 0 ? new string(value) : null;
 0351                return true;
 352            }
 353
 0354            var builder = new ValueStringBuilder(stackalloc char[128]);
 355
 356            do
 0357            {
 0358                if (idx + 1 == value.Length)
 0359                {
 360                    // quoted-pair requires two characters: the quote, and the quoted character.
 0361                    builder.Dispose();
 0362                    result = null;
 0363                    return false;
 364                }
 365
 0366                if (idx != 0)
 0367                {
 0368                    builder.Append(value.Slice(0, idx));
 0369                }
 370
 0371                builder.Append(value[idx + 1]);
 372
 0373                value = value.Slice(idx + 2);
 0374                idx = value.IndexOf('\\');
 0375            }
 0376            while (idx != -1);
 377
 0378            if (value.Length != 0)
 0379            {
 0380                builder.Append(value);
 0381            }
 382
 0383            result = builder.ToString();
 0384            return true;
 0385        }
 386
 387        private static bool TryReadTokenOrQuotedInt32(string value, int startIndex, out int result, out int readLength)
 0388        {
 0389            if (startIndex >= value.Length)
 0390            {
 0391                result = 0;
 0392                readLength = 0;
 0393                return false;
 394            }
 395
 0396            int tokenLength = HttpRuleParser.GetTokenLength(value, startIndex);
 0397            if (tokenLength > 0)
 0398            {
 399                // No reason for integers to be quoted, so this should be the hot path.
 0400                readLength = tokenLength;
 0401                return HeaderUtilities.TryParseInt32(value, startIndex, tokenLength, out result);
 402            }
 403
 0404            if (HttpRuleParser.GetQuotedStringLength(value, startIndex, out int quotedLength) == HttpParseResult.Parsed)
 0405            {
 0406                readLength = quotedLength;
 0407                return TryReadQuotedInt32Value(value.AsSpan(1, quotedLength - 2), out result);
 408            }
 409
 0410            result = 0;
 0411            readLength = 0;
 0412            return false;
 0413        }
 414
 415        private static bool TryReadQuotedInt32Value(ReadOnlySpan<char> value, out int result)
 0416        {
 0417            if (value.Length == 0)
 0418            {
 0419                result = 0;
 0420                return false;
 421            }
 422
 0423            int port = 0;
 424
 0425            foreach (char ch in value)
 0426            {
 427                // The port shouldn't ever need a quoted-pair, but they're still valid... skip if found.
 0428                if (ch == '\\') continue;
 429
 0430                if (!char.IsAsciiDigit(ch))
 0431                {
 0432                    result = 0;
 0433                    return false;
 434                }
 435
 0436                long portTmp = port * 10L + (ch - '0');
 437
 0438                if (portTmp > int.MaxValue)
 0439                {
 0440                    result = 0;
 0441                    return false;
 442                }
 443
 0444                port = (int)portTmp;
 0445            }
 446
 0447            result = port;
 0448            return true;
 0449        }
 450
 451        private static bool TrySkipTokenOrQuoted(string value, int startIndex, out int readLength)
 0452        {
 0453            if (startIndex >= value.Length)
 0454            {
 0455                readLength = 0;
 0456                return false;
 457            }
 458
 0459            int tokenLength = HttpRuleParser.GetTokenLength(value, startIndex);
 0460            if (tokenLength > 0)
 0461            {
 0462                readLength = tokenLength;
 0463                return true;
 464            }
 465
 0466            if (HttpRuleParser.GetQuotedStringLength(value, startIndex, out int quotedLength) == HttpParseResult.Parsed)
 0467            {
 0468                readLength = quotedLength;
 0469                return true;
 470            }
 471
 0472            readLength = 0;
 0473            return false;
 0474        }
 475    }
 476}