diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 1c0e2765a2c13..fa4e5b64b1849 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -729,6 +729,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/Flutte FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java diff --git a/common/settings.h b/common/settings.h index 1f3bb0ed750df..ae52e4a7342ba 100644 --- a/common/settings.h +++ b/common/settings.h @@ -109,8 +109,10 @@ struct Settings { bool enable_dart_profiling = false; bool disable_dart_asserts = false; - // Used to signal the embedder whether HTTP connections are disabled. - bool disable_http = false; + // Whether embedder only allows secure connections. + bool may_insecurely_connect_to_all_domains = true; + // JSON-formatted domain network policy. + std::string domain_network_policy; // Used as the script URI in debug messages. Does not affect how the Dart code // is executed. diff --git a/lib/io/dart_io.cc b/lib/io/dart_io.cc index 6e5e538d74da0..d656e8b538943 100644 --- a/lib/io/dart_io.cc +++ b/lib/io/dart_io.cc @@ -16,19 +16,27 @@ using tonic::ToDart; namespace flutter { -void DartIO::InitForIsolate(bool disable_http) { - Dart_Handle result = Dart_SetNativeResolver( - Dart_LookupLibrary(ToDart("dart:io")), dart::bin::LookupIONative, - dart::bin::LookupIONativeSymbol); +void DartIO::InitForIsolate(bool may_insecurely_connect_to_all_domains, + std::string domain_network_policy) { + Dart_Handle io_lib = Dart_LookupLibrary(ToDart("dart:io")); + Dart_Handle result = Dart_SetNativeResolver(io_lib, dart::bin::LookupIONative, + dart::bin::LookupIONativeSymbol); FML_CHECK(!LogIfError(result)); - // The SDK expects this field to represent "allow http" so we switch the - // value. - Dart_Handle allow_http_value = disable_http ? Dart_False() : Dart_True(); - Dart_Handle set_field_result = - Dart_SetField(Dart_LookupLibrary(ToDart("dart:_http")), - ToDart("_embedderAllowsHttp"), allow_http_value); - FML_CHECK(!LogIfError(set_field_result)); + Dart_Handle embedder_config_type = + Dart_GetType(io_lib, ToDart("_EmbedderConfig"), 0, nullptr); + FML_CHECK(!LogIfError(embedder_config_type)); + + Dart_Handle allow_insecure_connections_result = Dart_SetField( + embedder_config_type, ToDart("_mayInsecurelyConnectToAllDomains"), + ToDart(may_insecurely_connect_to_all_domains)); + FML_CHECK(!LogIfError(allow_insecure_connections_result)); + + Dart_Handle dart_args[1]; + dart_args[0] = ToDart(domain_network_policy); + 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)); } } // namespace flutter diff --git a/lib/io/dart_io.h b/lib/io/dart_io.h index 27ce7aa65baeb..34bc8a54aaed9 100644 --- a/lib/io/dart_io.h +++ b/lib/io/dart_io.h @@ -6,6 +6,7 @@ #define FLUTTER_LIB_IO_DART_IO_H_ #include +#include #include "flutter/fml/macros.h" @@ -13,7 +14,8 @@ namespace flutter { class DartIO { public: - static void InitForIsolate(bool disable_http); + static void InitForIsolate(bool may_insecurely_connect_to_all_domains, + std::string domain_network_policy); private: FML_DISALLOW_IMPLICIT_CONSTRUCTORS(DartIO); diff --git a/runtime/dart_isolate.cc b/runtime/dart_isolate.cc index 3fff8139f65ee..06ef693b4b46b 100644 --- a/runtime/dart_isolate.cc +++ b/runtime/dart_isolate.cc @@ -139,7 +139,9 @@ DartIsolate::DartIsolate(const Settings& settings, settings.unhandled_exception_callback, DartVMRef::GetIsolateNameServer(), is_root_isolate), - disable_http_(settings.disable_http) { + may_insecurely_connect_to_all_domains_( + settings.may_insecurely_connect_to_all_domains), + domain_network_policy_(settings.domain_network_policy) { phase_ = Phase::Uninitialized; } @@ -263,7 +265,8 @@ bool DartIsolate::LoadLibraries() { tonic::DartState::Scope scope(this); - DartIO::InitForIsolate(disable_http_); + DartIO::InitForIsolate(may_insecurely_connect_to_all_domains_, + domain_network_policy_); DartUI::InitForIsolate(); diff --git a/runtime/dart_isolate.h b/runtime/dart_isolate.h index 770a99094b8ea..d3862d577ba73 100644 --- a/runtime/dart_isolate.h +++ b/runtime/dart_isolate.h @@ -398,7 +398,8 @@ class DartIsolate : public UIDartState { std::vector> kernel_buffers_; std::vector> shutdown_callbacks_; fml::RefPtr message_handling_task_runner_; - const bool disable_http_; + const bool may_insecurely_connect_to_all_domains_; + std::string domain_network_policy_; DartIsolate(const Settings& settings, TaskRunners task_runners, diff --git a/shell/common/switches.cc b/shell/common/switches.cc index e2f83f3f9c748..16494204ddf81 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -242,8 +242,11 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { } } - settings.disable_http = - command_line.HasOption(FlagForSwitch(Switch::DisableHttp)); + settings.may_insecurely_connect_to_all_domains = !command_line.HasOption( + FlagForSwitch(Switch::DisallowInsecureConnections)); + + command_line.GetOptionValue(FlagForSwitch(Switch::DomainNetworkPolicy), + &settings.domain_network_policy); // Disable need for authentication codes for VM service communication, if // specified. diff --git a/shell/common/switches.h b/shell/common/switches.h index 9b91355c33856..1110261f767c3 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -181,12 +181,15 @@ DEF_SWITCH(DisableDartAsserts, "disabled. This flag may be specified if the user wishes to run " "with assertions disabled in the debug product mode (i.e. with JIT " "or DBC).") -DEF_SWITCH(DisableHttp, - "disable-http", - "Dart VM has a master switch that can be set to disable insecure " - "HTTP and WebSocket protocols. Localhost or loopback addresses are " - "exempted. This flag can be specified if the embedder wants this " - "for a particular platform.") +DEF_SWITCH(DisallowInsecureConnections, + "disallow-insecure-connections", + "By default, dart:io allows all socket connections. If this switch " + "is set, all insecure connections are rejected.") +DEF_SWITCH(DomainNetworkPolicy, + "domain-network-policy", + "JSON encoded network policy per domain. This overrides the " + "DisallowInsecureConnections switch. Embedder can specify whether " + "to allow or disallow insecure connections at a domain level.") DEF_SWITCH( ForceMultithreading, "force-multithreading", diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 308c70af691e4..33c334ee2475a 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -153,6 +153,8 @@ android_java_sources = [ "io/flutter/embedding/engine/dart/DartExecutor.java", "io/flutter/embedding/engine/dart/DartMessenger.java", "io/flutter/embedding/engine/dart/PlatformMessageHandler.java", + "io/flutter/embedding/engine/loader/ApplicationInfoLoader.java", + "io/flutter/embedding/engine/loader/FlutterApplicationInfo.java", "io/flutter/embedding/engine/loader/FlutterLoader.java", "io/flutter/embedding/engine/loader/ResourceExtractor.java", "io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java", @@ -430,6 +432,7 @@ action("robolectric_tests") { "test/io/flutter/embedding/engine/PluginComponentTest.java", "test/io/flutter/embedding/engine/RenderingComponentTest.java", "test/io/flutter/embedding/engine/dart/DartExecutorTest.java", + "test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java", "test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java", "test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java", "test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java", diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java new file mode 100644 index 0000000000000..c29ddd9f21a9c --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java @@ -0,0 +1,161 @@ +// Copyright 2013 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. + +package io.flutter.embedding.engine.loader; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.security.NetworkSecurityPolicy; +import androidx.annotation.NonNull; +import java.io.IOException; +import org.json.JSONArray; +import org.xmlpull.v1.XmlPullParserException; + +/** Loads application information given a Context. */ +final class ApplicationInfoLoader { + // XML Attribute keys supported in AndroidManifest.xml + static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = + FlutterLoader.class.getName() + '.' + FlutterLoader.AOT_SHARED_LIBRARY_NAME; + static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + FlutterLoader.VM_SNAPSHOT_DATA_KEY; + static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = + FlutterLoader.class.getName() + '.' + FlutterLoader.ISOLATE_SNAPSHOT_DATA_KEY; + static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = + FlutterLoader.class.getName() + '.' + FlutterLoader.FLUTTER_ASSETS_DIR_KEY; + static final String NETWORK_POLICY_METADATA_KEY = "io.flutter.network-policy"; + + @NonNull + private static ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { + try { + return applicationContext + .getPackageManager() + .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + private static String getString(Bundle metadata, String key) { + if (metadata == null) { + return null; + } + return metadata.getString(key, null); + } + + private static String getNetworkPolicy(ApplicationInfo appInfo, Context context) { + // We cannot use reflection to look at networkSecurityConfigRes because + // Android throws an error when we try to access fields marked as @hide. + // Instead we rely on metadata. + Bundle metadata = appInfo.metaData; + if (metadata == null) { + return null; + } + + int networkSecurityConfigRes = metadata.getInt(NETWORK_POLICY_METADATA_KEY, 0); + if (networkSecurityConfigRes <= 0) { + return null; + } + + JSONArray output = new JSONArray(); + try { + XmlResourceParser xrp = context.getResources().getXml(networkSecurityConfigRes); + xrp.next(); + int eventType = xrp.getEventType(); + while (eventType != XmlResourceParser.END_DOCUMENT) { + if (eventType == XmlResourceParser.START_TAG) { + if (xrp.getName().equals("domain-config")) { + parseDomainConfig(xrp, output, false); + } + } + eventType = xrp.next(); + } + } catch (IOException | XmlPullParserException e) { + return null; + } + return output.toString(); + } + + private static boolean getUseEmbeddedView(ApplicationInfo appInfo) { + Bundle bundle = appInfo.metaData; + return bundle != null && bundle.getBoolean("io.flutter.embedded_views_preview"); + } + + private static void parseDomainConfig( + XmlResourceParser xrp, JSONArray output, boolean inheritedCleartextPermitted) + throws IOException, XmlPullParserException { + boolean cleartextTrafficPermitted = + xrp.getAttributeBooleanValue( + null, "cleartextTrafficPermitted", inheritedCleartextPermitted); + while (true) { + int eventType = xrp.next(); + if (eventType == XmlResourceParser.START_TAG) { + if (xrp.getName().equals("domain")) { + // There can be multiple domains. + parseDomain(xrp, output, cleartextTrafficPermitted); + } else if (xrp.getName().equals("domain-config")) { + parseDomainConfig(xrp, output, cleartextTrafficPermitted); + } else { + skipTag(xrp); + } + } else if (eventType == XmlResourceParser.END_TAG) { + break; + } + } + } + + private static void skipTag(XmlResourceParser xrp) throws IOException, XmlPullParserException { + String name = xrp.getName(); + int eventType = xrp.getEventType(); + while (eventType != XmlResourceParser.END_TAG || xrp.getName() != name) { + eventType = xrp.next(); + } + } + + private static void parseDomain( + XmlResourceParser xrp, JSONArray output, boolean cleartextPermitted) + throws IOException, XmlPullParserException { + boolean includeSubDomains = xrp.getAttributeBooleanValue(null, "includeSubdomains", false); + xrp.next(); + if (xrp.getEventType() != XmlResourceParser.TEXT) { + throw new IllegalStateException("Expected text"); + } + String domain = xrp.getText().trim(); + JSONArray outputArray = new JSONArray(); + outputArray.put(domain); + outputArray.put(includeSubDomains); + outputArray.put(cleartextPermitted); + output.put(outputArray); + xrp.next(); + if (xrp.getEventType() != XmlResourceParser.END_TAG) { + throw new IllegalStateException("Expected end of domain tag"); + } + } + + /** + * Initialize our Flutter config values by obtaining them from the manifest XML file, falling back + * to default values. + */ + @NonNull + public static FlutterApplicationInfo load(@NonNull Context applicationContext) { + ApplicationInfo appInfo = getApplicationInfo(applicationContext); + // Prior to API 23, cleartext traffic is allowed. + boolean clearTextPermitted = true; + if (android.os.Build.VERSION.SDK_INT >= 23) { + clearTextPermitted = NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(); + } + + return new FlutterApplicationInfo( + getString(appInfo.metaData, PUBLIC_AOT_SHARED_LIBRARY_NAME), + getString(appInfo.metaData, PUBLIC_VM_SNAPSHOT_DATA_KEY), + getString(appInfo.metaData, PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY), + getString(appInfo.metaData, PUBLIC_FLUTTER_ASSETS_DIR_KEY), + getNetworkPolicy(appInfo, applicationContext), + appInfo.nativeLibraryDir, + clearTextPermitted, + getUseEmbeddedView(appInfo)); + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java new file mode 100644 index 0000000000000..3d5c2b10c40d6 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java @@ -0,0 +1,46 @@ +// Copyright 2013 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. + +package io.flutter.embedding.engine.loader; + +/** Encapsulates all the information that Flutter needs from application manifest. */ +public final class FlutterApplicationInfo { + private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; + private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; + private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; + private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; + + final String aotSharedLibraryName; + final String vmSnapshotData; + final String isolateSnapshotData; + final String flutterAssetsDir; + final String domainNetworkPolicy; + final String nativeLibraryDir; + final boolean clearTextPermitted; + // TODO(cyanlaz): Remove this when dynamic thread merging is done. + // https://github.com/flutter/flutter/issues/59930 + final boolean useEmbeddedView; + + public FlutterApplicationInfo( + String aotSharedLibraryName, + String vmSnapshotData, + String isolateSnapshotData, + String flutterAssetsDir, + String domainNetworkPolicy, + String nativeLibraryDir, + boolean clearTextPermitted, + boolean useEmbeddedView) { + this.aotSharedLibraryName = + aotSharedLibraryName == null ? DEFAULT_AOT_SHARED_LIBRARY_NAME : aotSharedLibraryName; + this.vmSnapshotData = vmSnapshotData == null ? DEFAULT_VM_SNAPSHOT_DATA : vmSnapshotData; + this.isolateSnapshotData = + isolateSnapshotData == null ? DEFAULT_ISOLATE_SNAPSHOT_DATA : isolateSnapshotData; + this.flutterAssetsDir = + flutterAssetsDir == null ? DEFAULT_FLUTTER_ASSETS_DIR : flutterAssetsDir; + this.nativeLibraryDir = nativeLibraryDir; + this.domainNetworkPolicy = domainNetworkPolicy == null ? "" : domainNetworkPolicy; + this.clearTextPermitted = clearTextPermitted; + this.useEmbeddedView = useEmbeddedView; + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index 9156a64ef061e..2aee56dee94e3 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -5,10 +5,8 @@ package io.flutter.embedding.engine.loader; import android.content.Context; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; -import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -31,35 +29,15 @@ public class FlutterLoader { private static final String TAG = "FlutterLoader"; // Must match values in flutter::switches - private static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; - private static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; - private static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; - private static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; - private static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; - - // XML Attribute keys supported in AndroidManifest.xml - private static final String PUBLIC_AOT_SHARED_LIBRARY_NAME = - FlutterLoader.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME; - private static final String PUBLIC_VM_SNAPSHOT_DATA_KEY = - FlutterLoader.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY; - private static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY = - FlutterLoader.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_KEY; - private static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY = - FlutterLoader.class.getName() + '.' + FLUTTER_ASSETS_DIR_KEY; + static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; + static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; + static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data"; + static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data"; + static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir"; // Resource names used for components of the precompiled snapshot. - private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so"; - private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data"; - private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data"; private static final String DEFAULT_LIBRARY = "libflutter.so"; private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin"; - private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets"; - - // Mutable because default values can be overridden via config properties - private String aotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME; - private String vmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA; - private String isolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA; - private String flutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR; private static FlutterLoader instance; @@ -78,9 +56,17 @@ public static FlutterLoader getInstance() { return instance; } + @NonNull + public static FlutterLoader getInstanceForTest(FlutterApplicationInfo flutterApplicationInfo) { + FlutterLoader loader = new FlutterLoader(); + loader.flutterApplicationInfo = flutterApplicationInfo; + return loader; + } + private boolean initialized = false; @Nullable private Settings settings; private long initStartTimestampMillis; + private FlutterApplicationInfo flutterApplicationInfo; private static class InitResult { final String appStoragePath; @@ -131,7 +117,7 @@ public void startInitialization(@NonNull Context applicationContext, @NonNull Se this.settings = settings; initStartTimestampMillis = SystemClock.uptimeMillis(); - initConfig(appContext); + flutterApplicationInfo = ApplicationInfoLoader.load(appContext); VsyncWaiter.getInstance((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE)) .init(); @@ -195,10 +181,9 @@ public void ensureInitializationComplete( List shellArgs = new ArrayList<>(); shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat"); - ApplicationInfo applicationInfo = getApplicationInfo(applicationContext); shellArgs.add( "--icu-native-lib-path=" - + applicationInfo.nativeLibraryDir + + flutterApplicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY); if (args != null) { @@ -207,13 +192,16 @@ public void ensureInitializationComplete( String kernelPath = null; if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) { - String snapshotAssetPath = result.dataDirPath + File.separator + flutterAssetsDir; + String snapshotAssetPath = + result.dataDirPath + File.separator + flutterApplicationInfo.flutterAssetsDir; kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB; shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath); - shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData); - shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData); + shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.vmSnapshotData); + shellArgs.add( + "--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + flutterApplicationInfo.isolateSnapshotData); } else { - shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName); + shellArgs.add( + "--" + AOT_SHARED_LIBRARY_NAME + "=" + flutterApplicationInfo.aotSharedLibraryName); // Most devices can load the AOT shared library based on the library name // with no directory path. Provide a fully qualified path to the library @@ -222,28 +210,28 @@ public void ensureInitializationComplete( "--" + AOT_SHARED_LIBRARY_NAME + "=" - + applicationInfo.nativeLibraryDir + + flutterApplicationInfo.nativeLibraryDir + File.separator - + aotSharedLibraryName); + + flutterApplicationInfo.aotSharedLibraryName); } shellArgs.add("--cache-dir-path=" + result.engineCachesPath); + // TODO(mehmetf): Announce this since it is a breaking change then enable it. + // if (!flutterApplicationInfo.clearTextPermitted) { + // shellArgs.add("--disallow-insecure-connections"); + // } + if (flutterApplicationInfo.domainNetworkPolicy != null) { + shellArgs.add("--domain-network-policy=" + flutterApplicationInfo.domainNetworkPolicy); + } + if (flutterApplicationInfo.useEmbeddedView) { + shellArgs.add("--use-embedded-view"); + } if (settings.getLogTag() != null) { shellArgs.add("--log-tag=" + settings.getLogTag()); } long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; - // TODO(cyanlaz): Remove this when dynamic thread merging is done. - // https://github.com/flutter/flutter/issues/59930 - Bundle bundle = applicationInfo.metaData; - if (bundle != null) { - boolean use_embedded_view = bundle.getBoolean("io.flutter.embedded_views_preview"); - if (use_embedded_view) { - shellArgs.add("--use-embedded-view"); - } - } - FlutterJNI.nativeInit( applicationContext, shellArgs.toArray(new String[0]), @@ -306,40 +294,6 @@ public void run() { }); } - @NonNull - private ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) { - try { - return applicationContext - .getPackageManager() - .getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA); - } catch (PackageManager.NameNotFoundException e) { - throw new RuntimeException(e); - } - } - - /** - * Initialize our Flutter config values by obtaining them from the manifest XML file, falling back - * to default values. - */ - private void initConfig(@NonNull Context applicationContext) { - Bundle metadata = getApplicationInfo(applicationContext).metaData; - - // There isn't a `` tag as a direct child of `` in - // `AndroidManifest.xml`. - if (metadata == null) { - return; - } - - aotSharedLibraryName = - metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME); - flutterAssetsDir = - metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR); - - vmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA); - isolateSnapshotData = - metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA); - } - /** Extract assets out of the APK that need to be cached as uncompressed files on disk. */ private ResourceExtractor initResources(@NonNull Context applicationContext) { ResourceExtractor resourceExtractor = null; @@ -354,8 +308,8 @@ private ResourceExtractor initResources(@NonNull Context applicationContext) { // In debug/JIT mode these assets will be written to disk and then // mapped into memory so they can be provided to the Dart VM. resourceExtractor - .addResource(fullAssetPathFrom(vmSnapshotData)) - .addResource(fullAssetPathFrom(isolateSnapshotData)) + .addResource(fullAssetPathFrom(flutterApplicationInfo.vmSnapshotData)) + .addResource(fullAssetPathFrom(flutterApplicationInfo.isolateSnapshotData)) .addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB)); resourceExtractor.start(); @@ -365,7 +319,7 @@ private ResourceExtractor initResources(@NonNull Context applicationContext) { @NonNull public String findAppBundlePath() { - return flutterAssetsDir; + return flutterApplicationInfo.flutterAssetsDir; } /** @@ -396,7 +350,7 @@ public String getLookupKeyForAsset(@NonNull String asset, @NonNull String packag @NonNull private String fullAssetPathFrom(@NonNull String filePath) { - return flutterAssetsDir + File.separator + filePath; + return flutterApplicationInfo.flutterAssetsDir + File.separator + filePath; } public static class Settings { diff --git a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java index f4860fd62e845..01d012c38b83a 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.loader.FlutterApplicationInfo; import io.flutter.embedding.engine.loader.FlutterLoader; import io.flutter.embedding.engine.plugins.FlutterPlugin; import org.junit.Test; @@ -26,6 +27,8 @@ public void pluginsCanAccessFlutterAssetPaths() { // Setup test. FlutterJNI flutterJNI = mock(FlutterJNI.class); when(flutterJNI.isAttached()).thenReturn(true); + FlutterApplicationInfo emptyInfo = + new FlutterApplicationInfo(null, null, null, null, null, null, false, false); // FlutterLoader is the object to which the PluginRegistry defers for obtaining // the path to a Flutter asset. Ideally in this component test we would use a @@ -44,7 +47,8 @@ public void pluginsCanAccessFlutterAssetPaths() { public String answer(InvocationOnMock invocation) throws Throwable { // Defer to a real FlutterLoader to return the asset path. String fileNameOrSubpath = (String) invocation.getArguments()[0]; - return FlutterLoader.getInstance().getLookupKeyForAsset(fileNameOrSubpath); + return FlutterLoader.getInstanceForTest(emptyInfo) + .getLookupKeyForAsset(fileNameOrSubpath); } }); when(flutterLoader.getLookupKeyForAsset(any(String.class), any(String.class))) @@ -55,7 +59,7 @@ public String answer(InvocationOnMock invocation) throws Throwable { // Defer to a real FlutterLoader to return the asset path. String fileNameOrSubpath = (String) invocation.getArguments()[0]; String packageName = (String) invocation.getArguments()[1]; - return FlutterLoader.getInstance() + return FlutterLoader.getInstanceForTest(emptyInfo) .getLookupKeyForAsset(fileNameOrSubpath, packageName); } }); diff --git a/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java b/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java new file mode 100644 index 0000000000000..769525f7db3c9 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java @@ -0,0 +1,193 @@ +package io.flutter.embedding.engine.loader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.os.Bundle; +import android.security.NetworkSecurityPolicy; +import java.io.StringReader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserFactory; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +public class ApplicationInfoLoaderTest { + + @Test + public void itGeneratesCorrectApplicationInfoWithDefaultManifest() { + FlutterApplicationInfo info = ApplicationInfoLoader.load(RuntimeEnvironment.application); + assertNotNull(info); + assertEquals("libapp.so", info.aotSharedLibraryName); + assertEquals("vm_snapshot_data", info.vmSnapshotData); + assertEquals("isolate_snapshot_data", info.isolateSnapshotData); + assertEquals("flutter_assets", info.flutterAssetsDir); + assertEquals("", info.domainNetworkPolicy); + assertNull(info.nativeLibraryDir); + assertEquals(true, info.clearTextPermitted); + assertEquals(false, info.useEmbeddedView); + } + + @Config(shadows = {ApplicationInfoLoaderTest.ShadowNetworkSecurityPolicy.class}) + @Test + public void itVotesAgainstClearTextIfSecurityPolicySaysSo() { + FlutterApplicationInfo info = ApplicationInfoLoader.load(RuntimeEnvironment.application); + assertNotNull(info); + assertEquals(false, info.clearTextPermitted); + } + + @Implements(NetworkSecurityPolicy.class) + public static class ShadowNetworkSecurityPolicy { + @Implementation + public boolean isCleartextTrafficPermitted() { + return false; + } + } + + private Context generateMockContext(Bundle metadata, String networkPolicyXml) throws Exception { + Context context = mock(Context.class); + PackageManager packageManager = mock(PackageManager.class); + ApplicationInfo applicationInfo = mock(ApplicationInfo.class); + applicationInfo.metaData = metadata; + Resources resources = mock(Resources.class); + when(context.getPackageManager()).thenReturn(packageManager); + when(context.getResources()).thenReturn(resources); + when(packageManager.getApplicationInfo(any(String.class), any(int.class))) + .thenReturn(applicationInfo); + if (networkPolicyXml != null) { + metadata.putInt(ApplicationInfoLoader.NETWORK_POLICY_METADATA_KEY, 5); + doAnswer(invocationOnMock -> createMockResourceParser(networkPolicyXml)) + .when(resources) + .getXml(5); + } + return context; + } + + @Test + public void itGeneratesCorrectApplicationInfoWithCustomValues() throws Exception { + Bundle bundle = new Bundle(); + bundle.putString(ApplicationInfoLoader.PUBLIC_AOT_SHARED_LIBRARY_NAME, "testaot"); + bundle.putString(ApplicationInfoLoader.PUBLIC_VM_SNAPSHOT_DATA_KEY, "testvmsnapshot"); + bundle.putString(ApplicationInfoLoader.PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, "testisolatesnapshot"); + bundle.putString(ApplicationInfoLoader.PUBLIC_FLUTTER_ASSETS_DIR_KEY, "testassets"); + bundle.putBoolean("io.flutter.embedded_views_preview", true); + Context context = generateMockContext(bundle, null); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals("testaot", info.aotSharedLibraryName); + assertEquals("testvmsnapshot", info.vmSnapshotData); + assertEquals("testisolatesnapshot", info.isolateSnapshotData); + assertEquals("testassets", info.flutterAssetsDir); + assertNull(info.nativeLibraryDir); + assertEquals("", info.domainNetworkPolicy); + assertEquals(true, info.useEmbeddedView); + } + + @Test + public void itGeneratesCorrectNetworkPolicy() throws Exception { + Bundle bundle = new Bundle(); + String networkPolicyXml = + "" + + "" + + "secure.example.com" + + "" + + ""; + Context context = generateMockContext(bundle, networkPolicyXml); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals("[[\"secure.example.com\",true,false]]", info.domainNetworkPolicy); + } + + @Test + public void itHandlesBogusInformationInNetworkPolicy() throws Exception { + Bundle bundle = new Bundle(); + String networkPolicyXml = + "" + + "" + + "secure.example.com" + + "" + + "7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=" + + "" + + "fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=" + + "" + + "" + + ""; + Context context = generateMockContext(bundle, networkPolicyXml); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals("[[\"secure.example.com\",true,false]]", info.domainNetworkPolicy); + } + + @Test + public void itHandlesNestedSubDomains() throws Exception { + Bundle bundle = new Bundle(); + String networkPolicyXml = + "" + + "" + + "example.com" + + "" + + "insecure.example.com" + + "" + + "" + + "secure.example.com" + + "" + + "" + + ""; + Context context = generateMockContext(bundle, networkPolicyXml); + FlutterApplicationInfo info = ApplicationInfoLoader.load(context); + assertNotNull(info); + assertEquals( + "[[\"example.com\",true,true],[\"insecure.example.com\",true,true],[\"secure.example.com\",true,false]]", + info.domainNetworkPolicy); + } + + // The following ridiculousness is needed because Android gives no way for us + // to customize XmlResourceParser. We have to mock it and tie each method + // we use to an actual Xml parser. + private XmlResourceParser createMockResourceParser(String xml) throws Exception { + final XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); + xpp.setInput(new StringReader(xml)); + XmlResourceParser resourceParser = mock(XmlResourceParser.class); + final Answer invokeMethodOnRealParser = + invocation -> invocation.getMethod().invoke(xpp, invocation.getArguments()); + when(resourceParser.next()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getName()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getEventType()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getText()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeCount()).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeName(anyInt())).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeValue(anyInt())).thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeValue(any(String.class), any(String.class))) + .thenAnswer(invokeMethodOnRealParser); + when(resourceParser.getAttributeBooleanValue( + any(String.class), any(String.class), any(Boolean.class))) + .thenAnswer( + invocation -> { + Object[] args = invocation.getArguments(); + String result = xpp.getAttributeValue((String) args[0], (String) args[1]); + if (result == null) { + return (Boolean) args[2]; + } + return Boolean.parseBoolean(result); + }); + return resourceParser; + } +} diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm index c9bf2ba962e16..3fb7c350febd0 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -26,6 +26,42 @@ static const char* kApplicationKernelSnapshotFileName = "kernel_blob.bin"; +// TODO(mehmetf): Announce this since it is breaking change then enable it. +// static NSString* DomainNetworkPolicy(NSDictionary* appTransportSecurity) { +// if (appTransportSecurity == nil) { +// return @""; +// } +// // +// https://developer.apple.com/documentation/bundleresources/information_property_list/nsapptransportsecurity/nsexceptiondomains +// NSDictionary* exceptionDomains = [appTransportSecurity objectForKey:@"NSExceptionDomains"]; +// if (exceptionDomains == nil) { +// return @""; +// } +// NSMutableArray* networkConfigArray = [[NSMutableArray alloc] init]; +// for (NSString* domain in exceptionDomains) { +// NSDictionary* domainConfiguration = [exceptionDomains objectForKey:domain]; +// BOOL includesSubDomains = +// [[domainConfiguration objectForKey:@"NSIncludesSubdomains"] boolValue]; +// BOOL allowsCleartextCommunication = +// [[domainConfiguration objectForKey:@"NSExceptionAllowsInsecureHTTPLoads"] boolValue]; +// [networkConfigArray addObject:[NSArray arrayWithObjects:domain, includesSubDomains, +// allowsCleartextCommunication, nil]]; +// } +// NSData* jsonData = [NSJSONSerialization dataWithJSONObject:networkConfigArray +// options:0 +// error:NULL]; +// return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +// } + +// TODO(mehmetf): Announce this since it is breaking change then enable it. +// static bool AllowsArbitraryLoads(NSDictionary* appTransportSecurity) { +// if (appTransportSecurity != nil) { +// return [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue]; +// } else { +// return false; +// } +// } + static flutter::Settings DefaultSettingsForProcess(NSBundle* bundle = nil) { auto command_line = flutter::CommandLineFromNSProcessInfo(); @@ -132,6 +168,13 @@ } } + // TODO(mehmetf): Announce this since it is breaking change then enable it. + // Domain network configuration + // NSDictionary* appTransportSecurity = + // [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"]; + // settings.may_insecurely_connect_to_all_domains = AllowsArbitraryLoads(appTransportSecurity); + // settings.domain_network_policy = DomainNetworkPolicy(appTransportSecurity).UTF8String; + #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG // There are no ownership concerns here as all mappings are owned by the // embedder and not the engine.