summaryrefslogtreecommitdiffstats
path: root/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js')
-rw-r--r--dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js415
1 files changed, 415 insertions, 0 deletions
diff --git a/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js b/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js
new file mode 100644
index 0000000000..8dbfc5857f
--- /dev/null
+++ b/dom/workers/test/xpcshell/test_ext_redirects_sw_scripts.js
@@ -0,0 +1,415 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { ExtensionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"
+);
+
+const { createHttpServer } = AddonTestUtils;
+
+// Force ServiceWorkerRegistrar to init by calling do_get_profile.
+// (This has to be called before AddonTestUtils.init, because it does
+// also call do_get_profile internally but it doesn't notify
+// profile-after-change).
+do_get_profile(true);
+
+AddonTestUtils.init(this);
+ExtensionTestUtils.init(this);
+
+const server = createHttpServer({ hosts: ["localhost"] });
+
+server.registerPathHandler("/page.html", (request, response) => {
+ info(`/page.html is being requested: ${JSON.stringify(request)}`);
+ response.write(`<!DOCTYPE html>`);
+});
+
+server.registerPathHandler("/sw.js", (request, response) => {
+ info(`/sw.js is being requested: ${JSON.stringify(request)}`);
+ response.setHeader("Content-Type", "application/javascript");
+ response.write(`
+ dump('Executing http://localhost/sw.js\\n');
+ importScripts('sw-imported.js');
+ dump('Executed importScripts from http://localhost/sw.js\\n');
+ `);
+});
+
+server.registerPathHandler("/sw-imported.js", (request, response) => {
+ info(`/sw-imported.js is being requested: ${JSON.stringify(request)}`);
+ response.setHeader("Content-Type", "application/javascript");
+ response.write(`
+ dump('importScript loaded from http://localhost/sw-imported.js\\n');
+ self.onmessage = evt => evt.ports[0].postMessage('original-imported-script');
+ `);
+});
+
+Services.prefs.setBoolPref("dom.serviceWorkers.testing.enabled", true);
+// Make sure this test file doesn't run with the legacy behavior by
+// setting explicitly the expected default value.
+Services.prefs.setBoolPref(
+ "extensions.filterResponseServiceWorkerScript.disabled",
+ false
+);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("dom.serviceWorkers.testing.enabled");
+ Services.prefs.clearUserPref(
+ "extensions.filterResponseServiceWorkerScript.disabled"
+ );
+});
+
+// Helper function used to be sure to clear any data that a previous test case
+// may have left (e.g. service worker registration, cached service worker
+// scripts).
+//
+// NOTE: Given that xpcshell test are running isolated from each other (unlike
+// mochitests), we can just clear every storage type supported by clear data
+// (instead of cherry picking what we want to clear based on the test cases
+// part of this test file).
+async function ensureDataCleanup() {
+ info("Clear any service worker or data previous test cases may have left");
+ await new Promise(resolve =>
+ Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve)
+ );
+}
+
+// Note that the update algorithm (https://w3c.github.io/ServiceWorker/#update-algorithm)
+// builds an "updatedResourceMap" as part of its check process. This means that only a
+// single fetch will be performed for "sw-imported.js" as part of the update check and its
+// resulting install invocation. The installation's call to importScripts when evaluated
+// will load the script directly out of the Cache API.
+function testSWUpdate(contentPage) {
+ return contentPage.legacySpawn([], async () => {
+ const oldReg = await this.content.navigator.serviceWorker.ready;
+ const reg = await oldReg.update();
+ const sw = reg.installing || reg.waiting || reg.active;
+ return new Promise(resolve => {
+ const { port1, port2 } = new MessageChannel();
+ port1.onmessage = evt => resolve(evt.data);
+ sw.postMessage("worker-message", [port2]);
+ });
+ });
+}
+
+add_task(async function test_extension_invalid_sw_scripts_redirect_ignored() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["<all_urls>", "webRequest", "webRequestBlocking"],
+ },
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(
+ req => {
+ if (req.url == "http://localhost/sw.js") {
+ const filter = browser.webRequest.filterResponseData(req.requestId);
+ filter.ondata = event => filter.write(event.data);
+ filter.onstop = event => filter.disconnect();
+ filter.onerror = () => {
+ browser.test.sendMessage(
+ "filter-response-error:mainscript",
+ filter.error
+ );
+ };
+ return {
+ redirectUrl: browser.runtime.getURL("sw-unexpected-redirect.js"),
+ };
+ }
+
+ if (req.url == "http://localhost/sw-imported.js") {
+ const filter = browser.webRequest.filterResponseData(req.requestId);
+ filter.ondata = event => filter.write(event.data);
+ filter.onstop = event => filter.disconnect();
+ filter.onerror = () => {
+ browser.test.sendMessage(
+ "filter-response-error:importscript",
+ filter.error
+ );
+ };
+ return { redirectUrl: "about:blank" };
+ }
+
+ return {};
+ },
+ { urls: ["http://localhost/sw.js", "http://localhost/sw-imported.js"] },
+ ["blocking"]
+ );
+ },
+ files: {
+ "sw-unexpected-redirect.js": `
+ dump('importScript redirected to moz-extension://UUID/sw-unexpected-redirect.js\\n');
+ self.onmessage = evt => evt.ports[0].postMessage('sw-unexpected-redirect');
+ `,
+ },
+ });
+
+ // Start the test extension to redirect importScripts requests.
+ await extension.startup();
+
+ function awaitConsoleMessage(regexp) {
+ return new Promise(resolve => {
+ Services.console.registerListener(function listener(message) {
+ if (regexp.test(message.message)) {
+ Services.console.unregisterListener(listener);
+ resolve(message);
+ }
+ });
+ });
+ }
+
+ const awaitIgnoredMainScriptRedirect = awaitConsoleMessage(
+ /Invalid redirectUrl .* on service worker main script/
+ );
+ const awaitIgnoredImportScriptRedirect = awaitConsoleMessage(
+ /Invalid redirectUrl .* on service worker imported script/
+ );
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://localhost/page.html"
+ );
+
+ // Register the worker while the test extension isn't loaded and cannot
+ // intercept and redirect the importedScripts requests.
+ info("Register service worker from a content webpage");
+ let workerMessage = await contentPage.legacySpawn([], async () => {
+ const reg = await this.content.navigator.serviceWorker.register("/sw.js");
+ return new Promise(resolve => {
+ const { port1, port2 } = new MessageChannel();
+ port1.onmessage = evt => resolve(evt.data);
+ const sw = reg.active || reg.waiting || reg.installing;
+ sw.postMessage("worker-message", [port2]);
+ });
+ });
+
+ equal(
+ workerMessage,
+ "original-imported-script",
+ "Got expected worker reply (importScripts not intercepted)"
+ );
+
+ info("Wait for the expected error message on main script redirect");
+ const errorMsg = await awaitIgnoredMainScriptRedirect;
+ ok(errorMsg?.message, `Got error message: ${errorMsg?.message}`);
+ ok(
+ errorMsg?.message?.includes(extension.id),
+ "error message should include the addon id"
+ );
+ ok(
+ errorMsg?.message?.includes("http://localhost/sw.js"),
+ "error message should include the sw main script url"
+ );
+
+ info("Wait for the expected error message on import script redirect");
+ const errorMsg2 = await awaitIgnoredImportScriptRedirect;
+ ok(errorMsg2?.message, `Got error message: ${errorMsg2?.message}`);
+ ok(
+ errorMsg2?.message?.includes(extension.id),
+ "error message should include the addon id"
+ );
+ ok(
+ errorMsg2?.message?.includes("http://localhost/sw-imported.js"),
+ "error message should include the sw main script url"
+ );
+
+ info("Wait filterResponse error on main script");
+ equal(
+ await extension.awaitMessage("filter-response-error:mainscript"),
+ "Invalid request ID",
+ "Got expected error on main script"
+ );
+ info("Wait filterResponse error on import script");
+ equal(
+ await extension.awaitMessage("filter-response-error:importscript"),
+ "Invalid request ID",
+ "Got expected error on import script"
+ );
+
+ await extension.unload();
+ await contentPage.close();
+});
+
+add_task(async function test_filter_sw_script() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "<all_urls>",
+ "webRequest",
+ "webRequestBlocking",
+ "webRequestFilterResponse.serviceWorkerScript",
+ ],
+ },
+ background() {
+ browser.webRequest.onBeforeRequest.addListener(
+ req => {
+ if (req.url == "http://localhost/sw.js") {
+ const filter = browser.webRequest.filterResponseData(req.requestId);
+ let decoder = new TextDecoder("utf-8");
+ let encoder = new TextEncoder();
+ filter.ondata = event => {
+ let str = decoder.decode(event.data, { stream: true });
+ browser.test.log(`Got filter ondata event: ${str}\n`);
+ str = `
+ dump('Executing filterResponse script for http://localhost/sw.js\\n');
+ self.onmessage = evt => evt.ports[0].postMessage('filter-response-script');
+ dump('Executed firlterResponse script for http://localhost/sw.js\\n');
+ `;
+ filter.write(encoder.encode(str));
+ filter.disconnect();
+ };
+ }
+
+ return {};
+ },
+ { urls: ["http://localhost/sw.js", "http://localhost/sw-imported.js"] },
+ ["blocking"]
+ );
+ },
+ });
+
+ // Start the test extension to redirect importScripts requests.
+ await extension.startup();
+
+ await ensureDataCleanup();
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://localhost/page.html"
+ );
+
+ let workerMessage = await contentPage.legacySpawn([], async () => {
+ const reg = await this.content.navigator.serviceWorker.register("/sw.js");
+ return new Promise(resolve => {
+ const { port1, port2 } = new MessageChannel();
+ port1.onmessage = evt => resolve(evt.data);
+ const sw = reg.active || reg.waiting || reg.installing;
+ sw.postMessage("worker-message", [port2]);
+ });
+ });
+
+ equal(
+ workerMessage,
+ "filter-response-script",
+ "Got expected worker reply (filterResponse script)"
+ );
+
+ await extension.unload();
+ workerMessage = await testSWUpdate(contentPage);
+ equal(
+ workerMessage,
+ "original-imported-script",
+ "Got expected worker reply (original script)"
+ );
+
+ await contentPage.close();
+});
+
+add_task(async function test_extension_redirect_sw_imported_script() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["<all_urls>", "webRequest", "webRequestBlocking"],
+ web_accessible_resources: ["sw-imported-1.js", "sw-imported-2.js"],
+ },
+ background() {
+ let i = 1;
+ browser.webRequest.onBeforeRequest.addListener(
+ req => {
+ browser.test.log(
+ "Extension is redirecting http://localhost/sw-imported.js"
+ );
+ browser.test.sendMessage("request-redirected");
+ return {
+ redirectUrl: browser.runtime.getURL(`sw-imported-${i++}.js`),
+ };
+ },
+ { urls: ["http://localhost/sw-imported.js"] },
+ ["blocking"]
+ );
+ },
+ files: {
+ "sw-imported-1.js": `
+ dump('importScript redirected to moz-extension://UUID/sw-imported1.js \\n');
+ self.onmessage = evt => evt.ports[0].postMessage('redirected-imported-script-1');
+ `,
+ "sw-imported-2.js": `
+ dump('importScript redirected to moz-extension://UUID/sw-imported2.js \\n');
+ self.onmessage = evt => evt.ports[0].postMessage('redirected-imported-script-2');
+ `,
+ },
+ });
+
+ await ensureDataCleanup();
+ const contentPage = await ExtensionTestUtils.loadContentPage(
+ "http://localhost/page.html"
+ );
+
+ // Register the worker while the test extension isn't loaded and cannot
+ // intercept and redirect the importedScripts requests.
+ let workerMessage = await contentPage.legacySpawn([], async () => {
+ const reg = await this.content.navigator.serviceWorker.register("/sw.js");
+ return new Promise(resolve => {
+ const { port1, port2 } = new MessageChannel();
+ port1.onmessage = evt => resolve(evt.data);
+ const sw = reg.active || reg.waiting || reg.installing;
+ sw.postMessage("worker-message", [port2]);
+ });
+ });
+
+ equal(
+ workerMessage,
+ "original-imported-script",
+ "Got expected worker reply (importScripts not intercepted)"
+ );
+
+ // Start the test extension to redirect importScripts requests.
+ await extension.startup();
+
+ // Trigger an update on the registered service worker, then assert that the
+ // reply got is coming from the script where the extension is redirecting the
+ // request.
+ info("Update service worker and expect extension script to reply");
+ workerMessage = await testSWUpdate(contentPage);
+ await extension.awaitMessage("request-redirected");
+ equal(
+ workerMessage,
+ "redirected-imported-script-1",
+ "Got expected worker reply (importScripts redirected to moz-extension url)"
+ );
+
+ // Trigger a new update of the registered service worker, then assert that the
+ // reply got is coming from a different script where the extension is
+ // redirecting the second request (this confirms that the extension can
+ // intercept and can change the redirected imported scripts on new service
+ // worker updates).
+ info("Update service worker and expect new extension script to reply");
+ workerMessage = await testSWUpdate(contentPage);
+ await extension.awaitMessage("request-redirected");
+ equal(
+ workerMessage,
+ "redirected-imported-script-2",
+ "Got expected worker reply (importScripts redirected to moz-extension url again)"
+ );
+
+ // Uninstall the extension and trigger one more update of the registered
+ // service worker, then assert that the reply got is the one coming from the
+ // server (because difference from the one got from the cache).
+ // This verify that the service worker are updated as expected after the
+ // extension is uninstalled and the worker is not stuck on the script where
+ // the extension did redirect the request the last time.
+ info(
+ "Unload extension, update service worker and expect original script to reply"
+ );
+ await extension.unload();
+ workerMessage = await testSWUpdate(contentPage);
+ equal(
+ workerMessage,
+ "original-imported-script",
+ "Got expected worker reply (importScripts not intercepted)"
+ );
+
+ await contentPage.close();
+});