blob: a6001ecf9169c4669154300918696c53c1ee82e4 [file] [log] [blame]
// 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 'credential.dart';
/// This client authenticates requests by injecting `Authentication` header to
/// requests.
///
/// Requests to URLs not under [serverBaseUrl] 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.BaseClient _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 ocurr 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();
}
try {
final response = await _inner.send(request);
if (response.statusCode == 401) {
_detectInvalidCredentials = true;
_throwAuthException(response);
}
return response;
} on PubHttpException catch (e) {
if (e.response.statusCode == 403) {
_throwAuthException(e.response);
}
rethrow;
}
}
/// 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) {
// Only allow printable ASCII, map anything else to whitespace, take
// at-most 1024 characters.
serverMessage = String.fromCharCodes(serverMessage.runes
.map((r) => 32 <= r && r <= 127 ? r : 32)
.take(1024));
}
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(httpClient, 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.');
}
}
}
}