blob: 356e0a25daaaa18bbdfc1bbc222680017cff25cd [file] [log] [blame]
// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:collection';
import 'package:string_scanner/string_scanner.dart';
import 'case_insensitive_map.dart';
import 'scan.dart';
import 'utils.dart';
/// A single challenge in a WWW-Authenticate header, parsed as per [RFC 2617][].
///
/// [RFC 2617]: http://tools.ietf.org/html/rfc2617
///
/// Each WWW-Authenticate header contains one or more challenges, representing
/// valid ways to authenticate with the server.
class AuthenticationChallenge {
/// The scheme describing the type of authentication that's required, for
/// example "basic" or "digest".
///
/// This is normalized to always be lower-case.
final String scheme;
/// The parameters describing how to authenticate.
///
/// The semantics of these parameters are scheme-specific. The keys of this
/// map are case-insensitive.
final Map<String, String> parameters;
/// Parses a WWW-Authenticate header, which should contain one or more
/// challenges.
///
/// Throws a [FormatException] if the header is invalid.
static List<AuthenticationChallenge> parseHeader(String header) =>
wrapFormatException('authentication header', header, () {
final scanner = StringScanner(header);
scanner.scan(whitespace);
final challenges = parseList(scanner, () {
final scheme = _scanScheme(scanner, whitespaceName: '" " or "="');
// Manually parse the inner list. We need to do some lookahead to
// disambiguate between an auth param and another challenge.
final params = <String, String>{};
// Consume initial empty values.
while (scanner.scan(',')) {
scanner.scan(whitespace);
}
_scanAuthParam(scanner, params);
var beforeComma = scanner.position;
while (scanner.scan(',')) {
scanner.scan(whitespace);
// Empty elements are allowed, but excluded from the results.
if (scanner.matches(',') || scanner.isDone) continue;
scanner.expect(token, name: 'a token');
final name = scanner.lastMatch![0]!;
scanner.scan(whitespace);
// If there's no "=", then this is another challenge rather than a
// parameter for the current challenge.
if (!scanner.scan('=')) {
scanner.position = beforeComma;
break;
}
scanner.scan(whitespace);
if (scanner.scan(token)) {
params[name] = scanner.lastMatch![0]!;
} else {
params[name] = expectQuotedString(scanner,
name: 'a token or a quoted string');
}
scanner.scan(whitespace);
beforeComma = scanner.position;
}
return AuthenticationChallenge(scheme, params);
});
scanner.expectDone();
return challenges;
});
/// Parses a single WWW-Authenticate challenge value.
///
/// Throws a [FormatException] if the challenge is invalid.
factory AuthenticationChallenge.parse(String challenge) =>
wrapFormatException('authentication challenge', challenge, () {
final scanner = StringScanner(challenge);
scanner.scan(whitespace);
final scheme = _scanScheme(scanner);
final params = <String, String>{};
parseList(scanner, () => _scanAuthParam(scanner, params));
scanner.expectDone();
return AuthenticationChallenge(scheme, params);
});
/// Scans a single scheme name and asserts that it's followed by a space.
///
/// If [whitespaceName] is passed, it's used as the name for exceptions thrown
/// due to invalid trailing whitespace.
static String _scanScheme(StringScanner scanner, {String? whitespaceName}) {
scanner.expect(token, name: 'a token');
final scheme = scanner.lastMatch![0]!.toLowerCase();
scanner.scan(whitespace);
// The spec specifically requires a space between the scheme and its
// params.
if (scanner.lastMatch == null || !scanner.lastMatch![0]!.contains(' ')) {
scanner.expect(' ', name: whitespaceName);
}
return scheme;
}
/// Scans a single authentication parameter and stores its result in [params].
static void _scanAuthParam(StringScanner scanner, Map params) {
scanner.expect(token, name: 'a token');
final name = scanner.lastMatch![0];
scanner.scan(whitespace);
scanner.expect('=');
scanner.scan(whitespace);
if (scanner.scan(token)) {
params[name] = scanner.lastMatch![0];
} else {
params[name] =
expectQuotedString(scanner, name: 'a token or a quoted string');
}
scanner.scan(whitespace);
}
/// Creates a new challenge value with [scheme] and [parameters].
AuthenticationChallenge(this.scheme, Map<String, String> parameters)
: parameters = UnmodifiableMapView(CaseInsensitiveMap.from(parameters));
}