From 90235a8c25d753da1a8cf3eb05b48d82c9fb14d9 Mon Sep 17 00:00:00 2001 From: Alexander Aprelev Date: Tue, 25 May 2021 09:08:23 -0700 Subject: [PATCH] Reland "Provide better messaging when user attempts to use non-secure http connection. (#26226)" (#26400) This reverts commit c7ef259e14d7fc93892c5389ef363589d27ac6cb as dart sdk with corresponding changes has landed in internal repo. --- lib/io/dart_io.cc | 13 +++ lib/ui/hooks.dart | 41 +++++++++ .../http_allow_http_connections_test.dart | 30 +++++++ .../http_disallow_http_connections_test.dart | 86 +++++++++++++++++++ .../dart/window_hooks_integration_test.dart | 1 + testing/run_tests.py | 5 ++ 6 files changed, 176 insertions(+) create mode 100644 testing/dart/http_allow_http_connections_test.dart create mode 100644 testing/dart/http_disallow_http_connections_test.dart diff --git a/lib/io/dart_io.cc b/lib/io/dart_io.cc index a4dfa9351c095..017054a5388fb 100644 --- a/lib/io/dart_io.cc +++ b/lib/io/dart_io.cc @@ -36,6 +36,19 @@ void DartIO::InitForIsolate(bool may_insecurely_connect_to_all_domains, Dart_Handle set_domain_network_policy_result = Dart_Invoke( embedder_config_type, ToDart("_setDomainPolicies"), 1, dart_args); FML_CHECK(!LogIfError(set_domain_network_policy_result)); + + Dart_Handle ui_lib = Dart_LookupLibrary(ToDart("dart:ui")); + Dart_Handle dart_validate_args[1]; + dart_validate_args[0] = ToDart(may_insecurely_connect_to_all_domains); + Dart_Handle http_connection_hook_closure = + Dart_Invoke(ui_lib, ToDart("_getHttpConnectionHookClosure"), + /*number_of_arguments=*/1, dart_validate_args); + FML_CHECK(!LogIfError(http_connection_hook_closure)); + Dart_Handle http_lib = Dart_LookupLibrary(ToDart("dart:_http")); + FML_CHECK(!LogIfError(http_lib)); + Dart_Handle set_http_connection_hook_result = Dart_SetField( + http_lib, ToDart("_httpConnectionHook"), http_connection_hook_closure); + FML_CHECK(!LogIfError(set_http_connection_hook_result)); } } // namespace flutter diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 79299a60ed3aa..74c704042e229 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -224,3 +224,44 @@ void _invoke3(void Function(A1 a1, A2 a2, A3 a3)? callback, Zone zon }); } } + +bool _isLoopback(String host) { + if (host.isEmpty) { + return false; + } + if ('localhost' == host) { + return true; + } + try { + return InternetAddress(host).isLoopback; + } on ArgumentError { + return false; + } +} + +/// Loopback connections are always allowed. +/// Zone override with 'flutter.io.allow_http' takes first priority. +/// If zone override is not provided, engine setting is checked. +@pragma('vm:entry-point') +// ignore: unused_element +void Function(Uri) _getHttpConnectionHookClosure(bool mayInsecurelyConnectToAllDomains) { + return (Uri uri) { + if (_isLoopback(uri.host)) { + return; + } + final dynamic zoneOverride = Zone.current[#flutter.io.allow_http]; + if (zoneOverride == true) { + return; + } + if (zoneOverride == false && uri.isScheme('http')) { + // Going to throw + } else if (mayInsecurelyConnectToAllDomains || uri.isScheme('https')) { + // In absence of zone override, if engine setting allows the connection + // or if connection is to `https`, allow the connection. + return; + } + throw UnsupportedError( + 'Non-https connection "$uri" is not supported by the platform. ' + 'Refer to https://flutter.dev/docs/release/breaking-changes/network-policy-ios-android.'); + }; +} diff --git a/testing/dart/http_allow_http_connections_test.dart b/testing/dart/http_allow_http_connections_test.dart new file mode 100644 index 0000000000000..299dd3cee6357 --- /dev/null +++ b/testing/dart/http_allow_http_connections_test.dart @@ -0,0 +1,30 @@ +// Copyright 2021 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. + +// @dart = 2.9 + +import 'dart:async'; +import 'dart:io'; + +import 'package:litetest/litetest.dart'; + +import 'http_disallow_http_connections_test.dart'; + +void main() { + test('Normal HTTP request succeeds', () async { + final String host = await getLocalHostIP(); + await bindServerAndTest(host, (HttpClient httpClient, Uri uri) async { + await httpClient.getUrl(uri); + }); + }); + + test('We can ban HTTP explicitly.', () async { + final String host = await getLocalHostIP(); + await bindServerAndTest(host, (HttpClient httpClient, Uri uri) async { + asyncExpectThrows( + () async => runZoned(() => httpClient.getUrl(uri), + zoneValues: {#flutter.io.allow_http: false})); + }); + }); +} diff --git a/testing/dart/http_disallow_http_connections_test.dart b/testing/dart/http_disallow_http_connections_test.dart new file mode 100644 index 0000000000000..cc740d5887d2c --- /dev/null +++ b/testing/dart/http_disallow_http_connections_test.dart @@ -0,0 +1,86 @@ +// Copyright 2021 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. + +// @dart = 2.9 + +// FlutterTesterOptions=--disallow-insecure-connections + +import 'dart:async'; +import 'dart:io'; + +import 'package:litetest/litetest.dart'; + +/// Asserts that `callback` throws an exception of type `T`. +Future asyncExpectThrows(Function callback) async { + bool threw = false; + try { + await callback(); + } catch (e) { + expect(e is T, true); + threw = true; + } + expect(threw, true); +} + +Future getLocalHostIP() async { + final List interfaces = await NetworkInterface.list( + includeLoopback: false, type: InternetAddressType.IPv4); + return interfaces.first.addresses.first.address; +} + +Future bindServerAndTest(String serverHost, + Future Function(HttpClient client, Uri uri) testCode) async { + final HttpClient httpClient = HttpClient(); + final HttpServer server = await HttpServer.bind(serverHost, 0); + final Uri uri = Uri(scheme: 'http', host: serverHost, port: server.port); + try { + await testCode(httpClient, uri); + } finally { + httpClient.close(force: true); + await server.close(); + } +} + +/// Answers the question whether this computer supports binding to IPv6 addresses. +Future _supportsIPv6() async { + try { + final ServerSocket socket = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0); + await socket.close(); + return true; + } on SocketException catch (_) { + return false; + } +} + +void main() { + test('testWithHostname', () async { + await bindServerAndTest(await getLocalHostIP(), (HttpClient httpClient, Uri httpUri) async { + asyncExpectThrows( + () async => httpClient.getUrl(httpUri)); + asyncExpectThrows( + () async => runZoned(() => httpClient.getUrl(httpUri), + zoneValues: {#flutter.io.allow_http: 'foo'})); + asyncExpectThrows( + () async => runZoned(() => httpClient.getUrl(httpUri), + zoneValues: {#flutter.io.allow_http: false})); + await runZoned(() => httpClient.getUrl(httpUri), + zoneValues: {#flutter.io.allow_http: true}); + }); + }); + + test('testWithLoopback', () async { + await bindServerAndTest('127.0.0.1', (HttpClient httpClient, Uri uri) async { + await httpClient.getUrl(Uri.parse('http://localhost:${uri.port}')); + await httpClient.getUrl(Uri.parse('http://127.0.0.1:${uri.port}')); + }); + }); + + test('testWithIPV6', () async { + if (await _supportsIPv6()) { + await bindServerAndTest('::1', (HttpClient httpClient, Uri uri) async { + await httpClient.getUrl(uri); + }); + } + }); +} diff --git a/testing/dart/window_hooks_integration_test.dart b/testing/dart/window_hooks_integration_test.dart index bb0030eee98fd..9cbc720473f56 100644 --- a/testing/dart/window_hooks_integration_test.dart +++ b/testing/dart/window_hooks_integration_test.dart @@ -13,6 +13,7 @@ import 'dart:collection' as collection; import 'dart:convert'; import 'dart:developer' as developer; import 'dart:math' as math; +import 'dart:io' show InternetAddress; import 'dart:nativewrappers'; import 'dart:typed_data'; diff --git a/testing/run_tests.py b/testing/run_tests.py index 5f7e824505257..418fa864f0a0a 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -259,6 +259,11 @@ def RunDartTest(build_dir, test_packages, dart_file, verbose_dart_snapshot, mult if not enable_observatory: command_args.append('--disable-observatory') + dart_file_contents = open(dart_file, 'r') + custom_options = re.findall("// FlutterTesterOptions=(.*)", dart_file_contents.read()) + dart_file_contents.close() + command_args.extend(custom_options) + command_args += [ '--use-test-fonts', kernel_file_output