aboutsummaryrefslogtreecommitdiff
path: root/background
diff options
context:
space:
mode:
authorWojtek Kosior <wk@koszkonutek-tmp.pl.eu.org>2021-05-10 18:07:05 +0200
committerWojtek Kosior <wk@koszkonutek-tmp.pl.eu.org>2021-05-10 18:18:52 +0200
commit01937dc9d5215ef96ce756e3ccda51bf29032f58 (patch)
tree609ec5bb48c692796520f7982c06b30633038588 /background
downloadbrowser-extension-01937dc9d5215ef96ce756e3ccda51bf29032f58.tar.gz
browser-extension-01937dc9d5215ef96ce756e3ccda51bf29032f58.zip
initial commit
Diffstat (limited to 'background')
-rw-r--r--background/ResponseHandler.mjs257
-rw-r--r--background/ResponseMetaData.mjs107
-rw-r--r--background/ResponseProcessor.mjs145
-rw-r--r--background/background.html7
-rw-r--r--background/main.mjs168
-rw-r--r--background/message_server.mjs31
-rw-r--r--background/page_actions_server.mjs145
-rw-r--r--background/policy_smuggler.mjs60
-rw-r--r--background/reverse_use_info.mjs93
-rw-r--r--background/script_injector.mjs122
-rw-r--r--background/sha256.mjs524
-rw-r--r--background/storage.mjs380
-rw-r--r--background/storage_server.mjs58
-rw-r--r--background/url_item.mjs18
14 files changed, 2115 insertions, 0 deletions
diff --git a/background/ResponseHandler.mjs b/background/ResponseHandler.mjs
new file mode 100644
index 0000000..6b979e6
--- /dev/null
+++ b/background/ResponseHandler.mjs
@@ -0,0 +1,257 @@
+/**
+* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+* *
+* Copyright (C) 2017, 2018 Nathan Nichols
+* Copyright (C) 2018 Ruben Rodriguez <ruben@gnu.org>
+*
+* This file is part of GNU LibreJS.
+*
+* GNU LibreJS 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.
+*
+* GNU LibreJS 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 GNU LibreJS. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/**
+* This listener gets called as soon as we've got all the HTTP headers, can guess
+* content type and encoding, and therefore correctly parse HTML documents
+* and external script inclusions in search of crappy JavaScript
+*/
+
+import inject_scripts from './script_injector.mjs';
+import {ResponseProcessor} from './ResponseProcessor.mjs';
+
+"use strict";
+
+var ResponseHandler = {
+ /**
+ * Enforce white/black lists for url/site early (hashes will be handled later)
+ */
+ async pre(response) {
+ // TODO: reimplement blacklisting/whitelisting later
+ if (true) return ResponseProcessor.CONTINUE;
+
+ let {request} = response;
+ let {url, type, tabId, frameId, documentUrl} = request;
+
+ let fullUrl = url;
+ url = ListStore.urlItem(url);
+ let site = ListStore.siteItem(url);
+
+ let blacklistedSite = ListManager.siteMatch(site, blacklist);
+ let blacklisted = blacklistedSite || blacklist.contains(url);
+ let topUrl = type === "sub_frame" && request.frameAncestors && request.frameAncestors.pop() || documentUrl;
+
+ if (blacklisted) {
+ if (type === "script") {
+ // this shouldn't happen, because we intercept earlier in blockBlacklistedScripts()
+ return ResponseProcessor.REJECT;
+ }
+ if (type === "main_frame") { // we handle the page change here too, since we won't call edit_html()
+ activityReports[tabId] = await createReport({url: fullUrl, tabId});
+ // Go on without parsing the page: it was explicitly blacklisted
+ let reason = blacklistedSite
+ ? `All ${blacklistedSite} blacklisted by user`
+ : "Address blacklisted by user";
+ await addReportEntry(tabId, url, {"blacklisted": [blacklistedSite || url, reason], url: fullUrl});
+ }
+ // use CSP to restrict JavaScript execution in the page
+ request.responseHeaders.unshift({
+ name: `Content-security-policy`,
+ value: `script-src 'none';`
+ });
+ return {responseHeaders: request.responseHeaders}; // let's skip the inline script parsing, since we block by CSP
+ } else {
+
+ let whitelistedSite = ListManager.siteMatch(site, whitelist);
+ let whitelisted = response.whitelisted = whitelistedSite || whitelist.contains(url);
+ if (type === "script") {
+ if (whitelisted) {
+ // accept the script and stop processing
+ addReportEntry(tabId, url, {url: topUrl,
+ "whitelisted": [url, whitelistedSite ? `User whitelisted ${whitelistedSite}` : "Whitelisted by user"]});
+ return ResponseProcessor.ACCEPT;
+ } else {
+ let scriptInfo = await ExternalLicenses.check({url: fullUrl, tabId, frameId, documentUrl});
+ if (scriptInfo) {
+ let verdict, ret;
+ let msg = scriptInfo.toString();
+ if (scriptInfo.free) {
+ verdict = "accepted";
+ ret = ResponseProcessor.ACCEPT;
+ } else {
+ verdict = "blocked";
+ ret = ResponseProcessor.REJECT;
+ }
+ addReportEntry(tabId, url, {url, [verdict]: [url, msg]});
+ return ret;
+ }
+ }
+ }
+ }
+ // it's a page (it's too early to report) or an unknown script:
+ // let's keep processing
+ return ResponseProcessor.CONTINUE;
+ },
+
+ /**
+ * Here we do the heavylifting, analyzing unknown scripts
+ */
+ async post(response) {
+ let {type} = response.request;
+ return await handle_html(response, response.whitelisted);
+ }
+}
+
+/**
+* Serializes HTMLDocument objects including the root element and
+* the DOCTYPE declaration
+*/
+function doc2HTML(doc) {
+ let s = doc.documentElement.outerHTML;
+ if (doc.doctype) {
+ let dt = doc.doctype;
+ let sDoctype = `<!DOCTYPE ${dt.name || "html"}`;
+ if (dt.publicId) sDoctype += ` PUBLIC "${dt.publicId}"`;
+ if (dt.systemId) sDoctype += ` "${dt.systemId}"`;
+ s = `${sDoctype}>\n${s}`;
+ }
+ return s;
+}
+
+/**
+* Shortcut to create a correctly namespaced DOM HTML elements
+*/
+function createHTMLElement(doc, name) {
+ return doc.createElementNS("http://www.w3.org/1999/xhtml", name);
+}
+
+/**
+* Replace any element with a span having the same content (useful to force
+* NOSCRIPT elements to visible the same way as NoScript and uBlock do)
+*/
+function forceElement(doc, element) {
+ let replacement = createHTMLElement(doc, "span");
+ replacement.innerHTML = element.innerHTML;
+ element.replaceWith(replacement);
+ return replacement;
+}
+
+/**
+* Forces displaying any element having the "data-librejs-display" attribute and
+* <noscript> elements on pages where LibreJS disabled inline scripts (unless
+* they have the "data-librejs-nodisplay" attribute).
+*/
+function forceNoscriptElements(doc) {
+ let shown = 0;
+ // inspired by NoScript's onScriptDisabled.js
+ for (let noscript of doc.querySelectorAll("noscript:not([data-librejs-nodisplay])")) {
+ let replacement = forceElement(doc, noscript);
+ // emulate meta-refresh
+ let meta = replacement.querySelector('meta[http-equiv="refresh"]');
+ if (meta) {
+ refresh = true;
+ doc.head.appendChild(meta);
+ }
+ shown++;
+ }
+ return shown;
+}
+
+/**
+* Forces displaying any element having the "data-librejs-display" attribute and
+* <noscript> elements on pages where LibreJS disabled inline scripts (unless
+* they have the "data-librejs-nodisplay" attribute).
+*/
+function showConditionalElements(doc) {
+ let shown = 0;
+ for (let element of document.querySelectorAll("[data-librejs-display]")) {
+ forceElement(doc, element);
+ shown++;
+ }
+ return shown;
+}
+
+/**
+
+* Reads/changes the HTML of a page and the scripts within it.
+*/
+async function editHtml(html, documentUrl, tabId, frameId, whitelisted){
+
+ var parser = new DOMParser();
+ var html_doc = parser.parseFromString(html, "text/html");
+
+ if (whitelisted) { // don't bother rewriting
+ return null;
+ }
+
+ var scripts = html_doc.scripts;
+
+ let findLine = finder => finder.test(html) && html.substring(0, finder.lastIndex).split(/\n/).length || 0;
+
+ let modified = false;
+ // Deal with intrinsic events
+ let intrinsicFinder = /<[a-z][^>]*\b(on\w+|href\s*=\s*['"]?javascript:)/gi;
+ for (let element of html_doc.all) {
+ let line = -1;
+ for (let attr of element.attributes) {
+ let {name, value} = attr;
+ value = value.trim();
+ if (name.startsWith("on")) {
+ attr.value = "console.log(\"event script blocked by myext\")";
+ } else if (name === "href" && value.toLowerCase().startsWith("javascript:")){
+ if (line === -1) {
+ line = findLine(intrinsicFinder);
+ }
+ try {
+ attr.value = `view-source:${documentUrl}#line${line}`;
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ }
+
+ let modifiedInline = false;
+ let scriptFinder = /<script\b/ig;
+ for(let i = 0, len = scripts.length; i < len; i++) {
+ let script = scripts[i];
+ let line = findLine(scriptFinder);
+ if (!script.src) {
+ script.textContent = `//script blocked, you can examine it at view-source:${documentUrl}#line${line}`;
+ } else {
+ let src = script.src;
+ script.removeAttribute("src");
+ script.setAttribute("blocked-src", src);
+ script.textContent = "//script blocked";
+ }
+ }
+
+ showConditionalElements(html_doc);
+ forceNoscriptElements(html_doc);
+ await inject_scripts(documentUrl, html_doc);
+ return doc2HTML(html_doc);
+}
+
+/**
+* Here we handle html document responses
+*/
+async function handle_html(response, whitelisted) {
+ let {text, request} = response;
+ let {url, tabId, frameId, type} = request;
+ if (type === "main_frame") {
+ //activityReports[tabId] = await createReport({url, tabId});
+ //updateBadge(tabId);
+ }
+ return await editHtml(text, url, tabId, frameId, whitelisted);
+}
+
+export default ResponseHandler;
diff --git a/background/ResponseMetaData.mjs b/background/ResponseMetaData.mjs
new file mode 100644
index 0000000..345fc54
--- /dev/null
+++ b/background/ResponseMetaData.mjs
@@ -0,0 +1,107 @@
+/**
+* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+*
+* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net>
+*
+* This file is part of GNU LibreJS.
+*
+* GNU LibreJS 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.
+*
+* GNU LibreJS 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 GNU LibreJS. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/**
+ This class parses HTTP response headers to extract both the
+ MIME Content-type and the character set to be used, if specified,
+ to parse textual data through a decoder.
+*/
+
+"use strict";
+
+const BOM = [0xEF, 0xBB, 0xBF];
+const DECODER_PARAMS = {stream: true};
+
+class ResponseMetaData {
+ constructor(request) {
+ let {responseHeaders} = request;
+ this.headers = {};
+ for (let h of responseHeaders) {
+ if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) {
+ let propertyName = h.name.split("-")[1].trim();
+ propertyName = `content${propertyName.charAt(0).toUpperCase()}${propertyName.substring(1).toLowerCase()}`;
+ this[propertyName] = h.value;
+ this.headers[propertyName] = h;
+ }
+ }
+ this.computedCharset = "";
+ }
+
+ get charset() {
+ let charset = "";
+ if (this.contentType) {
+ let m = this.contentType.match(/;\s*charset\s*=\s*(\S+)/);
+ if (m) {
+ charset = m[1];
+ }
+ }
+ Object.defineProperty(this, "charset", { value: charset, writable: false, configurable: true });
+ return this.computedCharset = charset;
+ }
+
+ decode(data) {
+ let charset = this.charset;
+ let decoder = this.createDecoder();
+ let text = decoder.decode(data, DECODER_PARAMS);
+ if (!charset && /html/i.test(this.contentType)) {
+ // missing HTTP charset, sniffing in content...
+
+ if (data[0] === BOM[0] && data[1] === BOM[1] && data[2] === BOM[2]) {
+ // forced UTF-8, nothing to do
+ return text;
+ }
+
+ // let's try figuring out the charset from <meta> tags
+ let parser = new DOMParser();
+ let doc = parser.parseFromString(text, "text/html");
+ let meta = doc.querySelectorAll('meta[charset], meta[http-equiv="content-type"], meta[content*="charset"]');
+ for (let m of meta) {
+ charset = m.getAttribute("charset");
+ if (!charset) {
+ let match = m.getAttribute("content").match(/;\s*charset\s*=\s*([\w-]+)/i)
+ if (match) charset = match[1];
+ }
+ if (charset) {
+ decoder = this.createDecoder(charset, null);
+ if (decoder) {
+ this.computedCharset = charset;
+ return decoder.decode(data, DECODER_PARAMS);
+ }
+ }
+ }
+ }
+ return text;
+ }
+
+ createDecoder(charset = this.charset, def = "latin1") {
+ if (charset) {
+ try {
+ return new TextDecoder(charset);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ return def ? new TextDecoder(def) : null;
+ }
+};
+ResponseMetaData.UTF8BOM = new Uint8Array(BOM);
+
+export default ResponseMetaData;
diff --git a/background/ResponseProcessor.mjs b/background/ResponseProcessor.mjs
new file mode 100644
index 0000000..85c2655
--- /dev/null
+++ b/background/ResponseProcessor.mjs
@@ -0,0 +1,145 @@
+/**
+* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+*
+* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net>
+*
+* This file is part of GNU LibreJS.
+*
+* GNU LibreJS 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.
+*
+* GNU LibreJS 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 GNU LibreJS. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/**
+ An abstraction layer over the StreamFilter API, allowing its clients to process
+ only the "interesting" HTML and script requests and leaving the other alone
+*/
+
+import ResponseMetaData from './ResponseMetaData.mjs';
+import browser from '/common/browser.mjs';
+
+let listeners = new WeakMap();
+let webRequestEvent = browser.webRequest.onHeadersReceived;
+
+class ResponseProcessor {
+
+ static install(handler, types = ["main_frame", "sub_frame"]) {
+ if (listeners.has(handler)) return false;
+ let listener =
+ async request => await new ResponseTextFilter(request).process(handler);
+ listeners.set(handler, listener);
+ webRequestEvent.addListener(
+ listener,
+ {urls: ["<all_urls>"], types},
+ ["blocking", "responseHeaders"]
+ );
+ return true;
+ }
+
+ static uninstall(handler) {
+ let listener = listeners.get(handler);
+ if (listener) {
+ webRequestEvent.removeListener(listener);
+ }
+ }
+}
+
+Object.assign(ResponseProcessor, {
+ // control flow values to be returned by handler.pre() callbacks
+ ACCEPT: {},
+ REJECT: {cancel: true},
+ CONTINUE: null
+});
+
+class ResponseTextFilter {
+ constructor(request) {
+ this.request = request;
+ let {type, statusCode} = request;
+ let md = this.metaData = new ResponseMetaData(request);
+ this.canProcess = // we want to process html documents and scripts only
+ (statusCode < 300 || statusCode >= 400) && // skip redirections
+ !md.disposition && // skip forced downloads
+ (type === "script" || /\bhtml\b/i.test(md.contentType));
+ }
+
+ async process(handler) {
+ if (!this.canProcess) return ResponseProcessor.ACCEPT;
+ let {metaData, request} = this;
+ let response = {request, metaData}; // we keep it around allowing callbacks to store state
+ if (typeof handler.pre === "function") {
+ let res = await handler.pre(response);
+ if (res) return res;
+ if (handler.post) handler = handler.post;
+ if (typeof handler !== "function") return ResponseProcessor.ACCEPT;
+ }
+
+ return ResponseProcessor.ACCEPT;
+
+ let {requestId, responseHeaders} = request;
+ let filter = browser.webRequest.filterResponseData(requestId);
+ let buffer = [];
+
+ filter.ondata = event => {
+ buffer.push(event.data);
+ };
+
+ filter.onstop = async event => {
+ // concatenate chunks
+ let size = buffer.reduce((sum, chunk, n) => sum + chunk.byteLength, 0)
+ let allBytes = new Uint8Array(size);
+ let pos = 0;
+ for (let chunk of buffer) {
+ allBytes.set(new Uint8Array(chunk), pos);
+ pos += chunk.byteLength;
+ }
+ buffer = null; // allow garbage collection
+ if (allBytes.indexOf(0) !== -1) {
+ console.debug("Warning: zeroes in bytestream, probable cached encoding mismatch.", request);
+ if (request.type === "script") {
+ console.debug("It's a script, trying to refetch it.");
+ response.text = await (await fetch(request.url, {cache: "reload", credentials: "include"})).text();
+ } else {
+ console.debug("It's a %s, trying to decode it as UTF-16.", request.type);
+ response.text = new TextDecoder("utf-16be").decode(allBytes, {stream: true});
+ }
+ } else {
+ response.text = metaData.decode(allBytes);
+ }
+ let editedText = null;
+ try {
+ editedText = await handler(response);
+ } catch(e) {
+ console.error(e);
+ }
+ if (editedText !== null) {
+ // we changed the content, let's re-encode
+ let encoded = new TextEncoder().encode(editedText);
+ // pre-pending the UTF-8 BOM will force the charset per HTML 5 specs
+ allBytes = new Uint8Array(encoded.byteLength + 3);
+ allBytes.set(ResponseMetaData.UTF8BOM, 0); // UTF-8 BOM
+ allBytes.set(encoded, 3);
+ }
+ filter.write(allBytes);
+ filter.close();
+ }
+
+ return ResponseProcessor.ACCEPT;
+ }
+}
+
+/* The following was originally in Storage.js */
+function url_item(url) {
+ let queryPos = url.indexOf("?");
+ return queryPos === -1 ? url : url.substring(0, queryPos);
+}
+
+export {ResponseProcessor, url_item};
diff --git a/background/background.html b/background/background.html
new file mode 100644
index 0000000..519c2a2
--- /dev/null
+++ b/background/background.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <script src="./main.mjs" type="module"></script>
+ </head>
+</html>
diff --git a/background/main.mjs b/background/main.mjs
new file mode 100644
index 0000000..5d32d98
--- /dev/null
+++ b/background/main.mjs
@@ -0,0 +1,168 @@
+/**
+* Myext main background script
+*
+* Copyright (C) 2021 Wojtek Kosior
+*
+* Dual-licensed under:
+* - 0BSD license
+* - GPLv3 or (at your option) any later version
+*/
+
+import {TYPE_PREFIX} from '/common/stored_types.mjs';
+import get_storage from './storage.mjs';
+//import {ResponseProcessor} from './ResponseProcessor.mjs';
+//import ResponseHandler from './ResponseHandler.mjs';
+import start_storage_server from './storage_server.mjs';
+import start_page_actions_server from './page_actions_server.mjs';
+import start_policy_smuggler from './policy_smuggler.mjs';
+import browser from '/common/browser.mjs';
+
+"use strict";
+
+start_storage_server();
+start_page_actions_server();
+start_policy_smuggler();
+
+async function init_myext(install_details)
+{
+ console.log("details:", install_details);
+ if (install_details.reason != "install")
+ return;
+
+ let storage = await get_storage();
+
+ await storage.clear();
+
+ /*
+ * Below we add sample settings to the extension.
+ * Those should be considered example values for viewing in the options
+ * page. They won't make my.fsf.org work. The only scripts that does
+ * something useful right now is the opencores one.
+ */
+
+ let components = [];
+ for (let script_data of [
+ {url: "http://127.0.0.1:8000/myfsf_define_CRM.js",
+ hash:"bf0cc81c7e8d5f800877b4bc3f14639f946f5ac6d4dc120255ffac5eba5e48fe"},
+ {url: "https://my.fsf.org/misc/jquery.js?v=1.4.4",
+ hash:"261ae472fa0cbf27c80c9200a1599a60fde581a0e652eee4bf41def8cb61f2d0"},
+ {url: "https://my.fsf.org/misc/jquery-extend-3.4.0.js?v=1.4.4",
+ hash:"c54103ba57ee210ca55c052e70415402707548a4e6a68dd6efb3895019bee392"},
+ {url: "https://my.fsf.org/misc/jquery-html-prefilter-3.5.0-backport.js?v=1.4.4",
+ hash:"fad84efa145fb507e5df9b582fa01b1c4e6313de7f72ebdd55726d92fa4dbf06"},
+ {url: "https://my.fsf.org/misc/jquery.once.js?v=1.2",
+ hash:"1430f42c0d760ba8e05bb3762480502e541f654fec5739ee40625ab22dc38c4f"},
+ {url: "https://my.fsf.org/misc/drupal.js?qmaukd",
+ hash:"2e08dccbd4d8b728a6871562995a4636b89bfe0ed3b8fb0138191c922228b116"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery/dist/jquery.min.js?qmaukd",
+ hash:"a6d01520d28d15dbe476de84eea90eb3ee2d058722efc062ec73cb5fad78a17b"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery-ui/jquery-ui.min.js?qmaukd",
+ hash:"28ce75d953678c4942df47a11707a15e3c756021cf89090e3e6aa7ad6b6971c3"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/lodash-compat/lodash.min.js?qmaukd",
+ hash:"f2871cc80c52fe8c04c582c4a49797c9c8fd80391cf1452e47f7fe97835ed5cc"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.mousewheel.min.js?qmaukd",
+ hash:"f50233e84c2ac7ada37a094d3f7d3b3f7c97716d6b7b47bf69619d93ee4ac1ce"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/select2/select2.min.js?qmaukd",
+ hash:"ce61298fb9aa4ec49ccd4172d097e36a9e5db3af06a7b82796659368f15b7c1b"},
+ {url: "https://my.fsf.org/sites/all/modules/civi crm/packages/jquery/plugins/jquery.form.min.js?qmaukd",
+ hash:"c90f0e501d2948fbc2b61bffd654fa4ab64741fd48923782419eeb14d3816fb8"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.timeentry.min.js?qmaukd",
+ hash:"8e85df981e8ad7049d06dfb075277d038734d36a7097c7f021021b2bdccfe9bb"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.blockUI.min.js?qmaukd",
+ hash:"806aedff52ac822f2adc5797073e1e5c5cec32eb9f15f2319cb32a347dcd232b"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/datatables/media/js/jquery.dataTables.min.js?qmaukd",
+ hash:"b796504d9b1b422f0dc6ccc2d740ac78a8c9e5078cc3934836d39742b1121925"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery-validation/dist/jquery.validate.min.js?qmaukd",
+ hash:"f0f5373ad203101ea91bf826c5a7ef8f7cd74887f06bad2cb9277a504503b9e2"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.ui.datepicker.validation.min.js?qmaukd",
+ hash:"c6e6f6bf7f8fff25cca338045774e267e8eaa2d48ac9100540f3d59a6d2b3c61"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/js/Common.js?qmaukd",
+ hash:"17aa222a3af2e8958be16accb5e77ef39f67009cb3b500718d8fffd45b399148"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/js/crm.datepicker.js?qmaukd",
+ hash:"9bd8d10208aa99c156325f7819da6f0dd62ba221ac4119c3ccd4834e2cf36535"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/js/crm.ajax.js?qmaukd",
+ hash:"6401a4e257b7499ae4a00be2c200e4504a2c9b3d6b278a830c31a7b63374f0fe"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/js/wysiwyg/crm.wysiwyg.js?qmaukd",
+ hash:"fa962356072a36672c3b4b25bdeb657f020995a067e20a29cd5bb84b05157762"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/js/noconflict.js?qmaukd",
+ hash:"58d6d9f496a235d23cf891926d71f2104e4f2afe1d14bb4e2b5233f646c35e62"},
+ {url: "https://my.fsf.org/sites/all/modules/matomo/matomo.js?qmaukd",
+ hash:"7f39ccd085f348189cd2fb62ea4d4a658d96f6bba266265880b98605e777e2de"},
+ {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/global.js?qmaukd",
+ hash:"aa7983f6b902f9f4415cfc8346e0c3f194cc95b78f52f2ad09ec7effa1326b9c"},
+ {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.superfish.min.js?qmaukd",
+ hash:"5ef1f93bf3901227056bf9ed0ed93a148eec4dda30f419756b12bedd1098815e"},
+ {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.sidr.min.js?qmaukd",
+ hash:"c4914d415826676c6af2e61f16edb72c5388f8600ba6de9049892aee49d980a0"},
+ {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.flexslider.min.js?qmaukd",
+ hash:"cefaf715761b4494913851249b9d40dacb4a8cb61242b0efc859dc586d56e0d4"},
+ {url: "http://127.0.0.1:8000/myfsf_crap.js",
+ hash:"d91ccf21592d0f861ea0ba946bc257fc5d88269327cad0a91387da6cb8ff633e"},
+ {url: "https://my.fsf.org/sites/all/modules/civicrm/templates/CRM/Core/BillingBlock.js?r=lp7Di",
+ hash:"2f25d35e7a0c0060ab0a444a577f09dd3c9934ae898a7ee0eb20b6c986ab5a1c"},
+ {url: "https://my.fsf.org/extensions/com.aghstrategies.giftmemberships/js/giftpricefield.js?r=lp7Di",
+ hash:"f86080e6bd306fe46474039aeca2808235005bce5a2a29416d08210022039a45"},
+ {url: "https://my.fsf.org/extensions/com.ginkgostreet.negativenegator/js/negativenegator.js?r=lp7Di",
+ hash:"d0e87bac832856db70947d82a7ab4e0b7c8b1070d5f1a32335345e033ece3a14"}
+ ]) {
+ let name_regex = /\/([^/]+)\.js/;
+ let name = name_regex.exec(script_data.url)[1];
+ await storage.set(TYPE_PREFIX.SCRIPT, name, script_data);
+ components.push([TYPE_PREFIX.SCRIPT, name]);
+ }
+
+ await storage.set(TYPE_PREFIX.PAGE, "https://my.fsf.org/join", {components});
+
+ let hello_script = {
+ text: "console.log(\"hello, every1!\");\n"
+ };
+ await storage.set(TYPE_PREFIX.SCRIPT, "hello", hello_script);
+ await storage.set(TYPE_PREFIX.BUNDLE, "hello",
+ [[TYPE_PREFIX.SCRIPT, "hello"]]);
+ await storage.set(TYPE_PREFIX.PAGE, "https://my.fsf.org/", {
+ components: [[TYPE_PREFIX.BUNDLE, "hello"]],
+ allow: true
+ });
+
+ let opencores_script = {
+ text: `\
+let data = JSON.parse(document.getElementById("__NEXT_DATA__").textContent);
+let sections = {};
+for (let h1 of document.getElementsByClassName("cMJCrc")) {
+ let ul = document.createElement("ul");
+ if (h1.nextElementSibling !== null)
+ h1.parentNode.insertBefore(ul, h1.nextElementSibling);
+ else
+ h1.parentNode.appendChild(ul);
+
+ sections[h1.children[1].firstChild.textContent] = ul;
+}
+
+for (let prop of data.props.pageProps.list) {
+ let ul = sections[prop.category];
+ if (ul === undefined) {
+ console.log(\`unknown category "\${prop.category}" for project "\${prop.title}"\`);
+ continue;
+ }
+
+ let li = document.createElement("li");
+ let a = document.createElement("a");
+ a.setAttribute("href", "/projects/" + prop.slug);
+ a.textContent = prop.title;
+
+ li.appendChild(a);
+ ul.appendChild(li);
+}
+`
+ };
+
+ await storage.set(TYPE_PREFIX.SCRIPT, "opencores", opencores_script);
+ await storage.set(TYPE_PREFIX.PAGE, "https://opencores.org/projects", {
+ components: [[TYPE_PREFIX.SCRIPT, "opencores"]],
+ allow: false
+ });
+}
+
+browser.runtime.onInstalled.addListener(init_myext);
+
+console.log("hello, myext");
diff --git a/background/message_server.mjs b/background/message_server.mjs
new file mode 100644
index 0000000..e3f83d0
--- /dev/null
+++ b/background/message_server.mjs
@@ -0,0 +1,31 @@
+/**
+* Myext message server
+*
+* Copyright (C) 2021 Wojtek Kosior
+*
+* Dual-licensed under:
+* - 0BSD license
+* - GPLv3 or (at your option) any later version
+*/
+
+"use strict";
+
+import browser from '/common/browser.mjs';
+
+var listeners = {};
+
+/* magic should be one of the constants from /common/connection_types.mjs */
+
+export default function listen_for_connection(magic, cb)
+{
+ listeners[magic] = cb;
+}
+
+function raw_listen(port) {
+ if (listeners[port.name] === undefined)
+ return;
+
+ listeners[port.name](port);
+}
+
+browser.runtime.onConnect.addListener(raw_listen);
diff --git a/background/page_actions_server.mjs b/background/page_actions_server.mjs
new file mode 100644
index 0000000..5fb4924
--- /dev/null
+++ b/background/page_actions_server.mjs
@@ -0,0 +1,145 @@
+/**
+* Myext serving of page actions to content scripts
+*
+* Copyright (C) 2021 Wojtek Kosior
+*
+* Dual-licensed under:
+* - 0BSD license
+* - GPLv3 or (at your option) any later version
+*/
+
+import get_storage from './storage.mjs';
+import {TYPE_PREFIX} from '/common/stored_types.mjs';
+import CONNECTION_TYPE from '/common/connection_types.mjs';
+import browser from '/common/browser.mjs';
+import listen_for_connection from './message_server.mjs';
+import url_item from './url_item.mjs';
+import sha256 from './sha256.mjs';
+
+"use strict";
+
+var storage;
+var handler;
+
+function send_scripts(url, port)
+{
+ let settings = storage.get(TYPE_PREFIX.PAGE, url_item(url));
+ if (settings === undefined)
+ return;
+
+ let components = settings.components;
+ let processed_bundles = new Set();
+
+ send_scripts_rec(components, port, processed_bundles);
+}
+
+// TODO: parallelize script fetching
+async function send_scripts_rec(components, port, processed_bundles)
+{
+ for (let [prefix, name] of components) {
+ if (prefix === TYPE_PREFIX.BUNDLE) {
+ if (processed_bundles.has(name)) {
+ console.log(`preventing recursive inclusion of bundle ${name}`);
+ continue;
+ }
+
+ var bundle = storage.get(TYPE_PREFIX.BUNDLE, name);
+
+ if (bundle === undefined) {
+ console.log(`no bundle in storage for key ${name}`);
+ continue;
+ }
+
+ processed_bundles.add(name);
+ await send_scripts_rec(bundle, port, processed_bundles);
+ processed_bundles.delete(name);
+ } else {
+ let script_text = await get_script_text(name);
+ if (script_text === undefined)
+ continue;
+
+ port.postMessage({inject : [script_text]});
+ }
+ }
+}
+
+async function get_script_text(script_name)
+{
+ try {
+ let script_data = storage.get(TYPE_PREFIX.SCRIPT, script_name);
+ if (script_data === undefined) {
+ console.log(`missing data for ${script_name}`);
+ return;
+ }
+ let script_text = script_data.text;
+ if (!script_text)
+ script_text = await fetch_remote_script(script_data);
+ return script_text;
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+function ajax_callback()
+{
+ if (this.readyState == 4)
+ this.resolve_callback(this);
+}
+
+function initiate_ajax_request(resolve, method, url)
+{
+ var xhttp = new XMLHttpRequest();
+ xhttp.resolve_callback = resolve;
+ xhttp.onreadystatechange = ajax_callback;
+ xhttp.open(method, url, true);
+ xhttp.send();
+}
+
+function make_ajax_request(method, url)
+{
+ return new Promise((resolve, reject) =>
+ initiate_ajax_request(resolve, method, url));
+}
+
+async function fetch_remote_script(script_data)
+{
+ try {
+ let xhttp = await make_ajax_request("GET", script_data.url);
+ if (xhttp.status === 200) {
+ let computed_hash = sha256(xhttp.responseText);
+ if (computed_hash !== script_data.hash) {
+ console.log(`Bad hash for ${script_data.url}\n got ${computed_hash} instead of ${script_data.hash}`);
+ return;
+ }
+ return xhttp.responseText;
+ } else {
+ console.log("script not fetched: " + script_data.url);
+ return;
+ }
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+function handle_message(port, message, handler)
+{
+ port.onMessage.removeListener(handler[0]);
+ let url = message.url;
+ console.log({url});
+ send_scripts(url, port);
+}
+
+function new_connection(port)
+{
+ console.log("new page actions connection!");
+ let handler = [];
+ handler.push(m => handle_message(port, m, handler));
+ port.onMessage.addListener(handler[0]);
+}
+
+export default async function start()
+{
+ storage = await get_storage();
+
+ listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection);
+}
diff --git a/background/policy_smuggler.mjs b/background/policy_smuggler.mjs
new file mode 100644
index 0000000..61f34c5
--- /dev/null
+++ b/background/policy_smuggler.mjs
@@ -0,0 +1,60 @@
+/**
+* Myext smuggling policy to content script through url
+*
+* Copyright (C) 2021 Wojtek Kosior
+*
+* Dual-licensed under:
+* - 0BSD license
+* - GPLv3 or (at your option) any later version
+*/
+
+import {TYPE_PREFIX} from '/common/stored_types.mjs';
+import get_storage from './storage.mjs';
+import browser from '/common/browser.mjs';
+import url_item from './url_item.mjs';
+
+"use strict";
+
+var storage;
+
+function redirect(request)
+{
+ let url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/;
+ let match = url_re.exec(request.url);
+ let base_url = match[1];
+ let first_target = match[3];
+ let second_target = match[4];
+
+ if (first_target === "#myext-allow") {
+ console.log(["not redirecting"]);
+ return {cancel : false};
+ }
+
+ let url = url_item(request.url);
+ let settings = storage.get(TYPE_PREFIX.PAGE, url);
+ console.log("got", storage.get(TYPE_PREFIX.PAGE, url), "for", url);
+ if (settings === undefined || !settings.allow)
+ return {cancel : false};
+
+ second_target = (first_target || "") + (second_target || "")
+
+ console.log(["redirecting", request.url,
+ (base_url + "#myext-allow" + second_target)]);
+
+ return {
+ redirectUrl : (base_url + "#myext-allow" + second_target)
+ };
+}
+
+export default async function start() {
+ storage = await get_storage();
+
+ chrome.webRequest.onBeforeRequest.addListener(
+ redirect,
+ {
+ urls: ["<all_urls>"],
+ types: ["main_frame", "sub_frame"]
+ },
+ ["blocking"]
+ );
+}
diff --git a/background/reverse_use_info.mjs b/background/reverse_use_info.mjs
new file mode 100644
index 0000000..c38c52f
--- /dev/null
+++ b/background/reverse_use_info.mjs
@@ -0,0 +1,93 @@
+/**
+* Myext scripts and bundles usage index
+*
+* Copyright (C) 2021 Wojtek Kosior
+*
+* Dual-licensed under:
+* - 0BSD license
+* - GPLv3 or (at your option) any later version
+*/
+
+import TYPE_PREFIX from '/common/stored_types.mjs';
+import get_storage from './storage.mjs';
+import make_once from '/common/once.mjs';
+
+"use strict";
+
+/*
+ * We want to count referenes to scripts and bundles in order to know,
+ * for example, whether one can be safely deleted.
+ */
+
+var component_uses = new Map();
+var storage;
+
+function add_use_info_by_item(prefix, item, components)
+{
+ for (let component of components) {
+ component = component.join("");
+
+ let used_by_info = component_uses.get(component);
+
+ if (used_by_info === undefined) {
+ used_by_info = {};
+ component_uses.set(component, used_by_info);
+ }
+
+ if (used_by_info[prefix] === undefined)
+ used_by_info[prefix] = new Set();
+
+ used_by_info[prefix].add(item);
+ }
+}
+
+function remove_use_info_by_item(prefix, item, components)
+{
+ for (let component of components) {
+ used_by_info = component_uses.get(component.join(""));
+ if (used_by_info === undefined || used_by_info[prefix] === undefined)
+ return;
+
+ used_by_info[prefix].delete(item);
+ }
+}
+
+function build_reverse_uses_info(entries_it, type_prefix)
+{
+ for (let [item, components] of storage.get_all_it(type_prefix))
+ add_use_info_by_item(type_prefix, item, components);
+}
+
+function handle_change(change)
+{
+ if (change.old_val !== undefined)
+ remove_use_info_by_item(change.prefix, change.item, change.old_val);
+
+ if (change.new_val !== undefined)
+ add_use_info_by_item(change.prefix, change.item, change.new_val);
+}
+
+function get_uses(arg1, arg2=undefined)
+{
+ let [prefix, item] = [arg1, arg2];
+
+ if (arg2 === undefined)
+ [prefix, item] = arg1;
+
+ return component_uses.get(prefix + item);
+}
+
+async function init()
+{
+ storage = await get_storage();
+
+ prefixes = [TYPE_PREFIX.PAGE, TYPE_PREFIX.BUNDLE];
+ for (let prefix of prefixes)
+ build_reverse_uses_info(prefix);
+
+ storage.add_change_listener(handle_change, prefixes);
+
+ return get_uses;
+}
+
+export default make_once(init);
diff --git a/background/script_injector.mjs b/background/script_injector.mjs
new file mode 100644
index 0000000..3298a43
--- /dev/null
+++ b/background/script_injector.mjs
@@ -0,0 +1,122 @@
+/**
+* Myext script injector
+*
+* Copyright (C) 2021 Wojtek Kosior
+*
+* Dual-licensed under:
+* - 0BSD license
+* - GPLv3 or (at your option) any later version
+*/
+
+import {TYPE_PREFIX} from '/common/stored_types.mjs';
+import sha256 from './sha256.mjs';
+import {url_item} from './ResponseProcessor.mjs';
+import get_storage from './storage.mjs';
+
+"use strict";
+
+var storage;
+
+function ajax_callback()
+{
+ if (this.readyState == 4)
+ this.resolve_callback(this);
+}
+
+function initiate_ajax_request(resolve, method, url)
+{
+ var xhttp = new XMLHttpRequest();
+ xhttp.resolve_callback = resolve;
+ xhttp.onreadystatechange = ajax_callback;
+ xhttp.open(method, url, true);
+ xhttp.send();
+}
+
+function make_ajax_request(method, url)
+{
+ return new Promise((resolve, reject) =>
+ initiate_ajax_request(resolve, method, url));
+}
+
+async function fetch_remote_script(script_data)
+{
+ try {
+ let xhttp = await make_ajax_request("GET", script_data.url);
+ if (xhttp.status === 200) {
+ let computed_hash = sha256(xhttp.responseText);
+ if (computed_hash !== script_data.hash) {
+ console.log(`Bad hash for ${script_data.url}\n got ${computed_hash} instead of ${script_data.hash}`);
+ return;
+ }
+ return xhttp.responseText;
+ } else {
+ console.log("script not fetched: " + script_data.url);
+ return;
+ }
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+async function get_script_text(script_name)
+{
+ try {
+ let script_data = storage.get(TYPE_PREFIX.SCRIPT, script_name);
+ if (script_data === undefined) {
+ console.log(`missing data for ${script_name}`);
+ return;
+ }
+ let script_text = script_data.text;
+ if (!script_text)
+ script_text = await fetch_remote_script(script_data);
+ return script_text;
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+// TODO: parallelize script fetching
+// TODO: guard against infinite recursion
+
+async function inject_scripts_rec(components, doc)
+{
+ for (let [prefix, name] of components) {
+ if (prefix === TYPE_PREFIX.BUNDLE) {
+ var bundle = storage.get(TYPE_PREFIX.BUNDLE, name);
+
+ if (bundle === undefined) {
+ console.log(`no bundle in storage for key ${elem_key}`);
+ continue;
+ }
+ await inject_scripts_rec(bundle, doc);
+ } else {
+ let script_text = await get_script_text(name,);
+ if (script_text === undefined)
+ continue;
+
+ let script = doc.createElement("script");
+ script.textContent = script_text;
+ doc.body.appendChild(script);
+ }
+ }
+}
+
+async function inject_scripts(url, doc)
+{
+ storage = await get_storage();
+
+ url = url_item(url);
+
+ let components = storage.get(TYPE_PREFIX.PAGE, url);
+
+ if (components === undefined) {
+ console.log(`got nothing for ${url}`);
+ return
+ } else {
+ console.log(`got ${components.length} component(s) for ${url}`);
+ }
+
+ await inject_scripts_rec(components, doc);
+}
+
+export default inject_scripts;
diff --git a/background/sha256.mjs b/background/sha256.mjs
new file mode 100644
index 0000000..b8d9bda
--- /dev/null
+++ b/background/sha256.mjs
@@ -0,0 +1,524 @@
+/**
+ * [js-sha256]{@link https://github.com/emn178/js-sha256/}
+ *
+ * @version 0.9.0
+ * @author Chen, Yi-Cyuan [emn178@gmail.com]
+ * @copyright Chen, Yi-Cyuan 2014-2017
+ * @license MIT
+ */
+
+var fake_window = {};
+
+/*jslint bitwise: true */
+(function () {
+ 'use strict';
+
+ console.log('hello, crypto!');
+ var ERROR = 'input is invalid type';
+ var WINDOW = typeof window === 'object';
+ var root = /*WINDOW ? window : {}*/ fake_window;
+ if (root.JS_SHA256_NO_WINDOW) {
+ WINDOW = false;
+ }
+ var WEB_WORKER = !WINDOW && typeof self === 'object';
+ var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node;
+ if (NODE_JS) {
+ root = global;
+ } else if (WEB_WORKER) {
+ root = self;
+ }
+ var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports;
+ var AMD = typeof define === 'function' && define.amd;
+ var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined';
+ var HEX_CHARS = '0123456789abcdef'.split('');
+ var EXTRA = [-2147483648, 8388608, 32768, 128];
+ var SHIFT = [24, 16, 8, 0];
+ var K = [
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
+ ];
+ var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer'];
+
+ var blocks = [];
+
+ if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) {
+ Array.isArray = function (obj) {
+ return Object.prototype.toString.call(obj) === '[object Array]';
+ };
+ }
+
+ if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) {
+ ArrayBuffer.isView = function (obj) {
+ return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer;
+ };
+ }
+
+ var createOutputMethod = function (outputType, is224) {
+ return function (message) {
+ return new Sha256(is224, true).update(message)[outputType]();
+ };
+ };
+
+ var createMethod = function (is224) {
+ var method = createOutputMethod('hex', is224);
+ if (NODE_JS) {
+ method = nodeWrap(method, is224);
+ }
+ method.create = function () {
+ return new Sha256(is224);
+ };
+ method.update = function (message) {
+ return method.create().update(message);
+ };
+ for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
+ var type = OUTPUT_TYPES[i];
+ method[type] = createOutputMethod(type, is224);
+ }
+ return method;
+ };
+
+ var nodeWrap = function (method, is224) {
+ var crypto = eval("require('crypto')");
+ var Buffer = eval("require('buffer').Buffer");
+ var algorithm = is224 ? 'sha224' : 'sha256';
+ var nodeMethod = function (message) {
+ if (typeof message === 'string') {
+ return crypto.createHash(algorithm).update(message, 'utf8').digest('hex');
+ } else {
+ if (message === null || message === undefined) {
+ throw new Error(ERROR);
+ } else if (message.constructor === ArrayBuffer) {
+ message = new Uint8Array(message);
+ }
+ }
+ if (Array.isArray(message) || ArrayBuffer.isView(message) ||
+ message.constructor === Buffer) {
+ return crypto.createHash(algorithm).update(new Buffer(message)).digest('hex');
+ } else {
+ return method(message);
+ }
+ };
+ return nodeMethod;
+ };
+
+ var createHmacOutputMethod = function (outputType, is224) {
+ return function (key, message) {
+ return new HmacSha256(key, is224, true).update(message)[outputType]();
+ };
+ };
+
+ var createHmacMethod = function (is224) {
+ var method = createHmacOutputMethod('hex', is224);
+ method.create = function (key) {
+ return new HmacSha256(key, is224);
+ };
+ method.update = function (key, message) {
+ return method.create(key).update(message);
+ };
+ for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
+ var type = OUTPUT_TYPES[i];
+ method[type] = createHmacOutputMethod(type, is224);
+ }
+ return method;
+ };
+
+ function Sha256(is224, sharedMemory) {
+ if (sharedMemory) {
+ blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+ blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+ blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+ blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ this.blocks = blocks;
+ } else {
+ this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ }
+
+ if (is224) {
+ this.h0 = 0xc1059ed8;
+ this.h1 = 0x367cd507;
+ this.h2 = 0x3070dd17;
+ this.h3 = 0xf70e5939;
+ this.h4 = 0xffc00b31;
+ this.h5 = 0x68581511;
+ this.h6 = 0x64f98fa7;
+ this.h7 = 0xbefa4fa4;
+ } else { // 256
+ this.h0 = 0x6a09e667;
+ this.h1 = 0xbb67ae85;
+ this.h2 = 0x3c6ef372;
+ this.h3 = 0xa54ff53a;
+ this.h4 = 0x510e527f;
+ this.h5 = 0x9b05688c;
+ this.h6 = 0x1f83d9ab;
+ this.h7 = 0x5be0cd19;
+ }
+
+ this.block = this.start = this.bytes = this.hBytes = 0;
+ this.finalized = this.hashed = false;
+ this.first = true;
+ this.is224 = is224;
+ }
+
+ Sha256.prototype.update = function (message) {
+ if (this.finalized) {
+ return;
+ }
+ var notString, type = typeof message;
+ if (type !== 'string') {
+ if (type === 'object') {
+ if (message === null) {
+ throw new Error(ERROR);
+ } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) {
+ message = new Uint8Array(message);
+ } else if (!Array.isArray(message)) {
+ if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) {
+ throw new Error(ERROR);
+ }
+ }
+ } else {
+ throw new Error(ERROR);
+ }
+ notString = true;
+ }
+ var code, index = 0, i, length = message.length, blocks = this.blocks;
+
+ while (index < length) {
+ if (this.hashed) {
+ this.hashed = false;
+ blocks[0] = this.block;
+ blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+ blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+ blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+ blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ }
+
+ if (notString) {
+ for (i = this.start; index < length && i < 64; ++index) {
+ blocks[i >> 2] |= message[index] << SHIFT[i++ & 3];
+ }
+ } else {
+ for (i = this.start; index < length && i < 64; ++index) {
+ code = message.charCodeAt(index);
+ if (code < 0x80) {
+ blocks[i >> 2] |= code << SHIFT[i++ & 3];
+ } else if (code < 0x800) {
+ blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ } else if (code < 0xd800 || code >= 0xe000) {
+ blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ } else {
+ code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
+ blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ }
+ }
+ }
+
+ this.lastByteIndex = i;
+ this.bytes += i - this.start;
+ if (i >= 64) {
+ this.block = blocks[16];
+ this.start = i - 64;
+ this.hash();
+ this.hashed = true;
+ } else {
+ this.start = i;
+ }
+ }
+ if (this.bytes > 4294967295) {
+ this.hBytes += this.bytes / 4294967296 << 0;
+ this.bytes = this.bytes % 4294967296;
+ }
+ return this;
+ };
+
+ Sha256.prototype.finalize = function () {
+ if (this.finalized) {
+ return;
+ }
+ this.finalized = true;
+ var blocks = this.blocks, i = this.lastByteIndex;
+ blocks[16] = this.block;
+ blocks[i >> 2] |= EXTRA[i & 3];
+ this.block = blocks[16];
+ if (i >= 56) {
+ if (!this.hashed) {
+ this.hash();
+ }
+ blocks[0] = this.block;
+ blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+ blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+ blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+ blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ }
+ blocks[14] = this.hBytes << 3 | this.bytes >>> 29;
+ blocks[15] = this.bytes << 3;
+ this.hash();
+ };
+
+ Sha256.prototype.hash = function () {
+ var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6,
+ h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc;
+
+ for (j = 16; j < 64; ++j) {
+ // rightrotate
+ t1 = blocks[j - 15];
+ s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3);
+ t1 = blocks[j - 2];
+ s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10);
+ blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0;
+ }
+
+ bc = b & c;
+ for (j = 0; j < 64; j += 4) {
+ if (this.first) {
+ if (this.is224) {
+ ab = 300032;
+ t1 = blocks[0] - 1413257819;
+ h = t1 - 150054599 << 0;
+ d = t1 + 24177077 << 0;
+ } else {
+ ab = 704751109;
+ t1 = blocks[0] - 210244248;
+ h = t1 - 1521486534 << 0;
+ d = t1 + 143694565 << 0;
+ }
+ this.first = false;
+ } else {
+ s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
+ s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
+ ab = a & b;
+ maj = ab ^ (a & c) ^ bc;
+ ch = (e & f) ^ (~e & g);
+ t1 = h + s1 + ch + K[j] + blocks[j];
+ t2 = s0 + maj;
+ h = d + t1 << 0;
+ d = t1 + t2 << 0;
+ }
+ s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10));
+ s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7));
+ da = d & a;
+ maj = da ^ (d & b) ^ ab;
+ ch = (h & e) ^ (~h & f);
+ t1 = g + s1 + ch + K[j + 1] + blocks[j + 1];
+ t2 = s0 + maj;
+ g = c + t1 << 0;
+ c = t1 + t2 << 0;
+ s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10));
+ s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7));
+ cd = c & d;
+ maj = cd ^ (c & a) ^ da;
+ ch = (g & h) ^ (~g & e);
+ t1 = f + s1 + ch + K[j + 2] + blocks[j + 2];
+ t2 = s0 + maj;
+ f = b + t1 << 0;
+ b = t1 + t2 << 0;
+ s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10));
+ s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7));
+ bc = b & c;
+ maj = bc ^ (b & d) ^ cd;
+ ch = (f & g) ^ (~f & h);
+ t1 = e + s1 + ch + K[j + 3] + blocks[j + 3];
+ t2 = s0 + maj;
+ e = a + t1 << 0;
+ a = t1 + t2 << 0;
+ }
+
+ this.h0 = this.h0 + a << 0;
+ this.h1 = this.h1 + b << 0;
+ this.h2 = this.h2 + c << 0;
+ this.h3 = this.h3 + d << 0;
+ this.h4 = this.h4 + e << 0;
+ this.h5 = this.h5 + f << 0;
+ this.h6 = this.h6 + g << 0;
+ this.h7 = this.h7 + h << 0;
+ };
+
+ Sha256.prototype.hex = function () {
+ this.finalize();
+
+ var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5,
+ h6 = this.h6, h7 = this.h7;
+
+ var hex = HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] +
+ HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] +
+ HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] +
+ HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] +
+ HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] +
+ HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] +
+ HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] +
+ HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] +
+ HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] +
+ HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] +
+ HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] +
+ HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] +
+ HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] +
+ HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] +
+ HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] +
+ HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] +
+ HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] +
+ HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] +
+ HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] +
+ HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] +
+ HEX_CHARS[(h5 >> 28) & 0x0F] + HEX_CHARS[(h5 >> 24) & 0x0F] +
+ HEX_CHARS[(h5 >> 20) & 0x0F] + HEX_CHARS[(h5 >> 16) & 0x0F] +
+ HEX_CHARS[(h5 >> 12) & 0x0F] + HEX_CHARS[(h5 >> 8) & 0x0F] +
+ HEX_CHARS[(h5 >> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] +
+ HEX_CHARS[(h6 >> 28) & 0x0F] + HEX_CHARS[(h6 >> 24) & 0x0F] +
+ HEX_CHARS[(h6 >> 20) & 0x0F] + HEX_CHARS[(h6 >> 16) & 0x0F] +
+ HEX_CHARS[(h6 >> 12) & 0x0F] + HEX_CHARS[(h6 >> 8) & 0x0F] +
+ HEX_CHARS[(h6 >> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F];
+ if (!this.is224) {
+ hex += HEX_CHARS[(h7 >> 28) & 0x0F] + HEX_CHARS[(h7 >> 24) & 0x0F] +
+ HEX_CHARS[(h7 >> 20) & 0x0F] + HEX_CHARS[(h7 >> 16) & 0x0F] +
+ HEX_CHARS[(h7 >> 12) & 0x0F] + HEX_CHARS[(h7 >> 8) & 0x0F] +
+ HEX_CHARS[(h7 >> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F];
+ }
+ return hex;
+ };
+
+ Sha256.prototype.toString = Sha256.prototype.hex;
+
+ Sha256.prototype.digest = function () {
+ this.finalize();
+
+ var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5,
+ h6 = this.h6, h7 = this.h7;
+
+ var arr = [
+ (h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF,
+ (h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF,
+ (h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF,
+ (h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF,
+ (h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF,
+ (h5 >> 24) & 0xFF, (h5 >> 16) & 0xFF, (h5 >> 8) & 0xFF, h5 & 0xFF,
+ (h6 >> 24) & 0xFF, (h6 >> 16) & 0xFF, (h6 >> 8) & 0xFF, h6 & 0xFF
+ ];
+ if (!this.is224) {
+ arr.push((h7 >> 24) & 0xFF, (h7 >> 16) & 0xFF, (h7 >> 8) & 0xFF, h7 & 0xFF);
+ }
+ return arr;
+ };
+
+ Sha256.prototype.array = Sha256.prototype.digest;
+
+ Sha256.prototype.arrayBuffer = function () {
+ this.finalize();
+
+ var buffer = new ArrayBuffer(this.is224 ? 28 : 32);
+ var dataView = new DataView(buffer);
+ dataView.setUint32(0, this.h0);
+ dataView.setUint32(4, this.h1);
+ dataView.setUint32(8, this.h2);
+ dataView.setUint32(12, this.h3);
+ dataView.setUint32(16, this.h4);
+ dataView.setUint32(20, this.h5);
+ dataView.setUint32(24, this.h6);
+ if (!this.is224) {
+ dataView.setUint32(28, this.h7);
+ }
+ return buffer;
+ };
+
+ function HmacSha256(key, is224, sharedMemory) {
+ var i, type = typeof key;
+ if (type === 'string') {
+ var bytes = [], length = key.length, index = 0, code;
+ for (i = 0; i < length; ++i) {
+ code = key.charCodeAt(i);
+ if (code < 0x80) {
+ bytes[index++] = code;
+ } else if (code < 0x800) {
+ bytes[index++] = (0xc0 | (code >> 6));
+ bytes[index++] = (0x80 | (code & 0x3f));
+ } else if (code < 0xd800 || code >= 0xe000) {
+ bytes[index++] = (0xe0 | (code >> 12));
+ bytes[index++] = (0x80 | ((code >> 6) & 0x3f));
+ bytes[index++] = (0x80 | (code & 0x3f));
+ } else {
+ code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff));
+ bytes[index++] = (0xf0 | (code >> 18));
+ bytes[index++] = (0x80 | ((code >> 12) & 0x3f));
+ bytes[index++] = (0x80 | ((code >> 6) & 0x3f));
+ bytes[index++] = (0x80 | (code & 0x3f));
+ }
+ }
+ key = bytes;
+ } else {
+ if (type === 'object') {
+ if (key === null) {
+ throw new Error(ERROR);
+ } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) {
+ key = new Uint8Array(key);
+ } else if (!Array.isArray(key)) {
+ if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) {
+ throw new Error(ERROR);
+ }
+ }
+ } else {
+ throw new Error(ERROR);
+ }
+ }
+
+ if (key.length > 64) {
+ key = (new Sha256(is224, true)).update(key).array();
+ }
+
+ var oKeyPad = [], iKeyPad = [];
+ for (i = 0; i < 64; ++i) {
+ var b = key[i] || 0;
+ oKeyPad[i] = 0x5c ^ b;
+ iKeyPad[i] = 0x36 ^ b;
+ }
+
+ Sha256.call(this, is224, sharedMemory);
+
+ this.update(iKeyPad);
+ this.oKeyPad = oKeyPad;
+ this.inner = true;
+ this.sharedMemory = sharedMemory;
+ }
+ HmacSha256.prototype = new Sha256();
+
+ HmacSha256.prototype.finalize = function () {
+ Sha256.prototype.finalize.call(this);
+ if (this.inner) {
+ this.inner = false;
+ var innerHash = this.array();
+ Sha256.call(this, this.is224, this.sharedMemory);
+ this.update(this.oKeyPad);
+ this.update(innerHash);
+ Sha256.prototype.finalize.call(this);
+ }
+ };
+
+ var exports = createMethod();
+ exports.sha256 = exports;
+ exports.sha224 = createMethod(true);
+ exports.sha256.hmac = createHmacMethod();
+ exports.sha224.hmac = createHmacMethod(true);
+
+ if (COMMON_JS) {
+ module.exports = exports;
+ } else {
+ root.sha256 = exports.sha256;
+ root.sha224 = exports.sha224;
+ if (AMD) {
+ define(function () {
+ return exports;
+ });
+ }
+ }
+})();
+
+export default fake_window.sha256;
diff --git a/background/storage.mjs b/background/storage.mjs
new file mode 100644
index 0000000..00b1ace
--- /dev/null
+++ b/background/storage.mjs
@@ -0,0 +1,380 @@
+/**
+* Myext storage manager
+*
+* Copyright (C) 2021 Wojtek Kosior
+*
+* Dual-licensed under:
+* - 0BSD license
+* - GPLv3 or (at your option) any later version
+*/
+
+import {TYPE_PREFIX, TYPE_NAME, list_prefixes} from '/common/stored_types.mjs';
+import {make_lock, lock, unlock} from '/common/lock.mjs';
+import make_once from '/common/once.mjs';
+import browser from '/common/browser.mjs';
+
+"use strict";
+
+var exports = {};
+
+/* We're yet to decide how to handle errors... */
+
+/* Here are some basic wrappers for storage API functions */
+
+async function get(key)
+{
+ try {
+ /* Fix for fact that Chrome does not use promises here */
+ let promise = window.browser === undefined ?
+ new Promise((resolve, reject) =>
+ chrome.storage.local.get(key, val => resolve(val))) :
+ browser.storage.local.get(key);
+
+ return (await promise)[key];
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+async function set(key, value)
+{
+ try {
+ return browser.storage.local.set({[key]: value});
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+async function setn(keys_and_values)
+{
+ let obj = Object();
+ while (keys_and_values.length > 1) {
+ let value = keys_and_values.pop();
+ let key = keys_and_values.pop();
+ obj[key] = value;
+ }
+
+ try {
+ return browser.storage.local.set(obj);
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+async function set_var(name, value)
+{
+ return set(TYPE_PREFIX.VAR + name, value);
+}
+
+async function get_var(name)
+{
+ return get(TYPE_PREFIX.VAR + name);
+}
+
+/* A special case of a persisted variable is one that contains list of items. */
+
+async function get_list_var(name)
+{
+ let list = await get_var(name);
+
+ return list === undefined ? [] : list;
+}
+
+/* We maintain in-memory copies of some stored lists. */
+
+async function list(prefix)
+{
+ let name = TYPE_NAME[prefix] + "s"; /* Make plural. */
+ let map = new Map();
+
+ for (let item of await get_list_var(name))
+ map.set(item, await get(prefix + item));
+
+ return {map, prefix, name, listeners : new Set(), lock : make_lock()};
+}
+
+var pages;
+var bundles;
+var scripts;
+
+var list_by_prefix = {};
+
+async function init()
+{
+ for (let prefix of list_prefixes)
+ list_by_prefix[prefix] = await list(prefix);
+
+ return exports;
+}
+
+/*
+ * Facilitate listening to changes
+ */
+
+exports.add_change_listener = function (cb, prefixes=list_prefixes)
+{
+ if (typeof(prefixes) === "string")
+ prefixes = [prefixes];
+
+ for (let prefix of prefixes)
+ list_by_prefix[prefix].listeners.add(cb);
+}
+
+exports.remove_change_listener = function (cb, prefixes=list_prefixes)
+{
+ if (typeof(prefixes) === "string")
+ prefixes = [prefixes];
+
+ for (let prefix of prefixes)
+ list_by_prefix[prefix].listeners.delete(cb);
+}
+
+function broadcast_change(change, list)
+{
+ for (let listener_callback of list.listeners)
+ listener_callback(change);
+}
+
+/* Prepare some hepler functions to get elements of a list */
+
+function list_items_it(list, with_values=false)
+{
+ return with_values ? list.map.entries() : list.map.keys();
+}
+
+function list_entries_it(list)
+{
+ return list_items_it(list, true);
+}
+
+function list_items(list, with_values=false)
+{
+ let array = [];
+
+ for (let item of list_items_it(list, with_values))
+ array.push(item);
+
+ return array;
+}
+
+function list_entries(list)
+{
+ return list_items(list, true);
+}
+
+/*
+ * Below we make additional effort to update map of given kind of items
+ * every time an item is added/removed to keep everything coherent.
+ */
+async function set_item(item, value, list)
+{
+ await lock(list.lock);
+ let result = await _set_item(...arguments);
+ unlock(list.lock)
+ return result;
+}
+async function _set_item(item, value, list)
+{
+ let key = list.prefix + item;
+ let old_val = list.map.get(item);
+ if (old_val === undefined) {
+ let items = list_items(list);
+ items.push(item);
+ await setn([key, value, "_" + list.name, items]);
+ } else {
+ await set(key, value);
+ }
+
+ list.map.set(item, value)
+
+ let change = {
+ prefix : list.prefix,
+ item,
+ old_val,
+ new_val : value
+ };
+
+ broadcast_change(change, list);
+
+ return old_val;
+}
+
+// TODO: The actual idea to set value to undefined is good - this way we can
+// also set a new list of items in the same API call. But such key
+// is still stored in the storage. We need to somehow remove it later.
+// For that, we're going to have to store 1 more list of each kind.
+async function remove_item(item, list)
+{
+ await lock(list.lock);
+ let result = await _remove_item(...arguments);
+ unlock(list.lock)
+ return result;
+}
+async function _remove_item(item, list)
+{
+ let old_val = list.map.get(item);
+ if (old_val === undefined)
+ return;
+
+ let key = list.prefix + item;
+ let items = list_items(list);
+ let index = items.indexOf(item);
+ items.splice(index, 1);
+
+ await setn([key, undefined, "_" + list.name, items]);
+
+ list.map.delete(item);
+
+ let change = {
+ prefix : list.prefix,
+ item,
+ old_val,
+ new_val : undefined
+ };
+
+ broadcast_change(change, list);
+
+ return old_val;
+}
+
+// TODO: same as above applies here
+async function replace_item(old_item, new_item, list, new_val=undefined)
+{
+ await lock(list.lock);
+ let result = await _replace_item(...arguments);
+ unlock(list.lock)
+ return result;
+}
+async function _replace_item(old_item, new_item, list, new_val=undefined)
+{
+ let old_val = list.map.get(old_item);
+ if (new_val === undefined) {
+ if (old_val === undefined)
+ return;
+ new_val = old_val
+ } else if (new_val === old_val && new_item === old_item) {
+ return old_val;
+ }
+
+ if (old_item === new_item || old_val === undefined) {
+ await _set_item(new_item, new_val, list);
+ return old_val;
+ }
+
+ let new_key = list.prefix + new_item;
+ let old_key = list.prefix + old_item;
+ let items = list_items(list);
+ let index = items.indexOf(old_item);
+ items[index] = new_item;
+ await setn([old_key, undefined, new_key, new_val, "_" + list.name, items]);
+
+ list.map.delete(old_item);
+
+ let change = {
+ prefix : list.prefix,
+ item : old_item,
+ old_val,
+ new_val : undefined
+ };
+
+ broadcast_change(change, list);
+
+ list.map.set(new_item, new_val);
+
+ change.item = new_item;
+ change.old_val = undefined;
+ change.new_val = new_val;
+
+ broadcast_change(change, list);
+
+ return old_val;
+}
+
+/*
+ * For scripts, item name is chosen by user, data should be an object containing
+ * - script's url and hash or
+ * - script's text or
+ * - all three
+ */
+
+/*
+ * For bundles, item name is chosen by user, data is an array of 2-element
+ * arrays with type prefix and script/bundle names.
+ */
+
+/* For pages data argument is same as for bundles. Item name is url. */
+
+exports.set = async function (prefix, item, data)
+{
+ return set_item(item, data, list_by_prefix[prefix]);
+}
+
+exports.get = function (prefix, item)
+{
+ return list_by_prefix[prefix].map.get(item);
+}
+
+exports.remove = async function (prefix, item)
+{
+ return remove_item(item, list_by_prefix[prefix]);
+}
+
+exports.replace = async function (prefix, old_item, new_item,
+ new_data=undefined)
+{
+ return replace_item(old_item, new_item, list_by_prefix[prefix], new_data);
+}
+
+exports.get_all_names = function (prefix)
+{
+ return list_items(list_by_prefix[prefix]);
+}
+
+exports.get_all_names_it = function (prefix)
+{
+ return list_items_it(list_by_prefix[prefix]);
+}
+
+exports.get_all = function (prefix)
+{
+ return list_entries(list_by_prefix[prefix]);
+}
+
+exports.get_all_it = function (prefix)
+{
+ return list_entries_it(list_by_prefix[prefix]);
+}
+
+/* Finally, a quick way to wipe all the data. */
+// TODO: maybe delete items in such order that none of them ever references
+// an already-deleted one?
+exports.clear = async function ()
+{
+ let lists = list_prefixes.map((p) => list_by_prefix[p]);
+
+ for (let list of lists)
+ await lock(list.lock);
+
+ for (let list of lists) {
+
+ let change = {
+ prefix : list.prefix,
+ new_val : undefined
+ };
+
+ for (let [item, val] of list_entries_it(list)) {
+ change.item = item;
+ change.old_val = val;
+ broadcast_change(change, list);
+ }
+
+ list.map = new Map();
+ }
+
+ await browser.storage.local.clear();
+
+ for (let list of lists)
+ unlock(list.lock);
+}
+
+export default make_once(init);
diff --git a/background/storage_server.mjs b/background/storage_server.mjs
new file mode 100644
index 0000000..3a141ff
--- /dev/null
+++ b/background/storage_server.mjs
@@ -0,0 +1,58 @@
+/**
+* Myext storage through connection (server side)
+*
+* Copyright (C) 2021 Wojtek Kosior
+*
+* Dual-licensed under:
+* - 0BSD license
+* - GPLv3 or (at your option) any later version
+*/
+
+import listen_for_connection from './message_server.mjs';
+import get_storage from './storage.mjs';
+import {TYPE_PREFIX} from '/common/stored_types.mjs';
+import CONNECTION_TYPE from '/common/connection_types.mjs';
+
+var storage;
+
+async function handle_remote_call(port, message)
+{
+ let [call_id, func, args] = message;
+
+ try {
+ let result = await Promise.resolve(storage[func](...args));
+ port.postMessage({call_id, result});
+ } catch (error) {
+ error = error + '';
+ port.postMessage({call_id, error});
+ }
+}
+
+function remove_storage_listener(cb) {
+ storage.remove_change_listener(cb);
+}
+
+function new_connection(port)
+{
+ console.log("new remote storage connection!");
+
+ port.postMessage({
+ [TYPE_PREFIX.SCRIPT] : storage.get_all(TYPE_PREFIX.SCRIPT),
+ [TYPE_PREFIX.BUNDLE] : storage.get_all(TYPE_PREFIX.BUNDLE),
+ [TYPE_PREFIX.PAGE] : storage.get_all(TYPE_PREFIX.PAGE)
+ });
+
+ let handle_change = change => port.postMessage(change);
+
+ storage.add_change_listener(handle_change);
+
+ port.onMessage.addListener(m => handle_remote_call(port, m));
+ port.onDisconnect.addListener(() => remove_storage_listener(handle_change));
+}
+
+export default async function start()
+{
+ storage = await get_storage();
+
+ listen_for_connection(CONNECTION_TYPE.REMOTE_STORAGE, new_connection);
+}
diff --git a/background/url_item.mjs b/background/url_item.mjs
new file mode 100644
index 0000000..2ee1d6d
--- /dev/null
+++ b/background/url_item.mjs
@@ -0,0 +1,18 @@
+/**
+* Myext stripping url from query and target
+*
+* Copyright (C) 2021 Wojtek Kosior
+*
+* Dual-licensed under:
+* - 0BSD license
+* - GPLv3 or (at your option) any later version
+*/
+
+"use strict";
+
+export default function url_item(url)
+{
+ let url_re = /^([^?#]*).*$/;
+ let match = url_re.exec(url);
+ return match[1];
+}