Line data Source code
1 : // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 2 : // for details. All rights reserved. Use of this source code is governed by a 3 : // BSD-style license that can be found in the LICENSE file. 4 : 5 : import 'dart:collection'; 6 : 7 : import 'package:string_scanner/string_scanner.dart'; 8 : 9 : import 'case_insensitive_map.dart'; 10 : import 'scan.dart'; 11 : import 'utils.dart'; 12 : 13 : /// A single challenge in a WWW-Authenticate header, parsed as per [RFC 2617][]. 14 : /// 15 : /// [RFC 2617]: http://tools.ietf.org/html/rfc2617 16 : /// 17 : /// Each WWW-Authenticate header contains one or more challenges, representing 18 : /// valid ways to authenticate with the server. 19 : class AuthenticationChallenge { 20 : /// The scheme describing the type of authentication that's required, for 21 : /// example "basic" or "digest". 22 : /// 23 : /// This is normalized to always be lower-case. 24 : final String scheme; 25 : 26 : /// The parameters describing how to authenticate. 27 : /// 28 : /// The semantics of these parameters are scheme-specific. The keys of this 29 : /// map are case-insensitive. 30 : final Map<String, String> parameters; 31 : 32 : /// Parses a WWW-Authenticate header, which should contain one or more 33 : /// challenges. 34 : /// 35 : /// Throws a [FormatException] if the header is invalid. 36 0 : static List<AuthenticationChallenge> parseHeader(String header) => 37 0 : wrapFormatException('authentication header', header, () { 38 0 : final scanner = StringScanner(header); 39 0 : scanner.scan(whitespace); 40 0 : final challenges = parseList(scanner, () { 41 0 : final scheme = _scanScheme(scanner, whitespaceName: '" " or "="'); 42 : 43 : // Manually parse the inner list. We need to do some lookahead to 44 : // disambiguate between an auth param and another challenge. 45 0 : final params = <String, String>{}; 46 : 47 : // Consume initial empty values. 48 0 : while (scanner.scan(',')) { 49 0 : scanner.scan(whitespace); 50 : } 51 : 52 0 : _scanAuthParam(scanner, params); 53 : 54 0 : var beforeComma = scanner.position; 55 0 : while (scanner.scan(',')) { 56 0 : scanner.scan(whitespace); 57 : 58 : // Empty elements are allowed, but excluded from the results. 59 0 : if (scanner.matches(',') || scanner.isDone) continue; 60 : 61 0 : scanner.expect(token, name: 'a token'); 62 0 : final name = scanner.lastMatch![0]!; 63 0 : scanner.scan(whitespace); 64 : 65 : // If there's no "=", then this is another challenge rather than a 66 : // parameter for the current challenge. 67 0 : if (!scanner.scan('=')) { 68 0 : scanner.position = beforeComma; 69 : break; 70 : } 71 : 72 0 : scanner.scan(whitespace); 73 : 74 0 : if (scanner.scan(token)) { 75 0 : params[name] = scanner.lastMatch![0]!; 76 : } else { 77 0 : params[name] = expectQuotedString(scanner, 78 : name: 'a token or a quoted string'); 79 : } 80 : 81 0 : scanner.scan(whitespace); 82 0 : beforeComma = scanner.position; 83 : } 84 : 85 0 : return AuthenticationChallenge(scheme, params); 86 : }); 87 : 88 0 : scanner.expectDone(); 89 : return challenges; 90 : }); 91 : 92 : /// Parses a single WWW-Authenticate challenge value. 93 : /// 94 : /// Throws a [FormatException] if the challenge is invalid. 95 0 : factory AuthenticationChallenge.parse(String challenge) => 96 0 : wrapFormatException('authentication challenge', challenge, () { 97 0 : final scanner = StringScanner(challenge); 98 0 : scanner.scan(whitespace); 99 0 : final scheme = _scanScheme(scanner); 100 : 101 0 : final params = <String, String>{}; 102 0 : parseList(scanner, () => _scanAuthParam(scanner, params)); 103 : 104 0 : scanner.expectDone(); 105 0 : return AuthenticationChallenge(scheme, params); 106 : }); 107 : 108 : /// Scans a single scheme name and asserts that it's followed by a space. 109 : /// 110 : /// If [whitespaceName] is passed, it's used as the name for exceptions thrown 111 : /// due to invalid trailing whitespace. 112 0 : static String _scanScheme(StringScanner scanner, {String? whitespaceName}) { 113 0 : scanner.expect(token, name: 'a token'); 114 0 : final scheme = scanner.lastMatch![0]!.toLowerCase(); 115 : 116 0 : scanner.scan(whitespace); 117 : 118 : // The spec specifically requires a space between the scheme and its 119 : // params. 120 0 : if (scanner.lastMatch == null || !scanner.lastMatch![0]!.contains(' ')) { 121 0 : scanner.expect(' ', name: whitespaceName); 122 : } 123 : 124 : return scheme; 125 : } 126 : 127 : /// Scans a single authentication parameter and stores its result in [params]. 128 0 : static void _scanAuthParam(StringScanner scanner, Map params) { 129 0 : scanner.expect(token, name: 'a token'); 130 0 : final name = scanner.lastMatch![0]; 131 0 : scanner.scan(whitespace); 132 0 : scanner.expect('='); 133 0 : scanner.scan(whitespace); 134 : 135 0 : if (scanner.scan(token)) { 136 0 : params[name] = scanner.lastMatch![0]; 137 : } else { 138 0 : params[name] = 139 0 : expectQuotedString(scanner, name: 'a token or a quoted string'); 140 : } 141 : 142 0 : scanner.scan(whitespace); 143 : } 144 : 145 : /// Creates a new challenge value with [scheme] and [parameters]. 146 0 : AuthenticationChallenge(this.scheme, Map<String, String> parameters) 147 0 : : parameters = UnmodifiableMapView(CaseInsensitiveMap.from(parameters)); 148 : }