http_host_validator.dart 5.23 KB
Newer Older
1 2 3 4 5 6 7
// Copyright 2014 The Flutter Authors. 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:async';

import 'base/io.dart';
8
import 'base/net.dart';
9 10 11 12 13
import 'base/platform.dart';
import 'doctor_validator.dart';
import 'features.dart';

/// Common Flutter HTTP hosts.
14 15 16 17 18
const String kCloudHost = 'https://storage.googleapis.com/';
const String kCocoaPods = 'https://cocoapods.org/';
const String kGitHub = 'https://github.com/';
const String kMaven = 'https://maven.google.com/';
const String kPubDev = 'https://pub.dev/';
19

20 21
// Overridable environment variables.
const String kPubDevOverride = 'PUB_HOSTED_URL'; // https://dart.dev/tools/pub/environment-variables
22

23 24
// Validator that checks all provided hosts are reachable and responsive
class HttpHostValidator extends DoctorValidator {
25 26 27 28 29 30 31
  HttpHostValidator({
    required Platform platform,
    required FeatureFlags featureFlags,
    required HttpClient httpClient,
  }) : _platform = platform,
      _featureFlags = featureFlags,
      _httpClient = httpClient,
32
      super('Network resources');
33 34 35 36 37

  final Platform _platform;
  final FeatureFlags _featureFlags;
  final HttpClient _httpClient;

38
  final Set<Uri> _activeHosts = <Uri>{};
39

40 41 42 43 44 45 46
  @override
  String get slowWarning {
    if (_activeHosts.isEmpty) {
      return 'Network resources check is taking a long time...';
    }
    return 'Attempting to reach ${_activeHosts.map((Uri url) => url.host).join(", ")}...';
  }
47

48
  /// Make a head request to the HTTP host for checking availability
49
  Future<String?> _checkHostAvailability(Uri host) async {
50
    try {
51 52 53 54 55 56 57 58 59 60 61
      assert(!_activeHosts.contains(host));
      _activeHosts.add(host);
      final HttpClientRequest req = await _httpClient.headUrl(host);
      await req.close();
      // HTTP host is available if no exception happened.
      return null;
    } on SocketException catch (error) {
      return 'A network error occurred while checking "$host": ${error.message}';
    } on HttpException catch (error) {
      return 'An HTTP error occurred while checking "$host": ${error.message}';
    } on HandshakeException catch (error) {
Lioness100's avatar
Lioness100 committed
62
      return 'A cryptographic error occurred while checking "$host": ${error.message}\n'
63 64 65 66 67 68
             'You may be experiencing a man-in-the-middle attack, your network may be '
             'compromised, or you may have malware installed on your computer.';
    } on OSError catch (error) {
      return 'An error occurred while checking "$host": ${error.message}';
    } finally {
      _activeHosts.remove(host);
69 70 71
    }
  }

72 73 74 75 76 77 78 79
  static Uri? _parseUrl(String value) {
    final Uri? url = Uri.tryParse(value);
    if (url == null || !url.hasScheme || !url.hasAuthority || (!url.hasEmptyPath && !url.hasAbsolutePath) || url.hasFragment) {
      return null;
    }
    return url;
  }

80 81
  @override
  Future<ValidationResult> validate() async {
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
    final List<String?> availabilityResults = <String?>[];

    final List<Uri> requiredHosts = <Uri>[];
    if (_platform.environment.containsKey(kPubDevOverride)) {
      final Uri? url = _parseUrl(_platform.environment[kPubDevOverride]!);
      if (url == null) {
        availabilityResults.add(
          'Environment variable $kPubDevOverride does not specify a valid URL: "${_platform.environment[kPubDevOverride]}"\n'
          'Please see https://flutter.dev/community/china for an example of how to use it.'
        );
      } else {
        requiredHosts.add(url);
      }
    } else {
      requiredHosts.add(Uri.parse(kPubDev));
97
    }
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
    if (_platform.environment.containsKey(kFlutterStorageBaseUrl)) {
      final Uri? url = _parseUrl(_platform.environment[kFlutterStorageBaseUrl]!);
      if (url == null) {
        availabilityResults.add(
          'Environment variable $kFlutterStorageBaseUrl does not specify a valid URL: "${_platform.environment[kFlutterStorageBaseUrl]}"\n'
          'Please see https://flutter.dev/community/china for an example of how to use it.'
        );
      } else {
        requiredHosts.add(url);
      }
    } else {
      requiredHosts.add(Uri.parse(kCloudHost));
      if (_featureFlags.isAndroidEnabled) {
        // if kFlutterStorageBaseUrl is set it is used instead of Maven
        requiredHosts.add(Uri.parse(kMaven));
      }
    }
    if (_featureFlags.isMacOSEnabled) {
      requiredHosts.add(Uri.parse(kCocoaPods));
    }
    requiredHosts.add(Uri.parse(kGitHub));
119

120 121
    // Check all the hosts simultaneously.
    availabilityResults.addAll(await Future.wait<String?>(requiredHosts.map(_checkHostAvailability)));
122

123 124 125 126 127 128 129 130 131 132
    int failures = 0;
    int successes = 0;
    final List<ValidationMessage> messages = <ValidationMessage>[];
    for (final String? message in availabilityResults) {
      if (message == null) {
        successes += 1;
      } else {
        failures += 1;
        messages.add(ValidationMessage.error(message));
      }
133 134
    }

135 136 137 138 139 140 141 142 143
    if (failures == 0) {
      assert(successes > 0);
      assert(messages.isEmpty);
      return const ValidationResult(
        ValidationType.success,
        <ValidationMessage>[ValidationMessage('All expected network resources are available.')],
      );
    }
    assert(messages.isNotEmpty);
144
    return ValidationResult(
145
      successes == 0 ? ValidationType.notAvailable : ValidationType.partial,
146 147
      messages,
    );
148 149
  }
}