blob: c0b63ca80994d6156ee929c4a4dc0475aee6deb2 [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) {
return wrapFormatException("authentication header", header, () {
var scanner = new StringScanner(header);
scanner.scan(whitespace);
var challenges = parseList(scanner, () {
var 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.
var params = {};
// 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");
var 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 new 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) {
return wrapFormatException("authentication challenge", challenge, () {
var scanner = new StringScanner(challenge);
scanner.scan(whitespace);
var scheme = _scanScheme(scanner);
var params = {};
parseList(scanner, () => _scanAuthParam(scanner, params));
scanner.expectDone();
return new 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");
var 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");
var 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 = new UnmodifiableMapView(
new CaseInsensitiveMap.from(parameters));
}