Unverified Commit 15ccf24d authored by David Iglesias's avatar David Iglesias Committed by GitHub

[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
parent 82288243
...@@ -37,6 +37,8 @@ enum ServiceWorkerTestType { ...@@ -37,6 +37,8 @@ enum ServiceWorkerTestType {
withFlutterJsEntrypointLoadedEvent, withFlutterJsEntrypointLoadedEvent,
// Same as withFlutterJsEntrypointLoadedEvent, but with TrustedTypes enabled. // Same as withFlutterJsEntrypointLoadedEvent, but with TrustedTypes enabled.
withFlutterJsTrustedTypesOn, withFlutterJsTrustedTypesOn,
// Same as withFlutterJsEntrypointLoadedEvent, but with nonce required.
withFlutterJsNonceOn,
// Uses custom serviceWorkerVersion. // Uses custom serviceWorkerVersion.
withFlutterJsCustomServiceWorkerVersion, withFlutterJsCustomServiceWorkerVersion,
// Entrypoint generated by `flutter create`. // Entrypoint generated by `flutter create`.
...@@ -53,6 +55,7 @@ Future<void> main() async { ...@@ -53,6 +55,7 @@ Future<void> main() async {
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort); await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent); await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn); 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.withoutFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs); await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort); await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
...@@ -120,6 +123,8 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) { ...@@ -120,6 +123,8 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) {
indexFile = 'index_with_flutterjs_entrypoint_loaded.html'; indexFile = 'index_with_flutterjs_entrypoint_loaded.html';
case ServiceWorkerTestType.withFlutterJsTrustedTypesOn: case ServiceWorkerTestType.withFlutterJsTrustedTypesOn:
indexFile = 'index_with_flutterjs_el_tt_on.html'; indexFile = 'index_with_flutterjs_el_tt_on.html';
case ServiceWorkerTestType.withFlutterJsNonceOn:
indexFile = 'index_with_flutterjs_el_nonce.html';
case ServiceWorkerTestType.withFlutterJsCustomServiceWorkerVersion: case ServiceWorkerTestType.withFlutterJsCustomServiceWorkerVersion:
indexFile = 'index_with_flutterjs_custom_sw_version.html'; indexFile = 'index_with_flutterjs_custom_sw_version.html';
case ServiceWorkerTestType.generatedEntrypoint: case ServiceWorkerTestType.generatedEntrypoint:
......
...@@ -1260,6 +1260,7 @@ Future<void> _runWebLongRunningTests() async { ...@@ -1260,6 +1260,7 @@ Future<void> _runWebLongRunningTests() async {
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort), () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent), () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent),
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn), () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn),
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsNonceOn),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs), () => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs), () => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort), () => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
......
<!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; ...@@ -244,10 +244,10 @@ _flutter.loader = null;
* Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`. * Returns undefined when an `onEntrypointLoaded` callback is supplied in `options`.
*/ */
async loadEntrypoint(options) { async loadEntrypoint(options) {
const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded } = const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded, nonce } =
options || {}; options || {};
return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded); return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce);
} }
/** /**
...@@ -286,12 +286,12 @@ _flutter.loader = null; ...@@ -286,12 +286,12 @@ _flutter.loader = null;
* is loaded, or undefined if `onEntrypointLoaded` * is loaded, or undefined if `onEntrypointLoaded`
* is a function. * is a function.
*/ */
_loadEntrypoint(entrypointUrl, onEntrypointLoaded) { _loadEntrypoint(entrypointUrl, onEntrypointLoaded, nonce) {
const useCallback = typeof onEntrypointLoaded === "function"; const useCallback = typeof onEntrypointLoaded === "function";
if (!this._scriptLoaded) { if (!this._scriptLoaded) {
this._scriptLoaded = true; this._scriptLoaded = true;
const scriptTag = this._createScriptTag(entrypointUrl); const scriptTag = this._createScriptTag(entrypointUrl, nonce);
if (useCallback) { if (useCallback) {
// Just inject the script tag, and return nothing; Flutter will call // Just inject the script tag, and return nothing; Flutter will call
// `didCreateEngineInitializer` when it's done. // `didCreateEngineInitializer` when it's done.
...@@ -319,9 +319,12 @@ _flutter.loader = null; ...@@ -319,9 +319,12 @@ _flutter.loader = null;
* @param {string} url * @param {string} url
* @returns {HTMLScriptElement} * @returns {HTMLScriptElement}
*/ */
_createScriptTag(url) { _createScriptTag(url, nonce) {
const scriptTag = document.createElement("script"); const scriptTag = document.createElement("script");
scriptTag.type = "application/javascript"; scriptTag.type = "application/javascript";
if (nonce) {
scriptTag.nonce = nonce;
}
// Apply TrustedTypes validation, if available. // Apply TrustedTypes validation, if available.
let trustedUrl = url; let trustedUrl = url;
if (this._ttPolicy != null) { if (this._ttPolicy != null) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment