diff options
Diffstat (limited to 'background')
-rw-r--r-- | background/ResponseHandler.mjs | 257 | ||||
-rw-r--r-- | background/ResponseMetaData.mjs | 107 | ||||
-rw-r--r-- | background/ResponseProcessor.mjs | 145 | ||||
-rw-r--r-- | background/background.html | 7 | ||||
-rw-r--r-- | background/main.mjs | 168 | ||||
-rw-r--r-- | background/message_server.mjs | 31 | ||||
-rw-r--r-- | background/page_actions_server.mjs | 145 | ||||
-rw-r--r-- | background/policy_smuggler.mjs | 60 | ||||
-rw-r--r-- | background/reverse_use_info.mjs | 93 | ||||
-rw-r--r-- | background/script_injector.mjs | 122 | ||||
-rw-r--r-- | background/sha256.mjs | 524 | ||||
-rw-r--r-- | background/storage.mjs | 380 | ||||
-rw-r--r-- | background/storage_server.mjs | 58 | ||||
-rw-r--r-- | background/url_item.mjs | 18 |
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]; +} |