[web] Use TrustedTypes in flutter.js and other tools (#112969)

This commit is contained in:
David Iglesias 2022-10-21 09:03:51 -07:00 committed by GitHub
parent 782baecc50
commit 883469229e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 7 deletions

View file

@ -25,12 +25,18 @@ final String _targetWithBlockedServiceWorkers = path.join('lib', 'service_worker
final String _targetPath = path.join(_testAppDirectory, _target);
enum ServiceWorkerTestType {
// Mocks how FF disables service workers.
blockedServiceWorkers,
// Drops the main.dart.js directly on the page.
withoutFlutterJs,
// Uses the standard, promise-based, flutterJS initialization.
withFlutterJs,
// Uses the shorthand engineInitializer.autoStart();
withFlutterJsShort,
// Uses onEntrypointLoaded callback instead of returned promise.
withFlutterJsEntrypointLoadedEvent,
// Same as withFlutterJsEntrypointLoadedEvent, but with TrustedTypes enabled.
withFlutterJsTrustedTypesOn,
// Entrypoint generated by `flutter create`.
generatedEntrypoint,
}
@ -44,10 +50,12 @@ Future<void> main() async {
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn);
await runWebServiceWorkerTestWithGeneratedEntrypoint(headless: false);
await runWebServiceWorkerTestWithBlockedServiceWorkers(headless: false);
@ -112,6 +120,9 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) {
case ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent:
indexFile = 'index_with_flutterjs_entrypoint_loaded.html';
break;
case ServiceWorkerTestType.withFlutterJsTrustedTypesOn:
indexFile = 'index_with_flutterjs_el_tt_on.html';
break;
case ServiceWorkerTestType.generatedEntrypoint:
indexFile = 'generated_entrypoint.html';
break;

View file

@ -1195,10 +1195,12 @@ Future<void> _runWebLongRunningTests() async {
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent),
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn),
() => runWebServiceWorkerTestWithGeneratedEntrypoint(headless: true),
() => runWebServiceWorkerTestWithBlockedServiceWorkers(headless: true),
() => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'),

View file

@ -0,0 +1,46 @@
<!DOCTYPE HTML>
<!-- 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. -->
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<title>Integration test. App load with flutter.js and onEntrypointLoaded API. Trusted Types enabled.</title>
<!-- Enable TrustedTypes for 'script'-->
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test">
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
onEntrypointLoaded: onEntrypointLoaded,
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
}
});
// Once the entrypoint is ready, do things!
async function onEntrypointLoaded(engineInitializer) {
const appRunner = await engineInitializer.initializeEngine();
appRunner.runApp();
}
});
</script>
</body>
</html>

View file

@ -94,18 +94,45 @@ document.addEventListener('dart-app-ready', function (e) {
styleSheet.parentNode.removeChild(styleSheet);
});
// A map containing the URLs for the bootstrap scripts in debug.
let _scriptUrls = {
"mapper": "$mapperUrl",
"requireJs": "$requireUrl"
};
// Create a TrustedTypes policy so we can attach Scripts...
let _ttPolicy;
if (window.trustedTypes) {
_ttPolicy = trustedTypes.createPolicy("flutter-tools-bootstrap", {
createScriptURL: (url) => {
let scriptUrl = _scriptUrls[url];
if (!scriptUrl) {
console.error("Unknown Flutter Web bootstrap resource!", url);
}
return scriptUrl;
}
});
}
// Creates a TrustedScriptURL for a given `scriptName`.
// See `_scriptUrls` and `_ttPolicy` above.
function getTTScriptUrl(scriptName) {
let defaultUrl = _scriptUrls[scriptName];
return _ttPolicy ? _ttPolicy.createScriptURL(scriptName) : defaultUrl;
}
// Attach source mapping.
var mapperEl = document.createElement("script");
mapperEl.defer = true;
mapperEl.async = false;
mapperEl.src = "$mapperUrl";
mapperEl.src = getTTScriptUrl("mapper");
document.head.appendChild(mapperEl);
// Attach require JS.
var requireEl = document.createElement("script");
requireEl.defer = true;
requireEl.async = false;
requireEl.src = "$requireUrl";
requireEl.src = getTTScriptUrl("requireJs");
// This attribute tells require JS what to load as main (defined below).
requireEl.setAttribute("data-main", "main_module.bootstrap");
document.head.appendChild(requireEl);

View file

@ -56,12 +56,53 @@ _flutter.loader = null;
});
}
/**
* Handles the creation of a TrustedTypes `policy` that validates URLs based
* on an (optional) incoming array of RegExes.
*/
class FlutterTrustedTypesPolicy {
/**
* Constructs the policy.
* @param {[RegExp]} validPatterns the patterns to test URLs
* @param {String} policyName the policy name (optional)
*/
constructor(validPatterns, policyName = "flutter-js") {
const patterns = validPatterns || [
/\.dart\.js$/,
/^flutter_service_worker.js$/
];
if (window.trustedTypes) {
this.policy = trustedTypes.createPolicy(policyName, {
createScriptURL: function(url) {
const parsed = new URL(url, window.location);
const file = parsed.pathname.split("/").pop();
const matches = patterns.some((pattern) => pattern.test(file));
if (matches) {
return parsed.toString();
}
console.error(
"URL rejected by TrustedTypes policy",
policyName, ":", url, "(download prevented)");
}
});
}
}
}
/**
* Handles loading/reloading Flutter's service worker, if configured.
*
* @see: https://developers.google.com/web/fundamentals/primers/service-workers
*/
class FlutterServiceWorkerLoader {
/**
* Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
* @param {TrustedTypesPolicy | undefined} policy
*/
setTrustedTypesPolicy(policy) {
this._ttPolicy = policy;
}
/**
* Returns a Promise that resolves when the latest Flutter service worker,
* configured by `settings` has been loaded and activated.
@ -84,8 +125,14 @@ _flutter.loader = null;
timeoutMillis = 4000,
} = settings;
// Apply the TrustedTypes policy, if present.
let url = serviceWorkerUrl;
if (this._ttPolicy != null) {
url = this._ttPolicy.createScriptURL(url);
}
const serviceWorkerActivation = navigator.serviceWorker
.register(serviceWorkerUrl)
.register(url)
.then(this._getNewServiceWorker)
.then(this._waitForServiceWorkerActivation);
@ -173,6 +220,14 @@ _flutter.loader = null;
this._scriptLoaded = false;
}
/**
* Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
* @param {TrustedTypesPolicy | undefined} policy
*/
setTrustedTypesPolicy(policy) {
this._ttPolicy = policy;
}
/**
* Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a
* user-specified `onEntrypointLoaded` callback with an EngineInitializer
@ -262,7 +317,12 @@ _flutter.loader = null;
_createScriptTag(url) {
const scriptTag = document.createElement("script");
scriptTag.type = "application/javascript";
scriptTag.src = url;
// Apply TrustedTypes validation, if available.
let trustedUrl = url;
if (this._ttPolicy != null) {
trustedUrl = this._ttPolicy.createScriptURL(url);
}
scriptTag.src = trustedUrl;
return scriptTag;
}
}
@ -285,9 +345,13 @@ _flutter.loader = null;
async loadEntrypoint(options) {
const { serviceWorker, ...entrypoint } = options || {};
// A Trusted Types policy that is going to be used by the loader.
const flutterTT = new FlutterTrustedTypesPolicy();
// The FlutterServiceWorkerLoader instance could be injected as a dependency
// (and dynamically imported from a module if not present).
const serviceWorkerLoader = new FlutterServiceWorkerLoader();
serviceWorkerLoader.setTrustedTypesPolicy(flutterTT.policy);
await serviceWorkerLoader.loadServiceWorker(serviceWorker).catch(e => {
// Regardless of what happens with the injection of the SW, the show must go on
console.warn("Exception while loading service worker:", e);
@ -296,6 +360,7 @@ _flutter.loader = null;
// The FlutterEntrypointLoader instance could be injected as a dependency
// (and dynamically imported from a module if not present).
const entrypointLoader = new FlutterEntrypointLoader();
entrypointLoader.setTrustedTypesPolicy(flutterTT.policy);
// Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
this.didCreateEngineInitializer =
entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader);

View file

@ -14,9 +14,11 @@ void main() {
mapperUrl: 'mapper.js',
);
// require js source is interpolated correctly.
expect(result, contains('requireEl.src = "require.js";'));
expect(result, contains('"requireJs": "require.js"'));
expect(result, contains('requireEl.src = getTTScriptUrl("requireJs");'));
// stack trace mapper source is interpolated correctly.
expect(result, contains('mapperEl.src = "mapper.js";'));
expect(result, contains('"mapper": "mapper.js"'));
expect(result, contains('mapperEl.src = getTTScriptUrl("mapper");'));
// data-main is set to correct bootstrap module.
expect(result, contains('requireEl.setAttribute("data-main", "main_module.bootstrap");'));
});