Create WPT for partitioned service workers
Add a test that verifies that services workers are partitioned and
cannot interact with each other when third-party partitioning is
enabled.
This CL verifies the enabled feature case by running the test in a
virtual test suite.
The test expectations are also updated to expect the test to fail in
the standard suite and pass in the virtual suite.
I manually verified this test by removing the window and iframe
cleanup/removals, running it in content_shell, and verifying that the
windows displayed the appropriate text demonstrating that the SW did
indeed get used.
Bug: 1246549
Change-Id: I22450e3abf496e4e011cc60ef7cfdf04acfdbcda
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3183843
Reviewed-by: Mason Freed <masonf@chromium.org>
Reviewed-by: Ian Kilpatrick <ikilpatrick@chromium.org>
Reviewed-by: Mike Taylor <miketaylr@chromium.org>
Reviewed-by: Ben Kelly <wanderview@chromium.org>
Commit-Queue: Steven Bingler <bingler@chromium.org>
Cr-Commit-Position: refs/heads/main@{#932203}
diff --git a/service-workers/service-worker/partitioned-service-worker.tentative.https.html b/service-workers/service-worker/partitioned-service-worker.tentative.https.html
new file mode 100644
index 0000000..c006e09
--- /dev/null
+++ b/service-workers/service-worker/partitioned-service-worker.tentative.https.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<meta charset="utf-8"/>
+<title>Service Worker: Partitioned Service Workers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+
+<body>
+ The 3p iframe's postMessage:
+ <p id="iframe_response">No message received</p>
+
+<script>
+
+// The resolve function for the current pending event listener's promise.
+// It is nulled once the promise is resolved.
+var message_event_promise_resolve = null;
+
+function messageEventHandler(evt) {
+ if (message_event_promise_resolve) {
+ local_resolve = message_event_promise_resolve;
+ message_event_promise_resolve = null;
+ local_resolve(evt.data);
+ }
+}
+
+function makeMessagePromise() {
+ if (message_event_promise_resolve != null) {
+ // Do not create a new promise until the previous is settled.
+ return;
+ }
+
+ return new Promise(resolve => {
+ message_event_promise_resolve = resolve;
+ });
+}
+
+// Loads a url for the frame type and then returns a promise for
+// the data that was postMessage'd from the loaded frame.
+function loadAndReturnSwData(t, url, frame_type) {
+ if (frame_type !== 'iframe' && frame_type !== 'window') {
+ return;
+ }
+
+ const message_promise = makeMessagePromise();
+
+ // Create the iframe or window and then return the promise for data.
+ if ( frame_type === 'iframe' ) {
+ const frame = with_iframe(url, false);
+ t.add_cleanup(async () => {
+ const f = await frame;
+ f.remove();
+ });
+ }
+ else {
+ // 'window' case.
+ const w = window.open(url);
+ t.add_cleanup(() => w.close());
+ }
+
+ return message_promise;
+}
+
+promise_test(async t => {
+ const script = './resources/partitioned-storage-sw.js'
+ const scope = './resources/partitioned-'
+
+ // Add service worker to this 1P context.
+ const reg = await service_worker_unregister_and_register(t, script, scope);
+ t.add_cleanup(() => reg.unregister());
+ await wait_for_state(t, reg.installing, 'activated');
+
+ // Register the message listener.
+ self.addEventListener('message', messageEventHandler);
+ t.add_cleanup(() =>{
+ self.removeEventListener('message', messageEventHandler, false);
+ });
+
+ // Open an iframe that will create a promise within the SW.
+ // The query param is there to track which request the service worker is
+ // handling.
+ const wait_frame_url = new URL(
+ './resources/partitioned-waitUntilResolved.fakehtml?From1pFrame',
+ self.location);
+
+ const wait_frame_1p_data = await loadAndReturnSwData(t, wait_frame_url,
+ 'iframe');
+ assert_equals(wait_frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+
+ // Now create a 3p iframe that will try to resolve the SW in a 3p context.
+ const third_party_url = new URL(
+ './resources/partitioned-service-worker-third-party-window.html',
+ get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname);
+
+ // Create the 3p window (which will in turn create the iframe with the SW)
+ // and await on its data.
+ const frame_3p_data = await loadAndReturnSwData(t, third_party_url, 'window');
+ assert_equals(frame_3p_data.source, 'From3pFrame',
+ 'The data for the 3p frame came from the wrong source');
+
+ // Print some debug info to the main frame.
+ document.getElementById("iframe_response").innerHTML =
+ "3p iframe's has_pending: " + frame_3p_data.has_pending + " source: " +
+ frame_3p_data.source + ". ";
+
+ // Now do the same for the 1p iframe.
+ const resolve_frame_url = new URL(
+ './resources/partitioned-resolve.fakehtml?From1pFrame', self.location);
+
+ const frame_1p_data = await loadAndReturnSwData(t, resolve_frame_url,
+ 'iframe');
+ assert_equals(frame_1p_data.source, 'From1pFrame',
+ 'The data for the 1p frame came from the wrong source');
+ // Both the 1p frames should have been serviced by the same service worker ID.
+ // If this isn't the case then that means the SW could have been deactivated
+ // which invalidates the test.
+ assert_equals(frame_1p_data.ID, wait_frame_1p_data.ID,
+ 'The 1p frames were serviced by different service workers.');
+
+ document.getElementById("iframe_response").innerHTML +=
+ "1p iframe's has_pending: " + frame_1p_data.has_pending + " source: " +
+ frame_1p_data.source;
+
+ // If partitioning is working correctly then only the 1p iframe should see
+ // (and resolve) its SW's promise. Additionally the two frames should see
+ // different IDs.
+ assert_true(frame_1p_data.has_pending,
+ 'The 1p iframe saw a pending promise in the service worker.');
+ assert_false(frame_3p_data.has_pending,
+ 'The 3p iframe saw a pending promise in the service worker.');
+ assert_not_equals(frame_1p_data.ID, frame_3p_data.ID,
+ 'The frames were serviced by the same service worker thread.');
+}, 'Services workers under different top-level sites are partitioned.');
+
+</script>
+</body>
\ No newline at end of file
diff --git a/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html b/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
new file mode 100644
index 0000000..56c04b6
--- /dev/null
+++ b/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P iframe for partitioned service workers</title>
+<script src="/test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+
+<body>
+This iframe will register a service worker when it loads and then add its own
+iframe that will attempt to navigate to a url that service worker will intercept
+and use to resolve the service worker's internal Promise.
+<script>
+// We should keep track if we installed a worker or not. If we did then we need
+// to uninstall it. Otherwise we let the top level test uninstall it.
+var installed_a_worker = true;
+var reg;
+
+async function onLoad() {
+ const script = './partitioned-storage-sw.js'
+ const scope = './partitioned-'
+
+ reg = await navigator.serviceWorker.register(script, { scope: scope });
+ await new Promise(resolve => {
+ // Check if a worker is already activated.
+ var worker = reg.active;
+ // If so, just resolve.
+ if ( worker ) {
+ installed_a_worker = false;
+ resolve();
+ return;
+ }
+
+ //Otherwise check if one is waiting.
+ worker = reg.waiting;
+ // If not waiting, grab the installing worker.
+ if ( !worker ) {
+ worker = reg.installing;
+ }
+
+ // Resolve once it's activated.
+ worker.addEventListener('statechange', evt => {
+ if (worker.state === 'activated') {
+ resolve();
+ }
+ });
+ });
+
+ // When the SW's iframe finishes it'll post a message. This forwards it up to
+ // the window.
+ self.addEventListener('message', evt => {
+ window.parent.postMessage(evt.data, '*');
+ });
+
+ // Now try to resolve the SW's promise. If we're partitioned then there
+ // shouldn't be a promise to resolve.
+ const resolve_frame_url = new URL('./partitioned-resolve.fakehtml?From3pFrame', self.location);
+ const frame_resolve = await new Promise(resolve => {
+ var frame = document.createElement('iframe');
+ frame.src = resolve_frame_url;
+ frame.onload = function() { resolve(frame); };
+ document.body.appendChild(frame);
+ });
+}
+
+self.addEventListener('unload', async () => {
+ // If we didn't install a worker then that means the top level test did, and
+ // that test is therefore responsible for cleaning it up.
+ if ( !installed_a_worker ) {
+ return;
+ }
+
+ await reg.unregister();
+});
+
+self.addEventListener('load', onLoad);
+</script>
+</body>
\ No newline at end of file
diff --git a/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html b/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
new file mode 100644
index 0000000..21a8380
--- /dev/null
+++ b/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>Service Worker: 3P window for partitioned service workers</title>
+<script src="test-helpers.sub.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+
+<body>
+This page should be opened as a third-party window. It then loads an iframe that
+registers a service worker that, in turn, attempts to resolve its internal
+Promise. Once that happens this window will postMessage back to the test to let
+it know that the resolution occured.
+
+If 3p partitioning is active then this Promise resolution won't
+resolve the promise in the test's (that originally opened this page) 1p
+service worker.
+
+<script>
+
+async function onLoad() {
+ const message_promise = new Promise(resolve => {
+ self.addEventListener('message', evt => {
+ resolve(evt.data);
+ });
+ });
+
+ const iframe_url = new URL(
+ './partitioned-service-worker-third-party-iframe.html',
+ get_host_info().HTTPS_ORIGIN + self.location.pathname);
+
+ var frame = document.createElement('iframe');
+ frame.src = iframe_url;
+ frame.style.position = 'absolute';
+ document.body.appendChild(frame);
+
+
+ await message_promise.then(data => {
+ // We're done, forward the message and clean up.
+ window.opener.postMessage(data, '*');
+
+ frame.remove();
+ });
+}
+
+self.addEventListener('load', onLoad);
+
+</script>
+</body>
\ No newline at end of file
diff --git a/service-workers/service-worker/resources/partitioned-storage-sw.js b/service-workers/service-worker/resources/partitioned-storage-sw.js
new file mode 100644
index 0000000..6abf10a
--- /dev/null
+++ b/service-workers/service-worker/resources/partitioned-storage-sw.js
@@ -0,0 +1,62 @@
+// Holds the promise that the "resolve.fakehtml" call attempts to resolve.
+// This is "the SW's promise" that other parts of the test refer to.
+var promise;
+// Stores the resolve funcution for the current promise.
+var pending_resolve_func = null;
+// Unique ID to determine which service worker is being used.
+const ID = Math.random();
+
+function callAndResetResolve() {
+ var local_resolve = pending_resolve_func;
+ pending_resolve_func = null;
+ local_resolve();
+}
+
+self.addEventListener('fetch', function(event) {
+ fetchEventHandler(event);
+})
+
+async function fetchEventHandler(event){
+ var request_url = new URL(event.request.url);
+ var url_search = request_url.search.substr(1);
+ request_url.search = "";
+ if ( request_url.href.endsWith('waitUntilResolved.fakehtml') ) {
+
+ if (pending_resolve_func != null) {
+ // Respond with an error if there is already a pending promise
+ event.respondWith(Response.error());
+ return;
+ }
+
+ // Create the new promise.
+ promise = new Promise(function(resolve) {
+ pending_resolve_func = resolve;
+ });
+ event.waitUntil(promise);
+
+ event.respondWith(new Response(`
+ <html>
+ Promise created by ${url_search}
+ <script>self.parent.postMessage({ ID:${ID}, source: "${url_search}"
+ });</script>
+ </html>
+ `, {headers: {'Content-Type': 'text/html'}}
+ ));
+
+ }
+ else if ( request_url.href.endsWith('resolve.fakehtml') ) {
+ var has_pending = !!pending_resolve_func;
+
+ event.respondWith(new Response(`
+ <html>
+ Promise settled for ${url_search}
+ <script>self.parent.postMessage({ ID:${ID}, has_pending: ${has_pending},
+ source: "${url_search}" });</script>
+ </html>
+ `, {headers: {'Content-Type': 'text/html'}}));
+
+ if (has_pending) {
+ callAndResetResolve();
+ }
+ }
+}
\ No newline at end of file