| | | 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 | | |
| | | 4 | | using System.Diagnostics; |
| | | 5 | | using System.Diagnostics.CodeAnalysis; |
| | | 6 | | |
| | | 7 | | namespace System.Net.Http.Headers |
| | | 8 | | { |
| | | 9 | | public class AuthenticationHeaderValue : ICloneable |
| | | 10 | | { |
| | | 11 | | private readonly string _scheme; |
| | | 12 | | private readonly string? _parameter; |
| | | 13 | | |
| | | 14 | | public string Scheme |
| | | 15 | | { |
| | 0 | 16 | | get { return _scheme; } |
| | | 17 | | } |
| | | 18 | | |
| | | 19 | | // We simplify parameters by just considering them one string. The caller is responsible for correctly parsing |
| | | 20 | | // the string. |
| | | 21 | | // The reason is that we can't determine the format of parameters. According to Errata 1959 in RFC 2617 |
| | | 22 | | // parameters can be "token", "quoted-string", or "#auth-param" where "auth-param" is defined as |
| | | 23 | | // "token "=" ( token | quoted-string )". E.g. take the following BASIC example: |
| | | 24 | | // Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
| | | 25 | | // Due to Base64 encoding we have two final "=". The value is neither a token nor a quoted-string, so it must |
| | | 26 | | // be an auth-param according to the RFC definition. But that's also incorrect: auth-param means that we |
| | | 27 | | // consider the value before the first "=" as "name" and the final "=" as "value". |
| | | 28 | | public string? Parameter |
| | | 29 | | { |
| | 0 | 30 | | get { return _parameter; } |
| | | 31 | | } |
| | | 32 | | |
| | | 33 | | public AuthenticationHeaderValue(string scheme) |
| | 99518 | 34 | | : this(scheme, null) |
| | 99518 | 35 | | { |
| | 99518 | 36 | | } |
| | | 37 | | |
| | 109525 | 38 | | public AuthenticationHeaderValue(string scheme, string? parameter) |
| | 109525 | 39 | | { |
| | 109525 | 40 | | HeaderUtilities.CheckValidToken(scheme); |
| | 109525 | 41 | | HttpHeaders.CheckContainsNewLineOrNull(parameter); |
| | 109525 | 42 | | _scheme = scheme; |
| | 109525 | 43 | | _parameter = parameter; |
| | 109525 | 44 | | } |
| | | 45 | | |
| | 0 | 46 | | private AuthenticationHeaderValue(AuthenticationHeaderValue source) |
| | 0 | 47 | | { |
| | 0 | 48 | | Debug.Assert(source != null); |
| | | 49 | | |
| | 0 | 50 | | _scheme = source._scheme; |
| | 0 | 51 | | _parameter = source._parameter; |
| | 0 | 52 | | } |
| | | 53 | | |
| | | 54 | | public override string ToString() |
| | 178940 | 55 | | { |
| | 178940 | 56 | | if (string.IsNullOrEmpty(_parameter)) |
| | 162588 | 57 | | { |
| | 162588 | 58 | | return _scheme; |
| | | 59 | | } |
| | 16352 | 60 | | return _scheme + " " + _parameter; |
| | 178940 | 61 | | } |
| | | 62 | | |
| | | 63 | | public override bool Equals([NotNullWhen(true)] object? obj) |
| | 0 | 64 | | { |
| | 0 | 65 | | AuthenticationHeaderValue? other = obj as AuthenticationHeaderValue; |
| | | 66 | | |
| | 0 | 67 | | if (other == null) |
| | 0 | 68 | | { |
| | 0 | 69 | | return false; |
| | | 70 | | } |
| | | 71 | | |
| | 0 | 72 | | if (string.IsNullOrEmpty(_parameter) && string.IsNullOrEmpty(other._parameter)) |
| | 0 | 73 | | { |
| | 0 | 74 | | return (string.Equals(_scheme, other._scheme, StringComparison.OrdinalIgnoreCase)); |
| | | 75 | | } |
| | | 76 | | else |
| | 0 | 77 | | { |
| | | 78 | | // Since we can't parse the parameter, we use case-sensitive comparison. |
| | 0 | 79 | | return string.Equals(_scheme, other._scheme, StringComparison.OrdinalIgnoreCase) && |
| | 0 | 80 | | string.Equals(_parameter, other._parameter, StringComparison.Ordinal); |
| | | 81 | | } |
| | 0 | 82 | | } |
| | | 83 | | |
| | | 84 | | public override int GetHashCode() |
| | 0 | 85 | | { |
| | 0 | 86 | | int result = StringComparer.OrdinalIgnoreCase.GetHashCode(_scheme); |
| | | 87 | | |
| | 0 | 88 | | if (!string.IsNullOrEmpty(_parameter)) |
| | 0 | 89 | | { |
| | 0 | 90 | | result ^= _parameter.GetHashCode(); |
| | 0 | 91 | | } |
| | | 92 | | |
| | 0 | 93 | | return result; |
| | 0 | 94 | | } |
| | | 95 | | |
| | | 96 | | public static AuthenticationHeaderValue Parse(string input) |
| | 0 | 97 | | { |
| | 0 | 98 | | int index = 0; |
| | 0 | 99 | | return (AuthenticationHeaderValue)GenericHeaderParser.SingleValueAuthenticationParser.ParseValue( |
| | 0 | 100 | | input, null, ref index); |
| | 0 | 101 | | } |
| | | 102 | | |
| | | 103 | | public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out AuthenticationHeaderValue |
| | 0 | 104 | | { |
| | 0 | 105 | | int index = 0; |
| | 0 | 106 | | parsedValue = null; |
| | | 107 | | |
| | 0 | 108 | | if (GenericHeaderParser.SingleValueAuthenticationParser.TryParseValue(input, null, ref index, out object? ou |
| | 0 | 109 | | { |
| | 0 | 110 | | parsedValue = (AuthenticationHeaderValue)output!; |
| | 0 | 111 | | return true; |
| | | 112 | | } |
| | 0 | 113 | | return false; |
| | 0 | 114 | | } |
| | | 115 | | |
| | | 116 | | internal static int GetAuthenticationLength(string? input, int startIndex, out object? parsedValue) |
| | 113839 | 117 | | { |
| | 113839 | 118 | | Debug.Assert(startIndex >= 0); |
| | | 119 | | |
| | 113839 | 120 | | parsedValue = null; |
| | | 121 | | |
| | 113839 | 122 | | if (string.IsNullOrEmpty(input) || (startIndex >= input.Length) || HttpRuleParser.ContainsNewLineOrNull(inpu |
| | 4034 | 123 | | { |
| | 4034 | 124 | | return 0; |
| | | 125 | | } |
| | | 126 | | |
| | | 127 | | // Parse the scheme string: <scheme> in '<scheme> <parameter>' |
| | 109805 | 128 | | int schemeLength = HttpRuleParser.GetTokenLength(input, startIndex); |
| | | 129 | | |
| | 109805 | 130 | | if (schemeLength == 0) |
| | 58 | 131 | | { |
| | 58 | 132 | | return 0; |
| | | 133 | | } |
| | | 134 | | |
| | 109747 | 135 | | string? targetScheme = null; |
| | 109747 | 136 | | switch (schemeLength) |
| | | 137 | | { |
| | | 138 | | // Avoid allocating a scheme string for the most common cases. |
| | 75464 | 139 | | case 5: targetScheme = "Basic"; break; |
| | 20130 | 140 | | case 6: targetScheme = "Digest"; break; |
| | 12302 | 141 | | case 4: targetScheme = "NTLM"; break; |
| | 21678 | 142 | | case 9: targetScheme = "Negotiate"; break; |
| | | 143 | | } |
| | | 144 | | |
| | 109747 | 145 | | string scheme = targetScheme != null && string.CompareOrdinal(input, startIndex, targetScheme, 0, schemeLeng |
| | 109747 | 146 | | targetScheme : |
| | 109747 | 147 | | input.Substring(startIndex, schemeLength); |
| | | 148 | | |
| | 109747 | 149 | | int current = startIndex + schemeLength; |
| | 109747 | 150 | | int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current); |
| | 109747 | 151 | | current += whitespaceLength; |
| | | 152 | | |
| | 109747 | 153 | | if ((current == input.Length) || (input[current] == ',')) |
| | 99518 | 154 | | { |
| | | 155 | | // If we only have a scheme followed by whitespace, we're done. |
| | 99518 | 156 | | parsedValue = new AuthenticationHeaderValue(scheme); |
| | 99518 | 157 | | return current - startIndex; |
| | | 158 | | } |
| | | 159 | | |
| | | 160 | | // We need at least one space between the scheme and parameters. If there is no whitespace, then we must |
| | | 161 | | // have reached the end of the string (i.e. scheme-only string). |
| | 10229 | 162 | | if (whitespaceLength == 0) |
| | 146 | 163 | | { |
| | 146 | 164 | | return 0; |
| | | 165 | | } |
| | | 166 | | |
| | | 167 | | // If we get here, we have a <scheme> followed by a whitespace. Now we expect the following: |
| | | 168 | | // '<scheme> <blob>[,<name>=<value>]*[, <otherscheme>...]*': <blob> potentially contains one |
| | | 169 | | // or more '=' characters, optionally followed by additional name/value pairs, optionally followed by |
| | | 170 | | // other schemes. <blob> may be a quoted string. |
| | | 171 | | // We look at the value after ',': if it is <token>=<value> then we have a parameter for <scheme>. |
| | | 172 | | // If we have either a <token>-only or <token><whitespace><blob> then we have another scheme. |
| | 10083 | 173 | | int parameterStartIndex = current; |
| | 10083 | 174 | | int parameterEndIndex = current; |
| | 10083 | 175 | | if (!TrySkipFirstBlob(input, ref current, ref parameterEndIndex)) |
| | 18 | 176 | | { |
| | 18 | 177 | | return 0; |
| | | 178 | | } |
| | | 179 | | |
| | 10065 | 180 | | if (current < input.Length) |
| | 8705 | 181 | | { |
| | 8705 | 182 | | if (!TryGetParametersEndIndex(input, ref current, ref parameterEndIndex)) |
| | 58 | 183 | | { |
| | 58 | 184 | | return 0; |
| | | 185 | | } |
| | 8647 | 186 | | } |
| | | 187 | | |
| | 10007 | 188 | | string parameter = input.Substring(parameterStartIndex, parameterEndIndex - parameterStartIndex + 1); |
| | 10007 | 189 | | parsedValue = new AuthenticationHeaderValue(scheme, parameter); |
| | 10007 | 190 | | return current - startIndex; |
| | 113839 | 191 | | } |
| | | 192 | | |
| | | 193 | | private static bool TrySkipFirstBlob(string input, ref int current, ref int parameterEndIndex) |
| | 10083 | 194 | | { |
| | | 195 | | // Find the delimiter: Note that <blob> in "<scheme> <blob>" may be a token, quoted string, name/value |
| | | 196 | | // pair or a Base64 encoded string. So make sure that we don't consider ',' characters within a quoted |
| | | 197 | | // string as delimiter. |
| | 92806 | 198 | | while ((current < input.Length) && (input[current] != ',')) |
| | 82741 | 199 | | { |
| | 82741 | 200 | | if (input[current] == '"') |
| | 3034 | 201 | | { |
| | | 202 | | int quotedStringLength; |
| | 3034 | 203 | | if (HttpRuleParser.GetQuotedStringLength(input, current, out quotedStringLength) != |
| | 3034 | 204 | | HttpParseResult.Parsed) |
| | 18 | 205 | | { |
| | | 206 | | // We have a quote but an invalid quoted-string. |
| | 18 | 207 | | return false; |
| | | 208 | | } |
| | 3016 | 209 | | current += quotedStringLength; |
| | 3016 | 210 | | parameterEndIndex = current - 1; // -1 because 'current' points to the char after the final '"' |
| | 3016 | 211 | | } |
| | | 212 | | else |
| | 79707 | 213 | | { |
| | 79707 | 214 | | int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current); |
| | | 215 | | |
| | | 216 | | // We don't want trailing whitespace to be considered part of the parameter blob. Increment |
| | | 217 | | // 'parameterEndIndex' only if we don't have a whitespace. E.g. "Basic AbC= , NTLM" should return |
| | | 218 | | // "AbC=" as parameter ignoring the spaces before ','. |
| | 79707 | 219 | | if (whitespaceLength == 0) |
| | 68670 | 220 | | { |
| | 68670 | 221 | | parameterEndIndex = current; |
| | 68670 | 222 | | current++; |
| | 68670 | 223 | | } |
| | | 224 | | else |
| | 11037 | 225 | | { |
| | 11037 | 226 | | current += whitespaceLength; |
| | 11037 | 227 | | } |
| | 79707 | 228 | | } |
| | 82723 | 229 | | } |
| | | 230 | | |
| | 10065 | 231 | | return true; |
| | 10083 | 232 | | } |
| | | 233 | | |
| | | 234 | | private static bool TryGetParametersEndIndex(string input, ref int parseEndIndex, ref int parameterEndIndex) |
| | 8705 | 235 | | { |
| | 8705 | 236 | | Debug.Assert(parseEndIndex < input.Length, "Expected string to have at least 1 char"); |
| | 8705 | 237 | | Debug.Assert(input[parseEndIndex] == ','); |
| | | 238 | | |
| | 8705 | 239 | | int current = parseEndIndex; |
| | | 240 | | do |
| | 9518 | 241 | | { |
| | 9518 | 242 | | current++; // skip ',' delimiter |
| | | 243 | | |
| | 9518 | 244 | | current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, current, true, out _); |
| | 9518 | 245 | | if (current == input.Length) |
| | 300 | 246 | | { |
| | 300 | 247 | | return true; |
| | | 248 | | } |
| | | 249 | | |
| | | 250 | | // Now we have to determine if after ',' we have a list of <name>=<value> pairs that are part of |
| | | 251 | | // the auth scheme parameters OR if we have another auth scheme. Either way, after ',' we expect a |
| | | 252 | | // valid token that is either the <name> in a <name>=<value> pair OR <scheme> of another scheme. |
| | 9218 | 253 | | int tokenLength = HttpRuleParser.GetTokenLength(input, current); |
| | 9218 | 254 | | if (tokenLength == 0) |
| | 28 | 255 | | { |
| | 28 | 256 | | return false; |
| | | 257 | | } |
| | | 258 | | |
| | 9190 | 259 | | current += tokenLength; |
| | 9190 | 260 | | current += HttpRuleParser.GetWhitespaceLength(input, current); |
| | | 261 | | |
| | | 262 | | // If we reached the end of the string or the token is followed by anything but '=', then the parsed |
| | | 263 | | // token is another scheme name. The string representing parameters ends before the token (e.g. |
| | | 264 | | // "Digest a=b, c=d, NTLM": return scheme "Digest" with parameters string "a=b, c=d"). |
| | 9190 | 265 | | if ((current == input.Length) || (input[current] != '=')) |
| | 8240 | 266 | | { |
| | 8240 | 267 | | return true; |
| | | 268 | | } |
| | | 269 | | |
| | 950 | 270 | | current++; // skip '=' delimiter |
| | 950 | 271 | | current += HttpRuleParser.GetWhitespaceLength(input, current); |
| | 950 | 272 | | int valueLength = NameValueHeaderValue.GetValueLength(input, current); |
| | | 273 | | |
| | | 274 | | // After '<name>=' we expect a valid <value> (either token or quoted string) |
| | 950 | 275 | | if (valueLength == 0) |
| | 30 | 276 | | { |
| | 30 | 277 | | return false; |
| | | 278 | | } |
| | | 279 | | |
| | | 280 | | // Update parameter end index, since we just parsed a valid <name>=<value> pair that is part of the |
| | | 281 | | // parameters string. |
| | 920 | 282 | | current += valueLength; |
| | 920 | 283 | | parameterEndIndex = current - 1; // -1 because 'current' already points to the char after <value> |
| | 920 | 284 | | current += HttpRuleParser.GetWhitespaceLength(input, current); |
| | 920 | 285 | | parseEndIndex = current; // this essentially points to parameterEndIndex + whitespace + next char |
| | 1840 | 286 | | } while ((current < input.Length) && (input[current] == ',')); |
| | | 287 | | |
| | 107 | 288 | | return true; |
| | 8705 | 289 | | } |
| | | 290 | | |
| | | 291 | | object ICloneable.Clone() |
| | 0 | 292 | | { |
| | 0 | 293 | | return new AuthenticationHeaderValue(this); |
| | 0 | 294 | | } |
| | | 295 | | } |
| | | 296 | | } |