summaryrefslogtreecommitdiff
path: root/data/extensions/jsr@javascriptrestrictor/nscl/service
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/jsr@javascriptrestrictor/nscl/service')
-rw-r--r--data/extensions/jsr@javascriptrestrictor/nscl/service/DocStartInjection.js221
-rw-r--r--data/extensions/jsr@javascriptrestrictor/nscl/service/NavCache.js83
-rw-r--r--data/extensions/jsr@javascriptrestrictor/nscl/service/TabCache.js44
3 files changed, 348 insertions, 0 deletions
diff --git a/data/extensions/jsr@javascriptrestrictor/nscl/service/DocStartInjection.js b/data/extensions/jsr@javascriptrestrictor/nscl/service/DocStartInjection.js
new file mode 100644
index 0000000..aae8e38
--- /dev/null
+++ b/data/extensions/jsr@javascriptrestrictor/nscl/service/DocStartInjection.js
@@ -0,0 +1,221 @@
+/*
+ * NoScript Commons Library
+ * Reusable building blocks for cross-browser security/privacy WebExtensions.
+ * Copyright (C) 2020-2021 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/lib/sha256.js
+// depends on /nscl/common/uuid.js
+
+"use strict";
+
+var DocStartInjection = (() => {
+ const MSG_ID = "__DocStartInjection__";
+ let repeating = !("contentScripts" in browser);
+ let scriptBuilders = new Set();
+ let getId = ({requestId, tabId, frameId, url}) => requestId || `${tabId}:${frameId}:${url}`;
+ let pending = new Map();
+
+ function onMessage(msg, sender) {
+ let payload = msg[MSG_ID];
+ if (!payload) return;
+ let {id, tabId, frameId, url} = payload;
+ let ret = false;
+ if (tabId === sender.tab.id && frameId === sender.frameId && url === sender.url) {
+ end(payload, true);
+ ret = true;
+ }
+ return Promise.resolve(ret);
+ }
+
+ async function begin(request) {
+ let scripts = new Set();
+ let {tabId, frameId, url} = request;
+ if (tabId < 0 || !/^(?:(?:https?|ftp|data|blob|file):|about:blank$)/.test(url)) return;
+
+ await Promise.all([...scriptBuilders].map(async buildScript => {
+ try {
+ let script = await buildScript({tabId, frameId, url});
+ if (script) scripts.add(`try {
+ ${typeof script === "function" ? `(${script})();` : script}
+ } catch (e) {
+ console.error("Error in DocStartInjection script", e);
+ }`);
+ } catch (e) {
+ error("Error calling DocStartInjection scriptBuilder", buildScript, e);
+ }
+ }));
+
+ if (scripts.size === 0) {
+ debug(`DocStartInjection: no script to inject in ${url}`);
+ return;
+ }
+
+ let id = getId(request);
+
+ if (repeating) {
+ let scriptsBlock = [...scripts].join("\n");
+ let injectionId = `injection:${uuid()}:${sha256(scriptsBlock)}`;
+ let args = {
+ code: `(() => {
+ let injectionId = ${JSON.stringify(injectionId)};
+ if (document.readyState === "complete" ||
+ window[injectionId] ||
+ document.URL !== ${JSON.stringify(url)}
+ ) return window[injectionId];
+ window[injectionId] = true;
+ ${scriptsBlock}
+ return document.readyState === "loading";
+ })();`,
+ runAt: "document_start",
+ frameId,
+ };
+ pending.set(id, args);
+ await run(request, true);
+ } else {
+ let matches = [url];
+ try {
+ let urlObj = new URL(url);
+ if (urlObj.port) {
+ urlObj.port = "";
+ matches[0] = urlObj.toString();
+ }
+ } catch (e) {}
+
+ let ackMsg = JSON.stringify({
+ [MSG_ID]: {id, tabId, frameId, url}
+ });
+ scripts.add(`if (document.readyState !== "complete") browser.runtime.sendMessage(${ackMsg});`);
+
+ let options = {
+ js: [...scripts].map(code => ({code})),
+ runAt: "document_start",
+ matchAboutBlank: true,
+ matches,
+ allFrames: true,
+ };
+ let current = pending.get(id);
+ if (current) {
+ current.unregister();
+ }
+ pending.set(id, await browser.contentScripts.register(options));
+ }
+ }
+
+ async function run(request, repeat = false) {
+ let id = getId(request);
+ let args = pending.get(id);
+ if (!args) return;
+ let {url, tabId} = request;
+ let attempts = 0, success = false;
+ for (; pending.has(id);) {
+ attempts++;
+ try {
+ if (attempts % 1000 === 0) {
+ let tab = await browser.tabs.get(request.tabId);
+ if (tab.url !== url) {
+ console.error(`Tab mismatch: ${tab.url} <> ${url} (download-triggered?)`);
+ break;
+ }
+ console.error(`DocStartInjection at ${url} ${attempts} failed attempts so far...`);
+ }
+ let ret = await browser.tabs.executeScript(tabId, args);
+ if (success = ret[0]) {
+ break;
+ }
+ } catch (e) {
+ if (/No tab\b/.test(e.message)) {
+ break;
+ }
+ if (!/\baccess\b/.test(e.message)) {
+ console.error(e.message);
+ }
+ if (attempts % 1000 === 0) {
+ console.error(`DocStartInjection at ${url} ${attempts} failed attempts`, e);
+ }
+ } finally {
+ if (!repeat) break;
+ }
+ }
+ pending.delete(id);
+ debug(`DocStartInjection at ${url}, ${attempts} attempts, success = ${success}, repeat = ${repeat}.`);
+ }
+
+ function end(request, immediate = false) {
+ let id = getId(request);
+ let script = pending.get(id);
+ if (script) {
+ if (repeating) {
+ run(request, false);
+ } else {
+ pending.delete(id);
+ if (immediate) {
+ script.unregister();
+ } else {
+ setTimeout(() => script.unregister(), 500);
+ }
+ }
+ }
+ }
+
+ let listeners = {
+ onBeforeNavigate: begin,
+ onErrorOccurred: end,
+ onCompleted: end,
+ }
+
+ function listen(enabled) {
+ let {webNavigation, webRequest} = browser;
+ let method = `${enabled ? "add" : "remove"}Listener`;
+ let reqFilter = {urls: ["<all_urls>"], types: ["main_frame", "sub_frame", "object"]};
+ function setup(api, eventName, listener, ...args) {
+ let event = api[eventName];
+ if (event) {
+ event[method].apply(event, enabled ? [listener, ...args] : [listener]);
+ }
+ }
+ if (repeating) {
+ // Just Chromium
+ setup(webRequest, "onResponseStarted", begin, reqFilter);
+ } else {
+ // add or remove Firefox's webNavigation listeners for non-http loads
+ // and asynchronous blocking onHeadersReceived for registration on http
+ let navFilter = enabled && {url: [{schemes: ["file", "ftp"]}]};
+ for (let [eventName, listener] of Object.entries(listeners)) {
+ setup(webNavigation, eventName, listener, navFilter)
+ }
+ setup(webRequest, "onHeadersReceived", begin, reqFilter, ["blocking"]);
+ browser.runtime.onMessage[method](onMessage);
+ }
+
+ // add or remove common webRequest listener
+ for (let [eventName, listener] of Object.entries(listeners)) {
+ setup(webRequest, eventName, listener, reqFilter);
+ }
+ }
+
+ return {
+ register(scriptBuilder) {
+ if (scriptBuilders.size === 0) listen(true);
+ scriptBuilders.add(scriptBuilder);
+ },
+ unregister(scriptBuilder) {
+ scriptBuilders.delete(scriptBuilder);
+ if (scriptBuilders.size() === 0) listen(false);
+ }
+ };
+})(); \ No newline at end of file
diff --git a/data/extensions/jsr@javascriptrestrictor/nscl/service/NavCache.js b/data/extensions/jsr@javascriptrestrictor/nscl/service/NavCache.js
new file mode 100644
index 0000000..43ee91c
--- /dev/null
+++ b/data/extensions/jsr@javascriptrestrictor/nscl/service/NavCache.js
@@ -0,0 +1,83 @@
+/*
+ * NoScript Commons Library
+ * Reusable building blocks for cross-browser security/privacy WebExtensions.
+ * Copyright (C) 2020-2021 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/>.
+ */
+
+var NavCache = (() => {
+
+ let tabs = {};
+ let listeners = new Set();
+
+ let clone = structuredClone || (o => JSON.parse(JSON.stringify(o)));
+
+ browser.webNavigation.onCommitted.addListener(({tabId, frameId, url, parentFrameId}) => {
+ let tab = tabs[tabId];
+ let frame = tab && tab[frameId];
+ if (!tab || frameId == 0) {
+ tabs[tabId] = tab = {};
+ }
+ let previousUrl = frame && frame.url;
+ frame = tab[frameId] = {previousUrl, url, parentFrameId};
+ if (previousUrl !== url) {
+ for (let l of listeners) {
+ try {
+ l(Object.assign({tabId, frameId}, frame));
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ });
+
+ browser.tabs.onRemoved.addListener(tabId => {
+ tabs.delete(tabId);
+ });
+
+
+ (async () => {
+
+ async function populateFrames(tab) {
+ let tabId = tab.id;
+ let frames = await browser.webNavigation.getAllFrames({tabId});
+ if (!frames) return; // invalid tab
+ if (!tabs[tabId]) tabs[tabId] = {};
+ let top = tabs[tabId];
+ for ({frameId, url, parentFrameId} of frames) {
+ tab[frameId] = {url, parentFrameId};
+ }
+ }
+ await Promise.all((await browser.tabs.query({})).map(populateFrames));
+ })();
+
+ return {
+ getTab(tabId) {
+ return clone(tabs[tabId] || {});
+ },
+ getFrame(tabId, frameId) {
+ return clone((tabs[tabId] || {})[frameId]);
+ },
+ onUrlChanged: {
+ addListener(listener) {
+ listeners.add(listener);
+ },
+ removeListener(listener) {
+ listeners.remove(listeners);
+ }
+ }
+ };
+})();
diff --git a/data/extensions/jsr@javascriptrestrictor/nscl/service/TabCache.js b/data/extensions/jsr@javascriptrestrictor/nscl/service/TabCache.js
new file mode 100644
index 0000000..79a01ea
--- /dev/null
+++ b/data/extensions/jsr@javascriptrestrictor/nscl/service/TabCache.js
@@ -0,0 +1,44 @@
+/*
+ * NoScript Commons Library
+ * Reusable building blocks for cross-browser security/privacy WebExtensions.
+ * Copyright (C) 2020-2021 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/>.
+ */
+
+var TabCache = (() => {
+
+ let cache = new Map();
+
+ browser.tabs.onUpdated.addListener((tabId, changes, tab) => {
+ cache.set(tabId, tab);
+ });
+
+ browser.tabs.onRemoved.addListener(tabId => {
+ cache.delete(tabId);
+ });
+
+ (async () => {
+ for (let tab of await browser.tabs.query({})) {
+ cache.set(tab.id, tab);
+ }
+ })();
+
+ return {
+ get(tabId) {
+ return cache.get(tabId);
+ }
+ };
+})();