| // Copyright (c) 2012, 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. | 
 |  | 
 | /// Helpers for dealing with HTTP. | 
 | import 'dart:async'; | 
 | import 'dart:convert'; | 
 | import 'dart:io'; | 
 | import 'dart:math' as math; | 
 |  | 
 | import 'package:http/http.dart' as http; | 
 | import 'package:http/retry.dart'; | 
 | import 'package:pool/pool.dart'; | 
 | import 'package:stack_trace/stack_trace.dart'; | 
 |  | 
 | import 'command.dart'; | 
 | import 'io.dart'; | 
 | import 'log.dart' as log; | 
 | import 'oauth2.dart' as oauth2; | 
 | import 'package.dart'; | 
 | import 'sdk.dart'; | 
 | import 'utils.dart'; | 
 |  | 
 | /// Headers and field names that should be censored in the log output. | 
 | const _censoredFields = ['refresh_token', 'authorization']; | 
 |  | 
 | /// Headers required for pub.dartlang.org API requests. | 
 | /// | 
 | /// The Accept header tells pub.dartlang.org which version of the API we're | 
 | /// expecting, so it can either serve that version or give us a 406 error if | 
 | /// it's not supported. | 
 | const pubApiHeaders = {'Accept': 'application/vnd.pub.v2+json'}; | 
 |  | 
 | /// A unique ID to identify this particular invocation of pub. | 
 | final _sessionId = createUuid(); | 
 |  | 
 | /// An HTTP client that transforms 40* errors and socket exceptions into more | 
 | /// user-friendly error messages. | 
 | class _PubHttpClient extends http.BaseClient { | 
 |   final _requestStopwatches = <http.BaseRequest, Stopwatch>{}; | 
 |  | 
 |   http.Client _inner; | 
 |  | 
 |   _PubHttpClient([http.Client? inner]) : _inner = inner ?? http.Client(); | 
 |  | 
 |   @override | 
 |   Future<http.StreamedResponse> send(http.BaseRequest request) async { | 
 |     if (_shouldAddMetadata(request)) { | 
 |       request.headers['X-Pub-OS'] = Platform.operatingSystem; | 
 |       request.headers['X-Pub-Command'] = PubCommand.command; | 
 |       request.headers['X-Pub-Session-ID'] = _sessionId; | 
 |  | 
 |       var environment = Platform.environment['PUB_ENVIRONMENT']; | 
 |       if (environment != null) { | 
 |         request.headers['X-Pub-Environment'] = environment; | 
 |       } | 
 |  | 
 |       var type = Zone.current[#_dependencyType]; | 
 |       if (type != null && type != DependencyType.none) { | 
 |         request.headers['X-Pub-Reason'] = type.toString(); | 
 |       } | 
 |     } | 
 |  | 
 |     _requestStopwatches[request] = Stopwatch()..start(); | 
 |     request.headers[HttpHeaders.userAgentHeader] = 'Dart pub ${sdk.version}'; | 
 |     _logRequest(request); | 
 |  | 
 |     final streamedResponse = await _inner.send(request); | 
 |  | 
 |     _logResponse(streamedResponse); | 
 |  | 
 |     return streamedResponse; | 
 |   } | 
 |  | 
 |   /// Whether extra metadata headers should be added to [request]. | 
 |   bool _shouldAddMetadata(http.BaseRequest request) { | 
 |     if (runningFromTest && Platform.environment.containsKey('PUB_HOSTED_URL')) { | 
 |       if (request.url.origin != Platform.environment['PUB_HOSTED_URL']) { | 
 |         return false; | 
 |       } | 
 |     } else { | 
 |       if (request.url.origin != 'https://pub.dartlang.org') return false; | 
 |     } | 
 |  | 
 |     if (Platform.environment.containsKey('CI') && | 
 |         Platform.environment['CI'] != 'false') { | 
 |       return false; | 
 |     } | 
 |  | 
 |     return true; | 
 |   } | 
 |  | 
 |   /// Logs the fact that [request] was sent, and information about it. | 
 |   void _logRequest(http.BaseRequest request) { | 
 |     var requestLog = StringBuffer(); | 
 |     requestLog.writeln('HTTP ${request.method} ${request.url}'); | 
 |     request.headers | 
 |         .forEach((name, value) => requestLog.writeln(_logField(name, value))); | 
 |  | 
 |     if (request.method == 'POST') { | 
 |       var contentTypeString = request.headers[HttpHeaders.contentTypeHeader]; | 
 |       var contentType = ContentType.parse(contentTypeString ?? ''); | 
 |       if (request is http.MultipartRequest) { | 
 |         requestLog.writeln(); | 
 |         requestLog.writeln('Body fields:'); | 
 |         request.fields.forEach( | 
 |             (name, value) => requestLog.writeln(_logField(name, value))); | 
 |  | 
 |         // TODO(nweiz): make MultipartRequest.files readable, and log them? | 
 |       } else if (request is http.Request) { | 
 |         if (contentType.value == 'application/x-www-form-urlencoded') { | 
 |           requestLog.writeln(); | 
 |           requestLog.writeln('Body fields:'); | 
 |           request.bodyFields.forEach( | 
 |               (name, value) => requestLog.writeln(_logField(name, value))); | 
 |         } else if (contentType.value == 'text/plain' || | 
 |             contentType.value == 'application/json') { | 
 |           requestLog.write(request.body); | 
 |         } | 
 |       } | 
 |     } | 
 |  | 
 |     log.io(requestLog.toString().trim()); | 
 |   } | 
 |  | 
 |   /// Logs the fact that [response] was received, and information about it. | 
 |   void _logResponse(http.StreamedResponse response) { | 
 |     // TODO(nweiz): Fork the response stream and log the response body. Be | 
 |     // careful not to log OAuth2 private data, though. | 
 |  | 
 |     var responseLog = StringBuffer(); | 
 |     var request = response.request!; | 
 |     var stopwatch = _requestStopwatches.remove(request)!..stop(); | 
 |     responseLog.writeln('HTTP response ${response.statusCode} ' | 
 |         '${response.reasonPhrase} for ${request.method} ${request.url}'); | 
 |     responseLog.writeln('took ${stopwatch.elapsed}'); | 
 |     response.headers | 
 |         .forEach((name, value) => responseLog.writeln(_logField(name, value))); | 
 |  | 
 |     log.io(responseLog.toString().trim()); | 
 |   } | 
 |  | 
 |   /// Returns a log-formatted string for the HTTP field or header with the given | 
 |   /// [name] and [value]. | 
 |   String _logField(String name, String value) { | 
 |     if (_censoredFields.contains(name.toLowerCase())) { | 
 |       return '$name: <censored>'; | 
 |     } else { | 
 |       return '$name: $value'; | 
 |     } | 
 |   } | 
 |  | 
 |   @override | 
 |   void close() => _inner.close(); | 
 | } | 
 |  | 
 | /// The [_PubHttpClient] wrapped by [httpClient]. | 
 | final _pubClient = _PubHttpClient(); | 
 |  | 
 | /// A set of all hostnames for which we've printed a message indicating that | 
 | /// we're waiting for them to come back up. | 
 | final _retriedHosts = <String>{}; | 
 |  | 
 | /// Intercepts all requests and throws exceptions if the response was not | 
 | /// considered successful. | 
 | class _ThrowingClient extends http.BaseClient { | 
 |   final http.Client _inner; | 
 |  | 
 |   _ThrowingClient(this._inner); | 
 |  | 
 |   @override | 
 |   Future<http.StreamedResponse> send(http.BaseRequest request) async { | 
 |     late http.StreamedResponse streamedResponse; | 
 |     try { | 
 |       streamedResponse = await _inner.send(request); | 
 |     } on SocketException catch (error, stackTraceOrNull) { | 
 |       // Work around issue 23008. | 
 |       var stackTrace = stackTraceOrNull; | 
 |  | 
 |       if (error.osError == null) rethrow; | 
 |  | 
 |       // Handle error codes known to be related to DNS or SSL issues. While it | 
 |       // is tempting to handle these error codes before retrying, saving time | 
 |       // for the end-user, it is known that DNS lookups can fail intermittently | 
 |       // in some cloud environments. Furthermore, since these error codes are | 
 |       // platform-specific (undocumented) and essentially cargo-culted along | 
 |       // skipping retries may lead to intermittent issues that could be fixed | 
 |       // with a retry. Failing to retry intermittent issues is likely to cause | 
 |       // customers to wrap pub in a retry loop which will not improve the | 
 |       // end-user experience. | 
 |       if (error.osError!.errorCode == 8 || | 
 |           error.osError!.errorCode == -2 || | 
 |           error.osError!.errorCode == -5 || | 
 |           error.osError!.errorCode == 11001 || | 
 |           error.osError!.errorCode == 11004) { | 
 |         fail('Could not resolve URL "${request.url.origin}".', error, | 
 |             stackTrace); | 
 |       } else if (error.osError!.errorCode == -12276) { | 
 |         fail( | 
 |             'Unable to validate SSL certificate for ' | 
 |             '"${request.url.origin}".', | 
 |             error, | 
 |             stackTrace); | 
 |       } else { | 
 |         rethrow; | 
 |       } | 
 |     } | 
 |  | 
 |     var status = streamedResponse.statusCode; | 
 |     // 401 responses should be handled by the OAuth2 client. It's very | 
 |     // unlikely that they'll be returned by non-OAuth2 requests. We also want | 
 |     // to pass along 400 responses from the token endpoint. | 
 |     var tokenRequest = streamedResponse.request!.url == oauth2.tokenEndpoint; | 
 |     if (status < 400 || status == 401 || (status == 400 && tokenRequest)) { | 
 |       return streamedResponse; | 
 |     } | 
 |  | 
 |     if (status == 406 && request.headers['Accept'] == pubApiHeaders['Accept']) { | 
 |       fail('Pub ${sdk.version} is incompatible with the current version of ' | 
 |           '${request.url.host}.\n' | 
 |           'Upgrade pub to the latest version and try again.'); | 
 |     } | 
 |  | 
 |     if (status == 500 && | 
 |         (request.url.host == 'pub.dartlang.org' || | 
 |             request.url.host == 'storage.googleapis.com')) { | 
 |       fail('HTTP error 500: Internal Server Error at ${request.url}.\n' | 
 |           'This is likely a transient error. Please try again later.'); | 
 |     } | 
 |  | 
 |     throw PubHttpException(await http.Response.fromStream(streamedResponse)); | 
 |   } | 
 |  | 
 |   @override | 
 |   void close() => _inner.close(); | 
 | } | 
 |  | 
 | /// The HTTP client to use for all HTTP requests. | 
 | final httpClient = _ThrottleClient( | 
 |     16, | 
 |     _ThrowingClient(RetryClient(_pubClient, | 
 |         retries: math.max( | 
 |           1, // Having less than 1 retry is **always** wrong. | 
 |           int.tryParse(Platform.environment['PUB_MAX_HTTP_RETRIES'] ?? '') ?? 7, | 
 |         ), | 
 |         when: (response) => | 
 |             const [500, 502, 503, 504].contains(response.statusCode), | 
 |         whenError: (error, stackTrace) { | 
 |           if (error is! IOException) return false; | 
 |  | 
 |           var chain = Chain.forTrace(stackTrace); | 
 |           log.io('HTTP error:\n$error\n\n${chain.terse}'); | 
 |           return true; | 
 |         }, | 
 |         delay: (retryCount) { | 
 |           if (retryCount < 3) { | 
 |             // Retry quickly a couple times in case of a short transient error. | 
 |             // | 
 |             // Add a random delay to avoid retrying a bunch of parallel requests | 
 |             // all at the same time. | 
 |             return Duration(milliseconds: 500) * math.pow(1.5, retryCount) + | 
 |                 Duration(milliseconds: random.nextInt(500)); | 
 |           } else { | 
 |             // If the error persists, wait a long time. This works around issues | 
 |             // where an AppEngine instance will go down and need to be rebooted, | 
 |             // which takes about a minute. | 
 |             return Duration(seconds: 30); | 
 |           } | 
 |         }, | 
 |         onRetry: (request, response, retryCount) { | 
 |           log.io('Retry #${retryCount + 1} for ' | 
 |               '${request.method} ${request.url}...'); | 
 |           if (retryCount != 3) return; | 
 |           if (!_retriedHosts.add(request.url.host)) return; | 
 |           log.message( | 
 |               'It looks like ${request.url.host} is having some trouble.\n' | 
 |               'Pub will wait for a while before trying to connect again.'); | 
 |         }))); | 
 |  | 
 | /// The underlying HTTP client wrapped by [httpClient]. | 
 | http.Client get innerHttpClient => _pubClient._inner; | 
 | set innerHttpClient(http.Client client) => _pubClient._inner = client; | 
 |  | 
 | /// Runs [callback] in a zone where all HTTP requests sent to `pub.dartlang.org` | 
 | /// will indicate the [type] of the relationship between the root package and | 
 | /// the package being requested. | 
 | /// | 
 | /// If [type] is [DependencyType.none], no extra metadata is added. | 
 | Future<T> withDependencyType<T>( | 
 |   DependencyType type, | 
 |   Future<T> Function() callback, | 
 | ) { | 
 |   return runZoned(callback, zoneValues: {#_dependencyType: type}); | 
 | } | 
 |  | 
 | /// Handles a successful JSON-formatted response from pub.dartlang.org. | 
 | /// | 
 | /// These responses are expected to be of the form `{"success": {"message": | 
 | /// "some message"}}`. If the format is correct, the message will be printed; | 
 | /// otherwise an error will be raised. | 
 | void handleJsonSuccess(http.Response response) { | 
 |   var parsed = parseJsonResponse(response); | 
 |   if (parsed['success'] is! Map || | 
 |       !parsed['success'].containsKey('message') || | 
 |       parsed['success']['message'] is! String) { | 
 |     invalidServerResponse(response); | 
 |   } | 
 |   log.message(log.green(parsed['success']['message'])); | 
 | } | 
 |  | 
 | /// Handles an unsuccessful JSON-formatted response from pub.dartlang.org. | 
 | /// | 
 | /// These responses are expected to be of the form `{"error": {"message": "some | 
 | /// message"}}`. If the format is correct, the message will be raised as an | 
 | /// error; otherwise an [invalidServerResponse] error will be raised. | 
 | void handleJsonError(http.Response response) { | 
 |   var errorMap = parseJsonResponse(response); | 
 |   if (errorMap['error'] is! Map || | 
 |       !errorMap['error'].containsKey('message') || | 
 |       errorMap['error']['message'] is! String) { | 
 |     invalidServerResponse(response); | 
 |   } | 
 |   fail(log.red(errorMap['error']['message'])); | 
 | } | 
 |  | 
 | /// Parses a response body, assuming it's JSON-formatted. | 
 | /// | 
 | /// Throws a user-friendly error if the response body is invalid JSON, or if | 
 | /// it's not a map. | 
 | Map parseJsonResponse(http.Response response) { | 
 |   Object value; | 
 |   try { | 
 |     value = jsonDecode(response.body); | 
 |   } on FormatException { | 
 |     invalidServerResponse(response); | 
 |   } | 
 |   if (value is! Map) invalidServerResponse(response); | 
 |   return value; | 
 | } | 
 |  | 
 | /// Throws an error describing an invalid response from the server. | 
 | Never invalidServerResponse(http.Response response) => | 
 |     fail(log.red('Invalid server response:\n${response.body}')); | 
 |  | 
 | /// Exception thrown when an HTTP operation fails. | 
 | class PubHttpException implements Exception { | 
 |   final http.Response response; | 
 |  | 
 |   const PubHttpException(this.response); | 
 |  | 
 |   @override | 
 |   String toString() => 'HTTP error ${response.statusCode}: ' | 
 |       '${response.reasonPhrase}'; | 
 | } | 
 |  | 
 | /// A middleware client that throttles the number of concurrent requests. | 
 | /// | 
 | /// As long as the number of requests is within the limit, this works just like | 
 | /// a normal client. If a request is made beyond the limit, the underlying HTTP | 
 | /// request won't be sent until other requests have completed. | 
 | class _ThrottleClient extends http.BaseClient { | 
 |   final Pool _pool; | 
 |   final http.Client _inner; | 
 |  | 
 |   /// Creates a new client that allows no more than [maxActiveRequests] | 
 |   /// concurrent requests. | 
 |   /// | 
 |   /// If [inner] is passed, it's used as the inner client for sending HTTP | 
 |   /// requests. It defaults to `new http.Client()`. | 
 |   _ThrottleClient(int maxActiveRequests, this._inner) | 
 |       : _pool = Pool(maxActiveRequests); | 
 |  | 
 |   @override | 
 |   Future<http.StreamedResponse> send(http.BaseRequest request) async { | 
 |     var resource = await _pool.request(); | 
 |  | 
 |     http.StreamedResponse response; | 
 |     try { | 
 |       response = await _inner.send(request); | 
 |     } catch (_) { | 
 |       resource.release(); | 
 |       rethrow; | 
 |     } | 
 |  | 
 |     final responseController = StreamController<List<int>>(sync: true); | 
 |     unawaited(response.stream.pipe(responseController)); | 
 |     unawaited(responseController.done.then((_) => resource.release())); | 
 |     return http.StreamedResponse(responseController.stream, response.statusCode, | 
 |         contentLength: response.contentLength, | 
 |         request: response.request, | 
 |         headers: response.headers, | 
 |         isRedirect: response.isRedirect, | 
 |         persistentConnection: response.persistentConnection, | 
 |         reasonPhrase: response.reasonPhrase); | 
 |   } | 
 |  | 
 |   @override | 
 |   void close() => _inner.close(); | 
 | } |