| // Copyright (c) 2021, 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:io'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:http/http.dart' as http; | |
| import 'package:http_parser/http_parser.dart'; | |
| import '../http.dart'; | |
| import '../log.dart' as log; | |
| import '../system_cache.dart'; | |
| import '../utils.dart'; | |
| import 'credential.dart'; | |
| /// This client authenticates requests by injecting `Authentication` header to | |
| /// requests. | |
| /// | |
| /// Requests to URLs not under [_credential]'s url will not be authenticated. | |
| class _AuthenticatedClient extends http.BaseClient { | |
| /// Constructs Http client wrapper that injects `authorization` header to | |
| /// requests and handles authentication errors. | |
| /// | |
| /// [_credential] might be `null`. In that case `authorization` header will | |
| /// not be injected to requests. | |
| _AuthenticatedClient(this._inner, this._credential); | |
| final http.Client _inner; | |
| /// Authentication scheme that could be used for authenticating requests. | |
| final Credential? _credential; | |
| /// Detected that [_credential] are invalid, happens when server responds 401. | |
| bool _detectInvalidCredentials = false; | |
| @override | |
| Future<http.StreamedResponse> send(http.BaseRequest request) async { | |
| // Let's last time make sure that, we're allowed to use credential for this | |
| // request. | |
| // | |
| // This check ensures that this client will only authenticate requests sent | |
| // to given serverBaseUrl. Otherwise credential leaks might occur when | |
| // archive_url hosted on 3rd party server that should not receive | |
| // credentials of the first party. | |
| if (_credential != null && | |
| _credential.canAuthenticate(request.url.toString())) { | |
| request.headers[HttpHeaders.authorizationHeader] = | |
| await _credential.getAuthorizationHeaderValue(); | |
| } | |
| final response = await _inner.send(request); | |
| if (response.statusCode == 401) { | |
| _detectInvalidCredentials = true; | |
| } | |
| if (response.statusCode == 401 || response.statusCode == 403) { | |
| _throwAuthException(response); | |
| } | |
| return response; | |
| } | |
| /// Throws [AuthenticationException] that includes response status code and | |
| /// message parsed from WWW-Authenticate header usign | |
| /// [RFC 7235 section 4.1][RFC] specifications. | |
| /// | |
| /// [RFC]: https://datatracker.ietf.org/doc/html/rfc7235#section-4.1 | |
| void _throwAuthException(http.BaseResponse response) { | |
| String? serverMessage; | |
| if (response.headers.containsKey(HttpHeaders.wwwAuthenticateHeader)) { | |
| try { | |
| final header = response.headers[HttpHeaders.wwwAuthenticateHeader]!; | |
| final challenge = AuthenticationChallenge.parseHeader( | |
| header, | |
| ).firstWhereOrNull( | |
| (challenge) => | |
| challenge.scheme == 'bearer' && | |
| challenge.parameters['realm'] == 'pub' && | |
| challenge.parameters['message'] != null, | |
| ); | |
| if (challenge != null) { | |
| serverMessage = challenge.parameters['message']; | |
| } | |
| } on FormatException { | |
| // Ignore errors might be caused when parsing invalid header values | |
| } | |
| } | |
| if (serverMessage != null) { | |
| serverMessage = sanitizeForTerminal(serverMessage); | |
| } | |
| throw AuthenticationException(response.statusCode, serverMessage); | |
| } | |
| @override | |
| void close() => _inner.close(); | |
| } | |
| /// Token authenticated related exception. | |
| class AuthenticationException implements Exception { | |
| const AuthenticationException(this.statusCode, this.serverMessage); | |
| final int statusCode; | |
| final String? serverMessage; | |
| @override | |
| String toString() { | |
| var message = 'Authentication error ($statusCode)'; | |
| if (serverMessage != null) { | |
| message += ': $serverMessage'; | |
| } | |
| return message; | |
| } | |
| } | |
| /// Invoke [fn] with a [http.Client] capable of authenticating against | |
| /// [hostedUrl]. | |
| /// | |
| /// Importantly, requests to URLs not under [hostedUrl] will not be | |
| /// authenticated. | |
| Future<T> withAuthenticatedClient<T>( | |
| SystemCache systemCache, | |
| Uri hostedUrl, | |
| Future<T> Function(http.Client) fn, | |
| ) async { | |
| final credential = systemCache.tokenStore.findCredential(hostedUrl); | |
| final client = _AuthenticatedClient(globalHttpClient, credential); | |
| try { | |
| return await fn(client); | |
| } finally { | |
| if (client._detectInvalidCredentials) { | |
| // try to remove the credential, if we detected that it is invalid! | |
| final removed = systemCache.tokenStore.removeCredential(hostedUrl); | |
| if (removed) { | |
| log.warning('Invalid token for $hostedUrl deleted.'); | |
| } | |
| } | |
| } | |
| } |