diff options
Diffstat (limited to 'data/extensions/jsr@javascriptrestrictor/nscl/common/SyncMessage.js')
-rw-r--r-- | data/extensions/jsr@javascriptrestrictor/nscl/common/SyncMessage.js | 615 |
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; - }; - } -} |