| // 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)); | |
| } |