feat(flutter_tools): Added doctor host validation feat (#95386)

diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart
index 342bf44..35a30f6 100644
--- a/packages/flutter_tools/lib/src/doctor.dart
+++ b/packages/flutter_tools/lib/src/doctor.dart
@@ -10,6 +10,7 @@
 import 'base/async_guard.dart';
 import 'base/context.dart';
 import 'base/file_system.dart';
+import 'base/io.dart';
 import 'base/logger.dart';
 import 'base/os.dart';
 import 'base/platform.dart';
@@ -22,6 +23,7 @@
 import 'features.dart';
 import 'fuchsia/fuchsia_workflow.dart';
 import 'globals.dart' as globals;
+import 'http_host_validator.dart';
 import 'intellij/intellij_validator.dart';
 import 'linux/linux_doctor.dart';
 import 'linux/linux_workflow.dart';
@@ -132,6 +134,11 @@
           deviceManager: globals.deviceManager,
           userMessages: globals.userMessages,
         ),
+      HttpHostValidator(
+        platform: globals.platform,
+        featureFlags: featureFlags,
+        httpClient: globals.httpClientFactory?.call() ?? HttpClient(),
+      ),
     ];
     return _validators!;
   }
diff --git a/packages/flutter_tools/lib/src/http_host_validator.dart b/packages/flutter_tools/lib/src/http_host_validator.dart
new file mode 100644
index 0000000..76b748a
--- /dev/null
+++ b/packages/flutter_tools/lib/src/http_host_validator.dart
@@ -0,0 +1,126 @@
+// 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';
+import 'base/platform.dart';
+import 'doctor_validator.dart';
+import 'features.dart';
+
+// Overridable environment variables
+const String kEnvPubHostedUrl = 'PUB_HOSTED_URL';
+const String kEnvCloudUrl = 'FLUTTER_STORAGE_BASE_URL';
+const String kDoctorHostTimeout = 'FLUTTER_DOCTOR_HOST_TIMEOUT';
+
+/// Common Flutter HTTP hosts.
+const String kPubDevHttpHost = 'https://pub.dev/';
+const String kgCloudHttpHost = 'https://cloud.google.com/';
+
+/// Android specific required HTTP hosts.
+const List<String> androidRequiredHttpHosts = <String>[
+  'https://maven.google.com/',
+];
+
+/// MacOS specific required HTTP hosts.
+const List<String> macOSRequiredHttpHosts = <String>[
+  'https://cocoapods.org/',
+];
+
+// Validator that checks all provided hosts are reachable and responsive
+class HttpHostValidator extends DoctorValidator {
+  HttpHostValidator(
+      {required Platform platform,
+        required FeatureFlags featureFlags,
+        required HttpClient httpClient})
+      : _platform = platform,
+        _featureFlags = featureFlags,
+        _httpClient = httpClient,
+        super('HTTP Host Availability');
+
+  final Platform _platform;
+  final FeatureFlags _featureFlags;
+  final HttpClient _httpClient;
+
+  @override
+  String get slowWarning =>
+      'HTTP Host availability check is taking a long time...';
+
+  List<String> get _requiredHosts => <String>[
+    if (_featureFlags.isMacOSEnabled) ...macOSRequiredHttpHosts,
+    if (_featureFlags.isAndroidEnabled) ...androidRequiredHttpHosts,
+    _platform.environment[kEnvPubHostedUrl] ?? kPubDevHttpHost,
+    _platform.environment[kEnvCloudUrl] ?? kgCloudHttpHost,
+  ];
+
+  /// Make a head request to the HTTP host. HTTP Host is available if no exception happened
+  Future<_HostValidationResult> _checkHostAvailability(String host) async {
+    try {
+      final int timeout =
+      int.parse(_platform.environment[kDoctorHostTimeout] ?? '10');
+      final HttpClientRequest req = await _httpClient.headUrl(Uri.parse(host));
+      await req.close().timeout(Duration(seconds: timeout));
+      // HTTP Host is available if no exception happened
+      return _HostValidationResult.success(host);
+    } on TimeoutException {
+      return _HostValidationResult.fail(
+          host, 'Failed to connect to $host in seconds');
+    } on SocketException catch (e) {
+      return _HostValidationResult.fail(
+          host, 'An error occurred while checking the HTTP host: ${e.message}');
+    } on HttpException catch (e) {
+      return _HostValidationResult.fail(host,
+          'An error occurred while checking the HTTP host: ${e.toString()}');
+    } on OSError catch (e) {
+      return _HostValidationResult.fail(
+          host, 'An error occurred while checking the HTTP host: ${e.message}');
+    }
+  }
+
+  @override
+  Future<ValidationResult> validate() async {
+    final List<ValidationMessage> messages = <ValidationMessage>[];
+    final Iterable<Future<_HostValidationResult>> availabilityResultFutures =
+    _requiredHosts.map(_checkHostAvailability);
+
+    final List<_HostValidationResult> availabilityResults =
+    (await Future.wait(availabilityResultFutures)).toList();
+
+    if (availabilityResults
+        .every((_HostValidationResult result) => result.available)) {
+      return ValidationResult(
+          ValidationType.installed,
+          messages
+            ..add(const ValidationMessage(
+                'All required HTTP hosts are available')));
+    }
+
+    availabilityResults
+        .removeWhere((_HostValidationResult result) => result.available);
+
+    for (final _HostValidationResult result in availabilityResults) {
+      messages.add(ValidationMessage.error(
+          'HTTP host ${result.host} is not reachable. Reason: ${result.failResultInfo}'));
+    }
+
+    return ValidationResult(
+        availabilityResults.length == _requiredHosts.length
+            ? ValidationType.notAvailable
+            : ValidationType.partial,
+        messages);
+  }
+}
+
+class _HostValidationResult {
+  _HostValidationResult.success(this.host)
+      : failResultInfo = '',
+        available = true;
+
+  _HostValidationResult.fail(this.host, this.failResultInfo)
+      : available = false;
+
+  final String failResultInfo;
+  final String host;
+  final bool available;
+}
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/http_host_validator_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/http_host_validator_test.dart
new file mode 100644
index 0000000..97a20d6
--- /dev/null
+++ b/packages/flutter_tools/test/commands.shard/hermetic/http_host_validator_test.dart
@@ -0,0 +1,266 @@
+// 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:io';
+
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/doctor_validator.dart';
+import 'package:flutter_tools/src/http_host_validator.dart';
+
+import '../../src/common.dart';
+import '../../src/fake_http_client.dart';
+import '../../src/fakes.dart';
+
+// The environment variables used to override some URLs
+const String kTestEnvPubHost = 'https://pub.flutter-io.cn';
+const String kTestEnvGCloudHost = 'https://storage.flutter-io.cn';
+const Map<String, String> kTestEnvironment = <String, String>{
+  'PUB_HOSTED_URL': kTestEnvPubHost,
+  'FLUTTER_STORAGE_BASE_URL': kTestEnvGCloudHost,
+  'FLUTTER_DOCTOR_HOST_TIMEOUT': '1',
+};
+
+void main() {
+  group('http host validator', () {
+    const List<String> osTested = <String>['windows', 'macos', 'linux'];
+
+    group('no env variables', () {
+      testWithoutContext('all http hosts are available', () async {
+        final FakeHttpClient mockClient = FakeHttpClient.any();
+
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os),
+            featureFlags: TestFeatureFlags(),
+            httpClient: mockClient,
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.installed));
+        }
+      });
+
+      testWithoutContext('all http hosts are not available', () async {
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os),
+            featureFlags: TestFeatureFlags(),
+            httpClient: FakeHttpClient.list(<FakeRequest>[
+              FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(androidRequiredHttpHosts[0]), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+            ]),
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.notAvailable));
+        }
+      });
+
+      testWithoutContext('one http hosts are not available', () async {
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os),
+            featureFlags: TestFeatureFlags(),
+            httpClient: FakeHttpClient.list(<FakeRequest>[
+              FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(androidRequiredHttpHosts[0]), method: HttpMethod.head),
+              FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head),
+              FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head),
+            ]),
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.partial));
+        }
+      });
+
+      testWithoutContext('one http hosts are not available', () async {
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os),
+            featureFlags: TestFeatureFlags(),
+            httpClient: FakeHttpClient.list(<FakeRequest>[
+              FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(androidRequiredHttpHosts[0]), method: HttpMethod.head),
+              FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head),
+              FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head),
+            ]),
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.partial));
+        }
+      });
+    });
+
+    group('with env variables', () {
+      testWithoutContext('all http hosts are available', () async {
+        final FakeHttpClient mockClient = FakeHttpClient.any();
+
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os, environment: kTestEnvironment),
+            featureFlags: TestFeatureFlags(),
+            httpClient: mockClient,
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.installed));
+        }
+      });
+
+      testWithoutContext('all http hosts are not available', () async {
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os, environment: kTestEnvironment),
+            featureFlags: TestFeatureFlags(),
+            httpClient: FakeHttpClient.list(<FakeRequest>[
+              FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(androidRequiredHttpHosts[0]), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+            ]),
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.notAvailable));
+        }
+      });
+
+      testWithoutContext('one http hosts are not available', () async {
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os, environment: kTestEnvironment),
+            featureFlags: TestFeatureFlags(),
+            httpClient: FakeHttpClient.list(<FakeRequest>[
+              FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(androidRequiredHttpHosts[0]), method: HttpMethod.head),
+              FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head),
+              FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head),
+            ]),
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.partial));
+        }
+      });
+
+      testWithoutContext('one http hosts are not available', () async {
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os, environment: kTestEnvironment),
+            featureFlags: TestFeatureFlags(),
+            httpClient: FakeHttpClient.list(<FakeRequest>[
+              FakeRequest(Uri.parse(kTestEnvGCloudHost), method: HttpMethod.head, responseError: const OSError('Name or service not known', -2)),
+              FakeRequest(Uri.parse(androidRequiredHttpHosts[0]), method: HttpMethod.head),
+              FakeRequest(Uri.parse(kTestEnvPubHost), method: HttpMethod.head),
+              FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head),
+            ]),
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.partial));
+        }
+      });
+    });
+
+    group('specific os disabled', () {
+      testWithoutContext('all http hosts are available - android disabled', () async {
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os),
+            featureFlags: TestFeatureFlags(isAndroidEnabled: false),
+            httpClient: FakeHttpClient.list(<FakeRequest>[
+              FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head),
+              FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head),
+              FakeRequest(Uri.parse(macOSRequiredHttpHosts[0]), method: HttpMethod.head),
+            ]),
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.installed));
+        }
+      });
+
+      testWithoutContext('all http hosts are available - iOS disabled', () async {
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os),
+            featureFlags: TestFeatureFlags(isIOSEnabled: false),
+            httpClient: FakeHttpClient.list(<FakeRequest>[
+              FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head),
+              FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head),
+              FakeRequest(Uri.parse(androidRequiredHttpHosts[0]), method: HttpMethod.head),
+            ]),
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.installed));
+        }
+      });
+
+      testWithoutContext('all http hosts are available - android, iOS disabled', () async {
+        // Run the check for all operating systems one by one
+        for(final String os in osTested) {
+          final HttpHostValidator httpHostValidator = HttpHostValidator(
+            platform: FakePlatform(operatingSystem: os),
+            featureFlags: TestFeatureFlags(isAndroidEnabled: false, isIOSEnabled: false),
+            httpClient: FakeHttpClient.list(<FakeRequest>[
+              FakeRequest(Uri.parse(kgCloudHttpHost), method: HttpMethod.head),
+              FakeRequest(Uri.parse(kPubDevHttpHost), method: HttpMethod.head),
+            ]),
+          );
+
+          // Run the validation check and get the results
+          final ValidationResult result = await httpHostValidator.validate();
+
+          // Check for a ValidationType.installed result
+          expect(result.type, equals(ValidationType.installed));
+        }
+      });
+    });
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/runner/runner_test.dart b/packages/flutter_tools/test/general.shard/runner/runner_test.dart
index 42e8b73..4001616 100644
--- a/packages/flutter_tools/test/general.shard/runner/runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/runner/runner_test.dart
@@ -12,6 +12,7 @@
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart' as io;
+import 'package:flutter_tools/src/base/net.dart';
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/base/user_messages.dart';
 import 'package:flutter_tools/src/cache.dart';
@@ -22,6 +23,7 @@
 
 import '../../src/common.dart';
 import '../../src/context.dart';
+import '../../src/fake_http_client.dart';
 
 const String kCustomBugInstructions = 'These are instructions to report with a custom bug tracker.';
 
@@ -96,6 +98,7 @@
       ProcessManager: () => FakeProcessManager.any(),
       Usage: () => CrashingUsage(),
       Artifacts: () => Artifacts.test(),
+      HttpClientFactory: () => () => FakeHttpClient.any()
     });
 
     // This Completer completes when CrashingFlutterCommand.runCommand
@@ -138,6 +141,7 @@
       ProcessManager: () => FakeProcessManager.any(),
       CrashReporter: () => WaitingCrashReporter(commandCompleter.future),
       Artifacts: () => Artifacts.test(),
+      HttpClientFactory: () => () => FakeHttpClient.any()
     });
 
     testUsingContext('create local report', () async {
@@ -206,7 +210,8 @@
       ProcessManager: () => FakeProcessManager.any(),
       UserMessages: () => CustomBugInstructions(),
       Artifacts: () => Artifacts.test(),
-      CrashReporter: () => WaitingCrashReporter(Future<void>.value())
+      CrashReporter: () => WaitingCrashReporter(Future<void>.value()),
+      HttpClientFactory: () => () => FakeHttpClient.any()
     });
   });
 }