mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[web] Add 'nonce' prop to flutter.js loadEntrypoint (#137204)
## Description This PR adds a `nonce` parameter to flutter.js' `loadEntrypoint` method. When set, loadEntrypoint will add a `nonce` attribute to the `main.dart.js` script tag, which allows Flutter to run in environments slightly more restricted by CSP; those that don't add `'self'` as a valid source for `script-src`. ---- ### CSP directive After this change, the CSP directive for a Flutter Web index.html can be: ``` script-src 'nonce-YOUR_NONCE_VALUE' 'wasm-unsafe-eval'; font-src https://fonts.gstatic.com; style-src 'nonce-YOUR_NONCE_VALUE'; ``` When CSP is set via a `meta` tag (like in the test accompanying this change), and to use a service worker, the CSP needs an additional directive: [`worker-src 'self';`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/worker-src) When CSP set via response headers, the CSP that applies to `flutter_service_worker.js` is determined by its response headers. See **Web Workers API > [Content security policy](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#content_security_policy)** in MDN.) ---- ### Initialization If the CSP is set to disallow `script-src 'self'`, a nonce needs to also be passed to `loadEntrypoint`: ```javascript _flutter.loader.loadEntrypoint({ nonce: 'SOME_NONCE', onEntrypointLoaded: (engineInitializer) async { const appRunner = await engineInitializer.initializeEngine({ nonce: 'SOME_NONCE', }); appRunner.runApp(); }, }); ``` (`nonce` shows twice for now, because the entrypoint loader script doesn't have direct access to the `initializeEngine` call.) ---- ## Tests * Added a smoke test to ensure an app configured as described above starts. ## Issues * Fixes https://github.com/flutter/flutter/issues/126977
This commit is contained in:
parent
8228824334
commit
15ccf24d79
@ -37,6 +37,8 @@ enum ServiceWorkerTestType {
|
||||
withFlutterJsEntrypointLoadedEvent,
|
||||
// Same as withFlutterJsEntrypointLoadedEvent, but with TrustedTypes enabled.
|
||||
withFlutterJsTrustedTypesOn,
|
||||
// Same as withFlutterJsEntrypointLoadedEvent, but with nonce required.
|
||||
withFlutterJsNonceOn,
|
||||
// Uses custom serviceWorkerVersion.
|
||||
withFlutterJsCustomServiceWorkerVersion,
|
||||
// Entrypoint generated by `flutter create`.
|
||||
@ -53,6 +55,7 @@ Future<void> main() async {
|
||||
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
|
||||
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent);
|
||||
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn);
|
||||
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsNonceOn);
|
||||
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
|
||||
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
|
||||
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
|
||||
@ -120,6 +123,8 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) {
|
||||
indexFile = 'index_with_flutterjs_entrypoint_loaded.html';
|
||||
case ServiceWorkerTestType.withFlutterJsTrustedTypesOn:
|
||||
indexFile = 'index_with_flutterjs_el_tt_on.html';
|
||||
case ServiceWorkerTestType.withFlutterJsNonceOn:
|
||||
indexFile = 'index_with_flutterjs_el_nonce.html';
|
||||
case ServiceWorkerTestType.withFlutterJsCustomServiceWorkerVersion:
|
||||
indexFile = 'index_with_flutterjs_custom_sw_version.html';
|
||||
case ServiceWorkerTestType.generatedEntrypoint:
|
||||
|
@ -1260,6 +1260,7 @@ Future<void> _runWebLongRunningTests() async {
|
||||
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
|
||||
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent),
|
||||
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn),
|
||||
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsNonceOn),
|
||||
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
|
||||
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
|
||||
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
|
||||
|
@ -0,0 +1,49 @@
|
||||
<!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. nonce required.</title>
|
||||
|
||||
<!-- Enable a CSP that requires a nonce for script and style-src. -->
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-SOME_NONCE' 'wasm-unsafe-eval'; font-src https://fonts.gstatic.com; style-src 'nonce-SOME_NONCE'; worker-src 'self';">
|
||||
|
||||
<!-- 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 nonce="SOME_NONCE">
|
||||
// The value below is injected by flutter build, do not touch.
|
||||
var serviceWorkerVersion = null;
|
||||
</script>
|
||||
<!-- This script adds the flutter initialization JS code -->
|
||||
<script nonce="SOME_NONCE" src="flutter.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<script nonce="SOME_NONCE">
|
||||
window.addEventListener('load', function(ev) {
|
||||
// Download main.dart.js
|
||||
_flutter.loader.loadEntrypoint({
|
||||
nonce: 'SOME_NONCE',
|
||||
onEntrypointLoaded: onEntrypointLoaded,
|
||||
serviceWorker: {
|
||||
serviceWorkerVersion: serviceWorkerVersion,
|
||||
}
|
||||
});
|
||||
|
||||
// Once the entrypoint is ready, do things!
|
||||
async function onEntrypointLoaded(engineInitializer) {
|
||||
const appRunner = await engineInitializer.initializeEngine({
|
||||
nonce: 'SOME_NONCE',
|
||||
});
|
||||
appRunner.runApp();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -244,10 +244,10 @@ _flutter.loader = null;
|
||||
* Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`.
|
||||
*/
|
||||
async loadEntrypoint(options) {
|
||||
const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded } =
|
||||
const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded, nonce } =
|
||||
options || {};
|
||||
|
||||
return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded);
|
||||
return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -286,12 +286,12 @@ _flutter.loader = null;
|
||||
* is loaded, or undefined if `onEntrypointLoaded`
|
||||
* is a function.
|
||||
*/
|
||||
_loadEntrypoint(entrypointUrl, onEntrypointLoaded) {
|
||||
_loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce) {
|
||||
const useCallback = typeof onEntrypointLoaded === "function";
|
||||
|
||||
if (!this._scriptLoaded) {
|
||||
this._scriptLoaded = true;
|
||||
const scriptTag = this._createScriptTag(entrypointUrl);
|
||||
const scriptTag = this._createScriptTag(entrypointUrl, nonce);
|
||||
if (useCallback) {
|
||||
// Just inject the script tag, and return nothing; Flutter will call
|
||||
// `didCreateEngineInitializer` when it's done.
|
||||
@ -319,9 +319,12 @@ _flutter.loader = null;
|
||||
* @param {string} url
|
||||
* @returns {HTMLScriptElement}
|
||||
*/
|
||||
_createScriptTag(url) {
|
||||
_createScriptTag(url, nonce) {
|
||||
const scriptTag = document.createElement("script");
|
||||
scriptTag.type = "application/javascript";
|
||||
if (nonce) {
|
||||
scriptTag.nonce = nonce;
|
||||
}
|
||||
// Apply TrustedTypes validation, if available.
|
||||
let trustedUrl = url;
|
||||
if (this._ttPolicy != null) {
|
||||
|
Loading…
Reference in New Issue
Block a user