summaryrefslogtreecommitdiff
path: root/data/extensions/jsr@javascriptrestrictor/nscl/common/SyncMessage.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/jsr@javascriptrestrictor/nscl/common/SyncMessage.js')
-rw-r--r--data/extensions/jsr@javascriptrestrictor/nscl/common/SyncMessage.js615
1 files changed, 0 insertions, 615 deletions
diff --git a/data/extensions/jsr@javascriptrestrictor/nscl/common/SyncMessage.js b/data/extensions/jsr@javascriptrestrictor/nscl/common/SyncMessage.js
deleted file mode 100644
index db98d6f..0000000
--- a/data/extensions/jsr@javascriptrestrictor/nscl/common/SyncMessage.js
+++ /dev/null
@@ -1,615 +0,0 @@
-/*
- * NoScript Commons Library
- * Reusable building blocks for cross-browser security/privacy WebExtensions.
- * Copyright (C) 2020-2024 Giorgio Maone <https://maone.net>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later
- * version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with
- * this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-// depends on /nscl/common/uuid.js
-// depends on /nscl/common/SyncMessage/request.json
-// depends on /nscl/common/SyncMessage/response.json
-
-"use strict";
-if (!["onSyncMessage", "sendSyncMessage"].some((m) => browser.runtime[m])) {
- const MOZILLA =
- self.XMLHttpRequest && "mozSystem" in self.XMLHttpRequest.prototype;
-
- const INTERNAL_PATH = "/nscl/common/SyncMessage/";
-
- const MANIFEST = browser.runtime.getManifest();
- const USE_INTERNAL_URIS = MANIFEST.web_accessible_resources
- ?.some(({ resources }) =>
- resources.includes(`${INTERNAL_PATH}*`)
- );
- const IPV6_DUMMY_ENDPOINT = "https://[ff00::]";
- const BASE_PREFIX = browser.runtime.getURL(INTERNAL_PATH);
- // We cannot use BASE_PREFIX w/ internal URIs for requests (yet?) because
- // neither DNR nor webRequest nor ServiceWorker intercept our own extension URLs :(
- const REQUEST_PREFIX = `${IPV6_DUMMY_ENDPOINT}/${BASE_PREFIX}request.json?`;
- // But we can redirect to extension URLs on MV3
- const RESPONSE_PREFIX = USE_INTERNAL_URIS ? BASE_PREFIX + "response.json?" : "data:application/json,";
-
- const msgUrl = (msgId) => `${REQUEST_PREFIX}id=${encodeURIComponent(msgId)}`;
-
- // https://github.com/w3c/webappsec-permissions-policy/blob/main/permissions-policy-explainer.md#appendix-big-changes-since-this-was-called-feature-policy
- const allowSyncXhr = (policy) =>
- policy
- .replace(/(?:[,;]\s*)?\b(?:sync-xhr\b[^;,]*)/gi, "")
- .replace(/^\s*[;,]\s*/, "");
-
- if (browser.webRequest) {
- // Background script / event page / service worker
-
- const USE_SERVICE_WORKER = "onfetch" in self && REQUEST_PREFIX.startsWith(BASE_PREFIX);
-
- let anyMessageYet = false;
-
- const retries = new Set();
-
- // we don't care this is async, as long as it get called before the
- // sync XHR (we are not interested in the response on the content side)
- browser.runtime.onMessage.addListener((m, sender) => {
- let wrapper = m.__syncMessage__;
- if (!wrapper) return;
- if(wrapper.retry) {
- const retryKey = `${sender.tab.id}:${sender.frameId}:${sender.origin}@${sender.url}`;
- let retried = retries.has(retryKey);
- if (retried) {
- retries.delete(retryKey);
- } else {
- retries.add(retryKey);
- }
- return Promise.resolve(!retried);
- }
- if (wrapper.release) {
- suspender.release(wrapper.id);
- } else if ("payload" in wrapper) {
- anyMessageYet = true;
- wrapper.result = Promise.resolve(
- notifyListeners(JSON.stringify(wrapper.payload), sender)
- );
- suspender.hold(wrapper);
- }
- return Promise.resolve(null);
- });
-
- const asyncResults = new Map();
-
- const ret = (r) => ({
- redirectUrl: `${
- RESPONSE_PREFIX
- }${
- encodeURIComponent(JSON.stringify(r))
- }`,
- });
- const res = (payload) => ({ payload });
- const err = (e) => ({ error: { message: e.message, stack: e.stack } });
-
- const LOOP_RET = ret({ loop: 1 });
-
- const asyncRet = (msgId) => {
- let chunks = asyncResults.get(msgId);
- let chunk = chunks.shift();
- let more = chunks.length;
- if (more === 0) {
- asyncResults.delete(msgId);
- suspender.release(msgId);
- }
- return ret({ chunk, more });
- };
-
- const CHUNK_SIZE = 500000; // Work around any browser-dependent URL limit
- const storeAsyncRet = (msgId, r) => {
- r = JSON.stringify(r);
- const len = r === undefined ? 0 : r.length;
- const chunksCount = Math.ceil(len / CHUNK_SIZE);
- const chunks = [];
- for (let j = 0; j < chunksCount; j++) {
- chunks.push(r.substr(j * CHUNK_SIZE, CHUNK_SIZE));
- }
- asyncResults.set(msgId, chunks);
- };
-
- const listeners = new Set();
- function notifyListeners(msg, sender) {
- // Just like in the async runtime.sendMessage() API,
- // we process the listeners in order until we find a not undefined
- // result, then we return it (or undefined if none returns anything).
- for (let l of listeners) {
- try {
- let result = l(JSON.parse(msg), sender);
- if (result !== undefined) return result;
- } catch (e) {
- console.error("%o processing message %o from %o", e, msg, sender);
- }
- }
- }
-
- const url2MsgId = url => new URLSearchParams(url.split("?")[1])?.get("id");
- class Suspender {
- #pending = new Map();
- constructor(init) {
- init.apply(this);
- }
- async hold(wrapper) {
- this.#pending.set(wrapper.id, wrapper);
- }
- release(id) {
- this.#pending.delete(id);
- }
- get(id) {
- return this.#pending.get(id);
- }
- }
-
- const suspender =
- USE_SERVICE_WORKER
- ? new Suspender(function() {
- // MV3 with service worker
- addEventListener("fetch", event => {
- const msgId = url2MsgId(event.request.url);
- if (!msgId) return;
- const wrapper = this.get(msgId);
- this.release(msgId);
- event.respondWith((async () => new Response(await wrapper.result))());
- });
- })
- : browser.declarativeNetRequest && !MOZILLA
- ? (() => {
- // MV3
- const DNR_BASE_ID = 65535;
- const DNR_BASE_PRIORITY = 1000;
- let lastRuleId = DNR_BASE_ID;
- const msg2redirector = new Map();
- const { redirectUrl } = LOOP_RET;
- const resourceTypes = ["xmlhttprequest"];
-
- const createRedirector = async (
- urlFilter,
- redirectUrl,
- options
- ) => {
- const DEFAULT_OPTIONS = {
- ruleSet: "Session",
- priority: DNR_BASE_PRIORITY + 10,
- addRules: [],
- removeRuleIds: []
- }
- let { ruleSet, priority, addRules, removeRuleIds } = Object.assign(
- {},
- DEFAULT_OPTIONS,
- options
- );
-
- const rule = {
- id: ++lastRuleId,
- priority,
- action: {
- type: "redirect",
- redirect: { url: redirectUrl },
- },
- condition: {
- urlFilter,
- resourceTypes,
- },
- };
-
-
- addRules.push(rule);
- const method = `update${ruleSet}Rules`;
- await browser.declarativeNetRequest[method]({
- addRules,
- removeRuleIds,
- });
-
- return lastRuleId;
- };
-
- const removeRedirector = (redirId) => {
- browser.declarativeNetRequest.updateSessionRules({
- removeRuleIds: [redirId],
- });
- };
-
- (async () => {
- const allowSyncXhrRules = [
- {
- id: ++lastRuleId,
- priority: DNR_BASE_PRIORITY,
- action: {
- type: "modifyHeaders",
- // Note: notwithstanding poor documentation, looks like in modern browsers
- // permissions-policy overrides (document|feature)-policy, & DNR appending
- // to the header overrides the restrictive token despite inheritance rules,
- // making the following hack work, quite surprisingly and nicely (i.e.
- // other policies, if present, remain effective).
- responseHeaders: [
- {
- header: "permissions-policy",
- operation: "append",
- value: "sync-xhr=*",
- },
- ],
- },
- condition: {
- resourceTypes: ["main_frame", "sub_frame"],
- },
- },
- ];
-
- for (const ruleSet of ["Dynamic", "Session"]) {
- try {
- const removeRuleIds = (
- await browser.declarativeNetRequest[`get${ruleSet}Rules`]()
- )
- .map((r) => r.id)
- .filter((id) => id >= DNR_BASE_ID);
- const options = {
- ruleSet,
- priority: DNR_BASE_PRIORITY,
- addRules: allowSyncXhrRules,
- removeRuleIds,
- };
- await createRedirector(
- `|${REQUEST_PREFIX}*`,
- redirectUrl,
- options
- );
- } catch (e) {
- console.error(e, "Error initializing SyncMessage DNR responders.");
- }
- }
-
- })();
-
- return {
- async hold(wrapper) {
- let result;
- try {
- result = ret(res(await wrapper.result));
- } catch (e) {
- result = ret(err(e));
- }
- const { id } = wrapper;
- const urlFilter = `|${msgUrl(wrapper.id)}`;
- const redirId = await createRedirector(urlFilter, result.redirectUrl);
- msg2redirector.set(id, redirId);
- },
- release(id) {
- const redirId = msg2redirector.get(id);
- if (!redirId) return;
- msg2redirector.delete(id);
- removeRedirector(redirId);
- },
- };
- })()
- : new Suspender(function() {
- // MV2
- const CANCEL = { cancel: true };
- const onBeforeRequest = (request) => {
- try {
- const { url } = request;
- const shortUrl = url.replace(REQUEST_PREFIX, "");
- const msgId = url2MsgId(url);
-
- const chromeRet = (resultReady) => {
- const r = resultReady
- ? asyncRet(msgId) // promise was already resolved
- : LOOP_RET;
- return r;
- };
-
- if (asyncResults.has(msgId)) {
- return chromeRet(true);
- }
-
- const wrapper = this.get(msgId);
-
- if (!wrapper) {
- return anyMessageYet
- ? CANCEL // cannot reconcile with any pending message, abort
- : LOOP_RET; // never received any message yet, retry
- }
-
- if (MOZILLA) {
- // this should be a mozilla suspension request
- return (async () => {
- try {
- return ret(res(await wrapper.result));
- } catch (e) {
- return ret(err(e));
- } finally {
- this.release(msgId);
- }
- })();
- }
-
- // CHROMIUM from now on
- // On Chromium, if the promise is not resolved yet,
- // we redirect the XHR to the same URL (hence same msgId)
- // while the result get cached for asynchronous retrieval
- wrapper.result.then(
- (r) => storeAsyncRet(msgId, res(r)),
- (e) => storeAsyncRet(msgId, err(e))
- );
- return chromeRet(asyncResults.has(msgId));
- } catch (e) {
- console.error(e);
- return CANCEL;
- }
- };
-
- const NOP = () => {};
- let bug1899786 = NOP;
- if (browser.webRequest.filterResponseData) {
- bug1899786 = (request) => {
- // work-around for https://bugzilla.mozilla.org/show_bug.cgi?id=1899786
- let compressed = false,
- xml = false;
- for (const { name, value } of request.responseHeaders) {
- switch (name.toLowerCase()) {
- case "content-encoding":
- if (
- compressed ||
- !(compressed =
- /^(?:gzip|compress|deflate|br|zstd)$/i.test(value))
- ) {
- continue;
- }
- break;
- case "content-type":
- if (xml || !(xml = /\bxml\b/i.test(value))) {
- continue;
- }
- break;
- default:
- continue;
- }
- if (compressed && xml) {
- console.log("Applying mozbug 1899786 work-around", request);
- const filter = browser.webRequest.filterResponseData(
- request.requestId
- );
- filter.ondata = (e) => {
- filter.write(e.data);
- };
- filter.onstop = () => {
- filter.close();
- };
- break;
- }
- }
- };
- (async () => {
- const version = parseInt(
- (await browser.runtime.getBrowserInfo()).version
- );
- if (version < 126) bug1899786 = NOP;
- })();
- }
-
- const patchHeadersForXhr = MANIFEST.manifest_version < 3
- ? NOP // XHR don't need to bypass CSP in manifest V2
- : (request) => {
- let replaced = false;
- let replacedCSP = false;
- const { responseHeaders } = request;
- const CSP = "content-security-policy";
- const rxPolicy = /^(?:feature|permissions|document)-policy$/;
- for (let h of responseHeaders) {
- const name = h.name.toLowerCase();
- let value;
- if (rxPolicy.test(name)) {
- value = allowSyncXhr(h.value);
- } else if (name == CSP) {
- value = h.value.replace(/connect-src [^;]+/g, m => {
- const tokens = new Set(m.split(/\s+/));
- tokens.delete("'none'");
- const msgSrc = new URL(REQUEST_PREFIX).origin;
- tokens.has(msgSrc) || tokens.add(msgSrc);
- return [...tokens].join(" ");
- });
- replacedCSP = true;
- } else {
- continue;
- }
- if (value !== h.value) {
- h.value = value;
- replaced = true;
- }
- }
- if (replaced) {
- console.log("Patched responseHeaders", request.url, responseHeaders); // DEV_ONLY
- if (replacedCSP) {
- // We need to clear the header first, in order to avoid merging, see
- // - https://searchfox.org/mozilla-central/source/toolkit/components/extensions/webrequest/WebRequest.sys.mjs#257
- // - https://bugzilla.mozilla.org/show_bug.cgi?id=1462989
- // This does NOT work (yet?) on MV3, see https://github.com/w3c/webextensions/issues/730
- responseHeaders.unshift({name: CSP, value: ""});
- }
- return { responseHeaders };
- }
- };
-
- const onHeadersReceived = (request) => {
- bug1899786(request);
- return patchHeadersForXhr(request);
- };
-
- browser.webRequest.onBeforeRequest.addListener(
- onBeforeRequest,
- {
- urls: [`${REQUEST_PREFIX}*`],
- types: ["xmlhttprequest"],
- },
- ["blocking"]
- );
- browser.webRequest.onHeadersReceived.addListener(
- onHeadersReceived,
- {
- urls: ["<all_urls>"],
- types: ["main_frame", "sub_frame"],
- },
- ["blocking", "responseHeaders"]
- );
-
-
- }
- );
-
-
- browser.runtime.onSyncMessage = Object.freeze({
- BASE_PREFIX,
- REQUEST_PREFIX,
- RESPONSE_PREFIX,
- addListener(l) {
- listeners.add(l);
- },
- removeListener(l) {
- listeners.delete(l);
- },
- hasListener(l) {
- return listeners.has(l);
- },
- isMessageRequest({type, url}) {
- return (
- type === "xmlhttprequest" &&
- url.includes(INTERNAL_PATH) &&
- (url.includes(REQUEST_PREFIX) || url.includes(RESPONSE_PREFIX))
- );
- },
- });
- } else {
- // Content Script side
- {
- // re-enable Sync XHR if disabled by featurePolicy
- const allow = f => {
- if (f.allow) {
- const allowingValue = allowSyncXhr(f.allow);
- if (f.allow != allowingValue) {
- f.allow = allowingValue;
- f.src = f.src;
- }
- }
- };
- try {
- // this is probably useless, but nontheless...
- window.frameElement && allow(window.frameElement);
- } catch (e) {
- // SOP violation?
- console.error(e); // DEV_ONLY
- }
- const mutationsCallback = records => {
- for (var r of records) {
- switch (r.type) {
- case "attributes":
- allow(r.target);
- break;
- case "childList":
- [...r.addedNodes].forEach(allow);
- break;
- }
- }
- };
- const observer = new MutationObserver(mutationsCallback);
- observer.observe(document.documentElement, {
- childList: true,
- subtree: true,
- attributeFilter: ["allow"],
- });
- }
-
- const docId = uuid();
- browser.runtime.sendSyncMessage = (msg) => {
- let msgId = `${uuid()}:${docId}`;
- let url = msgUrl(msgId);
-
- const preSend = __syncMessage__ => browser.runtime.sendMessage({__syncMessage__});
-
- // We first need to send an async message with both the payload
- // and "trusted" sender metadata, along with an unique msgId to
- // reconcile with in the retrieval phase via synchronous XHR
- const preflight = preSend({ id: msgId, payload: msg });
-
- // Now go retrieve the result!
- const MAX_LOOPS = 1000;
- let r = new XMLHttpRequest();
-
- let result;
- let chunks = [];
- for (let loop = 0; ; ) {
- try {
- r.open("GET", url, false);
- r.send(null);
- const rawResult = r.responseURL.startsWith(RESPONSE_PREFIX)
- ? decodeURIComponent(r.responseURL.replace(RESPONSE_PREFIX, ""))
- : r.responseText;
- result = JSON.parse(rawResult);
- if ("chunk" in result) {
- let { chunk, more } = result;
- chunks.push(chunk);
- if (more) {
- continue;
- }
- result = JSON.parse(chunks.join(""));
- } else if (result.loop) {
- if (++loop > MAX_LOOPS) {
- console.debug(
- "Too many loops (%s), look for deadlock conditions.",
- loop
- );
- throw new Error("Too many SyncMessage loops!");
- }
- continue;
- } else if (result.error) {
- result.error = new Error(result.error.message + ` (${url})`, result.error);
- }
- } catch (e) {
- console.error(e,
- `SyncMessage ${msgId} error in ${document.URL}: ${e.message} (response ${url} - ${r.responseURL} - ${r.responseText})`
- );
- result = {
- error: new Error(`SyncMessage Error ${e.message}`, { cause: e }),
- };
- }
- break;
- }
- preSend({ id: msgId, release: true });
- if (result.error) {
- if (document.featurePolicy && !document.featurePolicy?.allowsFeature("sync-xhr")) {
- throw new Error(`SyncMessage fails on ${document.URL} because sync-xhr is not allowed!`);
- }
- if (document.readyState == "loading" && /Failed to load/.test(result.error.message)) {
- window.stop();
- (async () => {
- try {
- await preflight;
- browser.runtime.sendSyncMessage(msg);
- } catch (e) {
- console.error(e, `SyncMessage immediate retry failed on ${document.URL}!`);
- if (!(await preSend({retry: true}))) {
- return;
- }
- }
- history.go(0);
- })();
- }
- throw result.error;
- }
- return result.payload;
- };
- }
-}