|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace System.Net.Http.Headers
{
public class AuthenticationHeaderValue : ICloneable
{
private readonly string _scheme;
private readonly string? _parameter;
public string Scheme
{
get { return _scheme; }
}
// We simplify parameters by just considering them one string. The caller is responsible for correctly parsing
// the string.
// The reason is that we can't determine the format of parameters. According to Errata 1959 in RFC 2617
// parameters can be "token", "quoted-string", or "#auth-param" where "auth-param" is defined as
// "token "=" ( token | quoted-string )". E.g. take the following BASIC example:
// Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
// Due to Base64 encoding we have two final "=". The value is neither a token nor a quoted-string, so it must
// be an auth-param according to the RFC definition. But that's also incorrect: auth-param means that we
// consider the value before the first "=" as "name" and the final "=" as "value".
public string? Parameter
{
get { return _parameter; }
}
public AuthenticationHeaderValue(string scheme)
: this(scheme, null)
{
}
public AuthenticationHeaderValue(string scheme, string? parameter)
{
HeaderUtilities.CheckValidToken(scheme);
HttpHeaders.CheckContainsNewLine(parameter);
_scheme = scheme;
_parameter = parameter;
}
private AuthenticationHeaderValue(AuthenticationHeaderValue source)
{
Debug.Assert(source != null);
_scheme = source._scheme;
_parameter = source._parameter;
}
public override string ToString()
{
if (string.IsNullOrEmpty(_parameter))
{
return _scheme;
}
return _scheme + " " + _parameter;
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
AuthenticationHeaderValue? other = obj as AuthenticationHeaderValue;
if (other == null)
{
return false;
}
if (string.IsNullOrEmpty(_parameter) && string.IsNullOrEmpty(other._parameter))
{
return (string.Equals(_scheme, other._scheme, StringComparison.OrdinalIgnoreCase));
}
else
{
// Since we can't parse the parameter, we use case-sensitive comparison.
return string.Equals(_scheme, other._scheme, StringComparison.OrdinalIgnoreCase) &&
string.Equals(_parameter, other._parameter, StringComparison.Ordinal);
}
}
public override int GetHashCode()
{
int result = StringComparer.OrdinalIgnoreCase.GetHashCode(_scheme);
if (!string.IsNullOrEmpty(_parameter))
{
result ^= _parameter.GetHashCode();
}
return result;
}
public static AuthenticationHeaderValue Parse(string input)
{
int index = 0;
return (AuthenticationHeaderValue)GenericHeaderParser.SingleValueAuthenticationParser.ParseValue(
input, null, ref index);
}
public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out AuthenticationHeaderValue? parsedValue)
{
int index = 0;
parsedValue = null;
if (GenericHeaderParser.SingleValueAuthenticationParser.TryParseValue(input, null, ref index, out object? output))
{
parsedValue = (AuthenticationHeaderValue)output!;
return true;
}
return false;
}
internal static int GetAuthenticationLength(string? input, int startIndex, out object? parsedValue)
{
Debug.Assert(startIndex >= 0);
parsedValue = null;
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length) || HttpRuleParser.ContainsNewLine(input, startIndex))
{
return 0;
}
// Parse the scheme string: <scheme> in '<scheme> <parameter>'
int schemeLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (schemeLength == 0)
{
return 0;
}
string? targetScheme = null;
switch (schemeLength)
{
// Avoid allocating a scheme string for the most common cases.
case 5: targetScheme = "Basic"; break;
case 6: targetScheme = "Digest"; break;
case 4: targetScheme = "NTLM"; break;
case 9: targetScheme = "Negotiate"; break;
}
string scheme = targetScheme != null && string.CompareOrdinal(input, startIndex, targetScheme, 0, schemeLength) == 0 ?
targetScheme :
input.Substring(startIndex, schemeLength);
int current = startIndex + schemeLength;
int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current);
current += whitespaceLength;
if ((current == input.Length) || (input[current] == ','))
{
// If we only have a scheme followed by whitespace, we're done.
parsedValue = new AuthenticationHeaderValue(scheme);
return current - startIndex;
}
// We need at least one space between the scheme and parameters. If there is no whitespace, then we must
// have reached the end of the string (i.e. scheme-only string).
if (whitespaceLength == 0)
{
return 0;
}
// If we get here, we have a <scheme> followed by a whitespace. Now we expect the following:
// '<scheme> <blob>[,<name>=<value>]*[, <otherscheme>...]*': <blob> potentially contains one
// or more '=' characters, optionally followed by additional name/value pairs, optionally followed by
// other schemes. <blob> may be a quoted string.
// We look at the value after ',': if it is <token>=<value> then we have a parameter for <scheme>.
// If we have either a <token>-only or <token><whitespace><blob> then we have another scheme.
int parameterStartIndex = current;
int parameterEndIndex = current;
if (!TrySkipFirstBlob(input, ref current, ref parameterEndIndex))
{
return 0;
}
if (current < input.Length)
{
if (!TryGetParametersEndIndex(input, ref current, ref parameterEndIndex))
{
return 0;
}
}
string parameter = input.Substring(parameterStartIndex, parameterEndIndex - parameterStartIndex + 1);
parsedValue = new AuthenticationHeaderValue(scheme, parameter);
return current - startIndex;
}
private static bool TrySkipFirstBlob(string input, ref int current, ref int parameterEndIndex)
{
// Find the delimiter: Note that <blob> in "<scheme> <blob>" may be a token, quoted string, name/value
// pair or a Base64 encoded string. So make sure that we don't consider ',' characters within a quoted
// string as delimiter.
while ((current < input.Length) && (input[current] != ','))
{
if (input[current] == '"')
{
int quotedStringLength;
if (HttpRuleParser.GetQuotedStringLength(input, current, out quotedStringLength) !=
HttpParseResult.Parsed)
{
// We have a quote but an invalid quoted-string.
return false;
}
current += quotedStringLength;
parameterEndIndex = current - 1; // -1 because 'current' points to the char after the final '"'
}
else
{
int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current);
// We don't want trailing whitespace to be considered part of the parameter blob. Increment
// 'parameterEndIndex' only if we don't have a whitespace. E.g. "Basic AbC= , NTLM" should return
// "AbC=" as parameter ignoring the spaces before ','.
if (whitespaceLength == 0)
{
parameterEndIndex = current;
current++;
}
else
{
current += whitespaceLength;
}
}
}
return true;
}
private static bool TryGetParametersEndIndex(string input, ref int parseEndIndex, ref int parameterEndIndex)
{
Debug.Assert(parseEndIndex < input.Length, "Expected string to have at least 1 char");
Debug.Assert(input[parseEndIndex] == ',');
int current = parseEndIndex;
do
{
current++; // skip ',' delimiter
current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, current, true, out _);
if (current == input.Length)
{
return true;
}
// Now we have to determine if after ',' we have a list of <name>=<value> pairs that are part of
// the auth scheme parameters OR if we have another auth scheme. Either way, after ',' we expect a
// valid token that is either the <name> in a <name>=<value> pair OR <scheme> of another scheme.
int tokenLength = HttpRuleParser.GetTokenLength(input, current);
if (tokenLength == 0)
{
return false;
}
current += tokenLength;
current += HttpRuleParser.GetWhitespaceLength(input, current);
// If we reached the end of the string or the token is followed by anything but '=', then the parsed
// token is another scheme name. The string representing parameters ends before the token (e.g.
// "Digest a=b, c=d, NTLM": return scheme "Digest" with parameters string "a=b, c=d").
if ((current == input.Length) || (input[current] != '='))
{
return true;
}
current++; // skip '=' delimiter
current += HttpRuleParser.GetWhitespaceLength(input, current);
int valueLength = NameValueHeaderValue.GetValueLength(input, current);
// After '<name>=' we expect a valid <value> (either token or quoted string)
if (valueLength == 0)
{
return false;
}
// Update parameter end index, since we just parsed a valid <name>=<value> pair that is part of the
// parameters string.
current += valueLength;
parameterEndIndex = current - 1; // -1 because 'current' already points to the char after <value>
current += HttpRuleParser.GetWhitespaceLength(input, current);
parseEndIndex = current; // this essentially points to parameterEndIndex + whitespace + next char
} while ((current < input.Length) && (input[current] == ','));
return true;
}
object ICloneable.Clone()
{
return new AuthenticationHeaderValue(this);
}
}
}
|