Skip to content

Commit 8834692

Browse files
authored
[web] Use TrustedTypes in flutter.js and other tools (#112969)
1 parent 782baec commit 8834692

File tree

6 files changed

+160
-7
lines changed

6 files changed

+160
-7
lines changed

dev/bots/service_worker_test.dart

+12-1
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@ final String _targetWithBlockedServiceWorkers = path.join('lib', 'service_worker
2525
final String _targetPath = path.join(_testAppDirectory, _target);
2626

2727
enum ServiceWorkerTestType {
28+
// Mocks how FF disables service workers.
2829
blockedServiceWorkers,
30+
// Drops the main.dart.js directly on the page.
2931
withoutFlutterJs,
32+
// Uses the standard, promise-based, flutterJS initialization.
3033
withFlutterJs,
34+
// Uses the shorthand engineInitializer.autoStart();
3135
withFlutterJsShort,
36+
// Uses onEntrypointLoaded callback instead of returned promise.
3237
withFlutterJsEntrypointLoadedEvent,
33-
38+
// Same as withFlutterJsEntrypointLoadedEvent, but with TrustedTypes enabled.
39+
withFlutterJsTrustedTypesOn,
3440
// Entrypoint generated by `flutter create`.
3541
generatedEntrypoint,
3642
}
@@ -44,10 +50,12 @@ Future<void> main() async {
4450
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
4551
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
4652
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent);
53+
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn);
4754
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
4855
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
4956
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
5057
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent);
58+
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn);
5159
await runWebServiceWorkerTestWithGeneratedEntrypoint(headless: false);
5260
await runWebServiceWorkerTestWithBlockedServiceWorkers(headless: false);
5361

@@ -112,6 +120,9 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) {
112120
case ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent:
113121
indexFile = 'index_with_flutterjs_entrypoint_loaded.html';
114122
break;
123+
case ServiceWorkerTestType.withFlutterJsTrustedTypesOn:
124+
indexFile = 'index_with_flutterjs_el_tt_on.html';
125+
break;
115126
case ServiceWorkerTestType.generatedEntrypoint:
116127
indexFile = 'generated_entrypoint.html';
117128
break;

dev/bots/test.dart

+2
Original file line numberDiff line numberDiff line change
@@ -1195,10 +1195,12 @@ Future<void> _runWebLongRunningTests() async {
11951195
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
11961196
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
11971197
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent),
1198+
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn),
11981199
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
11991200
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
12001201
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
12011202
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent),
1203+
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn),
12021204
() => runWebServiceWorkerTestWithGeneratedEntrypoint(headless: true),
12031205
() => runWebServiceWorkerTestWithBlockedServiceWorkers(headless: true),
12041206
() => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE HTML>
2+
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
3+
Use of this source code is governed by a BSD-style license that can be
4+
found in the LICENSE file. -->
5+
<html>
6+
<head>
7+
<meta charset="UTF-8">
8+
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
9+
10+
<title>Integration test. App load with flutter.js and onEntrypointLoaded API. Trusted Types enabled.</title>
11+
12+
<!-- Enable TrustedTypes for 'script'-->
13+
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'">
14+
15+
<!-- iOS meta tags & icons -->
16+
<meta name="apple-mobile-web-app-capable" content="yes">
17+
<meta name="apple-mobile-web-app-status-bar-style" content="black">
18+
<meta name="apple-mobile-web-app-title" content="Web Test">
19+
<link rel="manifest" href="manifest.json">
20+
<script>
21+
// The value below is injected by flutter build, do not touch.
22+
var serviceWorkerVersion = null;
23+
</script>
24+
<!-- This script adds the flutter initialization JS code -->
25+
<script src="flutter.js" defer></script>
26+
</head>
27+
<body>
28+
<script>
29+
window.addEventListener('load', function(ev) {
30+
// Download main.dart.js
31+
_flutter.loader.loadEntrypoint({
32+
onEntrypointLoaded: onEntrypointLoaded,
33+
serviceWorker: {
34+
serviceWorkerVersion: serviceWorkerVersion,
35+
}
36+
});
37+
38+
// Once the entrypoint is ready, do things!
39+
async function onEntrypointLoaded(engineInitializer) {
40+
const appRunner = await engineInitializer.initializeEngine();
41+
appRunner.runApp();
42+
}
43+
});
44+
</script>
45+
</body>
46+
</html>

packages/flutter_tools/lib/src/web/bootstrap.dart

+29-2
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,45 @@ document.addEventListener('dart-app-ready', function (e) {
9494
styleSheet.parentNode.removeChild(styleSheet);
9595
});
9696
97+
// A map containing the URLs for the bootstrap scripts in debug.
98+
let _scriptUrls = {
99+
"mapper": "$mapperUrl",
100+
"requireJs": "$requireUrl"
101+
};
102+
103+
// Create a TrustedTypes policy so we can attach Scripts...
104+
let _ttPolicy;
105+
if (window.trustedTypes) {
106+
_ttPolicy = trustedTypes.createPolicy("flutter-tools-bootstrap", {
107+
createScriptURL: (url) => {
108+
let scriptUrl = _scriptUrls[url];
109+
if (!scriptUrl) {
110+
console.error("Unknown Flutter Web bootstrap resource!", url);
111+
}
112+
return scriptUrl;
113+
}
114+
});
115+
}
116+
117+
// Creates a TrustedScriptURL for a given `scriptName`.
118+
// See `_scriptUrls` and `_ttPolicy` above.
119+
function getTTScriptUrl(scriptName) {
120+
let defaultUrl = _scriptUrls[scriptName];
121+
return _ttPolicy ? _ttPolicy.createScriptURL(scriptName) : defaultUrl;
122+
}
123+
97124
// Attach source mapping.
98125
var mapperEl = document.createElement("script");
99126
mapperEl.defer = true;
100127
mapperEl.async = false;
101-
mapperEl.src = "$mapperUrl";
128+
mapperEl.src = getTTScriptUrl("mapper");
102129
document.head.appendChild(mapperEl);
103130
104131
// Attach require JS.
105132
var requireEl = document.createElement("script");
106133
requireEl.defer = true;
107134
requireEl.async = false;
108-
requireEl.src = "$requireUrl";
135+
requireEl.src = getTTScriptUrl("requireJs");
109136
// This attribute tells require JS what to load as main (defined below).
110137
requireEl.setAttribute("data-main", "main_module.bootstrap");
111138
document.head.appendChild(requireEl);

packages/flutter_tools/lib/src/web/file_generators/flutter_js.dart

+67-2
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,53 @@ _flutter.loader = null;
5656
});
5757
}
5858
59+
/**
60+
* Handles the creation of a TrustedTypes `policy` that validates URLs based
61+
* on an (optional) incoming array of RegExes.
62+
*/
63+
class FlutterTrustedTypesPolicy {
64+
/**
65+
* Constructs the policy.
66+
* @param {[RegExp]} validPatterns the patterns to test URLs
67+
* @param {String} policyName the policy name (optional)
68+
*/
69+
constructor(validPatterns, policyName = "flutter-js") {
70+
const patterns = validPatterns || [
71+
/\.dart\.js$/,
72+
/^flutter_service_worker.js$/
73+
];
74+
if (window.trustedTypes) {
75+
this.policy = trustedTypes.createPolicy(policyName, {
76+
createScriptURL: function(url) {
77+
const parsed = new URL(url, window.location);
78+
const file = parsed.pathname.split("/").pop();
79+
const matches = patterns.some((pattern) => pattern.test(file));
80+
if (matches) {
81+
return parsed.toString();
82+
}
83+
console.error(
84+
"URL rejected by TrustedTypes policy",
85+
policyName, ":", url, "(download prevented)");
86+
}
87+
});
88+
}
89+
}
90+
}
91+
5992
/**
6093
* Handles loading/reloading Flutter's service worker, if configured.
6194
*
6295
* @see: https://developers.google.com/web/fundamentals/primers/service-workers
6396
*/
6497
class FlutterServiceWorkerLoader {
98+
/**
99+
* Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
100+
* @param {TrustedTypesPolicy | undefined} policy
101+
*/
102+
setTrustedTypesPolicy(policy) {
103+
this._ttPolicy = policy;
104+
}
105+
65106
/**
66107
* Returns a Promise that resolves when the latest Flutter service worker,
67108
* configured by `settings` has been loaded and activated.
@@ -84,8 +125,14 @@ _flutter.loader = null;
84125
timeoutMillis = 4000,
85126
} = settings;
86127
128+
// Apply the TrustedTypes policy, if present.
129+
let url = serviceWorkerUrl;
130+
if (this._ttPolicy != null) {
131+
url = this._ttPolicy.createScriptURL(url);
132+
}
133+
87134
const serviceWorkerActivation = navigator.serviceWorker
88-
.register(serviceWorkerUrl)
135+
.register(url)
89136
.then(this._getNewServiceWorker)
90137
.then(this._waitForServiceWorkerActivation);
91138
@@ -173,6 +220,14 @@ _flutter.loader = null;
173220
this._scriptLoaded = false;
174221
}
175222
223+
/**
224+
* Injects a TrustedTypesPolicy (or undefined if the feature is not supported).
225+
* @param {TrustedTypesPolicy | undefined} policy
226+
*/
227+
setTrustedTypesPolicy(policy) {
228+
this._ttPolicy = policy;
229+
}
230+
176231
/**
177232
* Loads flutter main entrypoint, specified by `entrypointUrl`, and calls a
178233
* user-specified `onEntrypointLoaded` callback with an EngineInitializer
@@ -262,7 +317,12 @@ _flutter.loader = null;
262317
_createScriptTag(url) {
263318
const scriptTag = document.createElement("script");
264319
scriptTag.type = "application/javascript";
265-
scriptTag.src = url;
320+
// Apply TrustedTypes validation, if available.
321+
let trustedUrl = url;
322+
if (this._ttPolicy != null) {
323+
trustedUrl = this._ttPolicy.createScriptURL(url);
324+
}
325+
scriptTag.src = trustedUrl;
266326
return scriptTag;
267327
}
268328
}
@@ -285,9 +345,13 @@ _flutter.loader = null;
285345
async loadEntrypoint(options) {
286346
const { serviceWorker, ...entrypoint } = options || {};
287347
348+
// A Trusted Types policy that is going to be used by the loader.
349+
const flutterTT = new FlutterTrustedTypesPolicy();
350+
288351
// The FlutterServiceWorkerLoader instance could be injected as a dependency
289352
// (and dynamically imported from a module if not present).
290353
const serviceWorkerLoader = new FlutterServiceWorkerLoader();
354+
serviceWorkerLoader.setTrustedTypesPolicy(flutterTT.policy);
291355
await serviceWorkerLoader.loadServiceWorker(serviceWorker).catch(e => {
292356
// Regardless of what happens with the injection of the SW, the show must go on
293357
console.warn("Exception while loading service worker:", e);
@@ -296,6 +360,7 @@ _flutter.loader = null;
296360
// The FlutterEntrypointLoader instance could be injected as a dependency
297361
// (and dynamically imported from a module if not present).
298362
const entrypointLoader = new FlutterEntrypointLoader();
363+
entrypointLoader.setTrustedTypesPolicy(flutterTT.policy);
299364
// Install the `didCreateEngineInitializer` listener where Flutter web expects it to be.
300365
this.didCreateEngineInitializer =
301366
entrypointLoader.didCreateEngineInitializer.bind(entrypointLoader);

packages/flutter_tools/test/general.shard/web/bootstrap_test.dart

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ void main() {
1414
mapperUrl: 'mapper.js',
1515
);
1616
// require js source is interpolated correctly.
17-
expect(result, contains('requireEl.src = "require.js";'));
17+
expect(result, contains('"requireJs": "require.js"'));
18+
expect(result, contains('requireEl.src = getTTScriptUrl("requireJs");'));
1819
// stack trace mapper source is interpolated correctly.
19-
expect(result, contains('mapperEl.src = "mapper.js";'));
20+
expect(result, contains('"mapper": "mapper.js"'));
21+
expect(result, contains('mapperEl.src = getTTScriptUrl("mapper");'));
2022
// data-main is set to correct bootstrap module.
2123
expect(result, contains('requireEl.setAttribute("data-main", "main_module.bootstrap");'));
2224
});

0 commit comments

Comments
 (0)