summaryrefslogtreecommitdiff
path: root/src/bg
diff options
context:
space:
mode:
Diffstat (limited to 'src/bg')
-rw-r--r--src/bg/RequestGuard.js549
-rw-r--r--src/bg/RequestUtil.js130
-rw-r--r--src/bg/Settings.js125
-rw-r--r--src/bg/defaults.js37
-rw-r--r--src/bg/main.js282
5 files changed, 1123 insertions, 0 deletions
diff --git a/src/bg/RequestGuard.js b/src/bg/RequestGuard.js
new file mode 100644
index 0000000..0f1558c
--- /dev/null
+++ b/src/bg/RequestGuard.js
@@ -0,0 +1,549 @@
+var RequestGuard = (() => {
+ 'use strict';
+ const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
+ browser.browserAction.setTitle({title: VERSION_LABEL});
+ const REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
+ const REPORT_GROUP = "NoScript-Endpoint";
+ const REPORT_TO = {
+ name: "Report-To",
+ value: JSON.stringify({ "url": REPORT_URI,
+ "group": REPORT_GROUP,
+ "max-age": 10886400 }),
+ };
+ const CSP = {
+ name: "content-security-policy",
+ start: `report-uri ${REPORT_URI};`,
+ end: `;report-to ${REPORT_URI};`,
+ isMine(header) {
+ let {name, value} = header;
+ if (name.toLowerCase() !== CSP.name) return false;
+ let startIdx = value.indexOf(this.start);
+ return startIdx > -1 && startIdx < value.lastIndexOf(this.end);
+ },
+ inject(headerValue, mine) {
+ let startIdx = headerValue.indexOf(this.start);
+ if (startIdx < 0) return `${headerValue};${mine}`;
+ let endIdx = headerValue.lastIndexOf(this.end);
+ let retValue = `${headerValue.substring(0, startIdx)}${mine}`;
+
+ return endIdx < 0 ? retValue : `${retValue}${headerValue.substring(endIdx + this.end.length + 1)}`;
+ },
+ create(...directives) {
+ return `${this.start}${directives.join(';')}${this.end}`;
+ },
+ createBlocker(...types) {
+ return this.create(...(types.map(type => `${type.name || type}-src ${type.value || "'none'"}`)));
+ },
+ blocks(header, type) {
+ return header.includes(`;${type}-src 'none';`)
+ },
+ types: ["script", "object", "media"],
+ };
+
+ const policyTypesMap = {
+ main_frame: "",
+ sub_frame: "frame",
+ script: "script",
+ xslt: "script",
+ xbl: "script",
+ font: "font",
+ object: "object",
+ object_subrequest: "fetch",
+ xmlhttprequest: "fetch",
+ ping: "ping",
+ beacon: "ping",
+ media: "media",
+ other: "",
+ };
+ const allTypes = Object.keys(policyTypesMap);
+ Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types
+
+ const FORBID_DATAURI_TYPES = ["font", "media", "object"];
+
+ const TabStatus = {
+ map: new Map(),
+ types: ["script", "object", "media", "frame", "font"],
+ newRecords() {
+ return {
+ allowed: {},
+ blocked: {},
+ noscriptFrames: {},
+ }
+ },
+
+ initTab(tabId, records = this.newRecords()) {
+ this.map.set(tabId, records);
+ return records;
+ },
+
+ _record(request, what, optValue) {
+ let {tabId, frameId, type, url, documentUrl} = request;
+ let policyType = policyTypesMap[type] || type;
+ let requestKey = Policy.requestKey(url, documentUrl, policyType);
+ let map = this.map;
+ let records;
+ if (map.has(tabId)) {
+ records = map.get(tabId);
+ } else {
+ records = this.initTab(tabId);
+ }
+
+ if (what === "noscriptFrame") {
+ let nsf = records.noscriptFrames;
+ if (frameId in nsf) {
+ return null;
+ }
+ nsf[frameId] = optValue;
+ what = optValue ? "blocked" : "allowed";
+ if (frameId === 0) {
+ request.type = type = "main_frame";
+ Content.reportTo(request, optValue, type);
+ }
+ }
+ let collection = records[what];
+ if (type in collection) {
+ if (!collection[type].includes(requestKey)) {
+ collection[type].push(requestKey);
+ }
+ } else {
+ collection[type] = [requestKey];
+ }
+ return records;
+ },
+
+ record(request, what, optValue) {
+ let records = this._record(request, what, optValue);
+ if (records) {
+ this.updateTab(request.tabId);
+ }
+ },
+
+ _pendingTabs: new Set(),
+
+ updateTab(tabId) {
+ if (this._pendingTabs.size === 0) {
+ window.setTimeout(() => { // clamp UI updates
+ for (let tabId of this._pendingTabs) {
+ this._updateTabNow(tabId);
+ }
+ this._pendingTabs.clear();
+ }, 200);
+ }
+ this._pendingTabs.add(tabId);
+ },
+ _updateTabNow(tabId) {
+ this._pendingTabs.delete(tabId);
+ let records = this.map.get(tabId) || this.initTab(tabId);
+ let {allowed, blocked, noscriptFrames} = records;
+ let topAllowed = !(noscriptFrames && noscriptFrames[0]);
+
+ let numAllowed = 0, numBlocked = 0, sum = 0;
+ let report = this.types.map(t => {
+ let a = allowed[t] && allowed[t].length || 0, b = blocked[t] && blocked[t].length || 0, s = a + b;
+ numAllowed+= a, numBlocked += b, sum += s;
+ return s && `<${t === "sub_frame" ? "frame" : t}>: ${b}/${s}`;
+ }).filter(s => s).join("\n");
+
+ let enforced = ns.isEnforced(tabId);
+
+ let icon = topAllowed ?
+ (numBlocked ? "part"
+ : enforced ? "yes" : "global")
+ : (numAllowed ? "sub" : "no");
+ let showBadge = ns.local.showCountBadge && numBlocked > 0;
+
+ let browserAction = browser.browserAction;
+ browserAction.setIcon({tabId, path: {64: `/img/ui-${icon}64.png`}});
+ browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""});
+ browserAction.setBadgeBackgroundColor({tabId, color: [255, 0, 0, 128]});
+ browserAction.setTitle({tabId,
+ title: `${VERSION_LABEL} \n${enforced ?
+ _("BlockedItems", [numBlocked, numAllowed + numBlocked]) + ` \n${report}`
+ : _("NotEnforced")}`
+ });
+ },
+
+ totalize(sum, value) {
+ return sum + value;
+ },
+
+ async probe(tabId) {
+ if (tabId === undefined) {
+ (await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id));
+ } else {
+ try {
+ TabStatus.recordAll(tabId, await ns.collectSeen(tabId));
+ } catch (e) {
+ error(e);
+ }
+ }
+ },
+
+ recordAll(tabId, seen) {
+ if (seen) {
+ let records = TabStatus.map.get(tabId);
+ if (records) {
+ records.allowed = {};
+ records.blocked = {};
+ }
+ for (let thing of seen) {
+ thing.request.tabId = tabId;
+ TabStatus._record(thing.request, thing.allowed ? "allowed" : "blocked");
+ }
+ this._updateTabNow(tabId);
+ }
+ },
+
+ async onActivatedTab(info) {
+ let {tabId} = info;
+ let seen = await ns.collectSeen(tabId);
+
+ TabStatus.recordAll(tabId, seen);
+ },
+ onRemovedTab(tabId) {
+ TabStatus.map.delete(tabId);
+ },
+ }
+ browser.tabs.onActivated.addListener(TabStatus.onActivatedTab);
+ browser.tabs.onRemoved.addListener(TabStatus.onRemovedTab);
+
+ if (!("setIcon" in browser.browserAction)) { // unsupported on Android
+ TabStatus._updateTabNow = TabStatus.updateTab = () => {};
+ }
+
+ const Content = {
+
+
+ async hearFrom(message, sender) {
+ debug("Received message from content", message, sender);
+ switch (message.type) {
+ case "pageshow":
+ TabStatus.recordAll(sender.tab.id, message.seen);
+ return true;
+ case "enable":
+ let {url, documentUrl, policyType} = message;
+ let TAG = `<${policyType.toUpperCase()}>`;
+ let origin = Sites.origin(url);
+ let {siteKey} = Sites.parse(url);
+ let options;
+ if (siteKey === origin) {
+ TAG += `@${siteKey}`;
+ } else {
+ options = [
+ {label: _("allowLocal", siteKey), checked: true},
+ {label: _("allowLocal", origin)}
+ ];
+ }
+ // let parsedDoc = Sites.parse(documentUrl);
+ let t = u => `${TAG}@${u}`;
+ let ret = await Prompts.prompt({
+ title: _("BlockedObjects"),
+ message: _("allowLocal", TAG),
+ options});
+ debug(`Prompt returned %o`);
+ if (ret.button !== 0) return;
+ let key = [siteKey, origin][ret.option || 0];
+ if (!key) return;
+ let {siteMatch, contextMatch, perms} = ns.policy.get(key, documentUrl);
+ let {capabilities} = perms;
+ if (!capabilities.has(policyType)) {
+ perms = new Permissions(new Set(capabilities), false);
+ perms.capabilities.add(policyType);
+
+ /* TODO: handle contextual permissions
+ if (documentUrl) {
+ let context = new URL(documentUrl).origin;
+ let contextualSites = new Sites([context, perms]);
+ perms = new Permissions(new Set(capabilities), false, contextualSites);
+ }
+ */
+ ns.policy.set(key, perms);
+ ns.savePolicy();
+ }
+ return true;
+ case "canScript":
+ let records = TabStatus.map.get(sender.tab.id);
+ debug("Records.noscriptFrames %o, canScript: %s", records && records.noscriptFrames, !(records && records.noscriptFrames[sender.frameId]));
+ return !(records && records.noscriptFrames[sender.frameId]);
+ }
+ },
+
+ async reportTo(request, allowed, policyType) {
+ let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
+ let pending = pendingRequests.get(requestId); // null if from a CSP report
+ let initialUrl = pending ? pending.initialUrl : request.url;
+ request = {
+ key: Policy.requestKey(url, type, documentUrl || "", /^(media|object|frame)$/.test(type)),
+ type, url, documentUrl, originUrl
+ };
+ if (tabId < 0) return;
+ if (pending) request.initialUrl = pending.initialUrl;
+ try {
+ browser.tabs.sendMessage(
+ tabId,
+ {type: "seen", request, allowed, policyType, ownFrame: true},
+ {frameId}
+ );
+ } catch (e) {
+ debug(`Couldn't deliver "seen" message for ${type}@${url} ${allowed ? "A" : "F" } to document ${documentUrl} (${frameId}/${tabId}`, e);
+ }
+ if (frameId === 0) return;
+ try {
+ browser.tabs.sendMessage(
+ tabId,
+ {type: "seen", request, allowed, policyType},
+ {frameId: 0}
+ );
+ } catch (e) {
+ debug(`Couldn't deliver "seen" message to top frame containing ${documentUrl} (${frameId}/${tabId}`, e);
+ }
+ }
+ };
+ browser.runtime.onMessage.addListener(Content.hearFrom);
+
+ const pendingRequests = new Map();
+ function initPendingRequest(request) {
+ let {requestId, url} = request;
+ let redirected = pendingRequests.get(requestId);
+ let initialUrl = redirected ? redirected.initialUrl : url;
+ pendingRequests.set(requestId, {
+ url, redirected,
+ onCompleted: new Set(),
+ });
+ return redirected;
+ }
+
+
+ const ABORT = {cancel: true}, ALLOW = {};
+ const listeners = {
+ onBeforeRequest(request) {
+ try {
+ let redirected = initPendingRequest(request);
+ let {policy} = ns;
+ let policyType = policyTypesMap[request.type];
+ if (policyType) {
+ let {url, originUrl, documentUrl} = request;
+ if (("fetch" === policyType || "frame" === policyType) &&
+ (url === originUrl && originUrl === documentUrl ||
+ /^(?:chrome|resource|moz-extension|about):/.test(originUrl))
+ ) {
+ // livemark request or similar browser-internal, always allow;
+ return ALLOW;
+ }
+
+ if (/^(?:data|blob):/.test(url)) {
+ request._dataUrl = url;
+ request.url = url = documentUrl;
+ }
+ let allowed = !ns.isEnforced(request.tabId) ||
+ policy.can(url, policyType, originUrl);
+ Content.reportTo(request, allowed, policyType);
+
+ if (!allowed) {
+ debug(`Blocking ${policyType}`, request);
+ TabStatus.record(request, "blocked");
+ return ABORT;
+ }
+ }
+ } catch (e) {
+ error(e);
+ }
+
+ return ALLOW;
+ },
+
+ async onHeadersReceived(request) {
+ // called for main_frame, sub_frame and object
+ debug("onHeadersReceived", request);
+
+ try {
+ let header, blocker;
+ let responseHeaders = request.responseHeaders;
+ let content = {}
+ for (let h of responseHeaders) {
+ if (CSP.isMine(h)) {
+ header = h;
+ h.value = CSP.inject(h.value, "");
+ } else if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) {
+ content[h.name.split("-")[1].trim().toLowerCase()] = h.value;
+ }
+ }
+
+
+ if (ns.isEnforced(request.tabId)) {
+ let policy = ns.policy;
+ let perms = policy.get(request.url, request.documentUrl).perms;
+ if (policy.autoAllowTop && request.frameId === 0 && perms === policy.DEFAULT) {
+ policy.set(Sites.optimalKey(request.url), perms = policy.TRUSTED.tempTwin);
+ }
+
+ let {capabilities} = perms;
+ let canScript = capabilities.has("script");
+
+ let blockedTypes;
+ let forbidData = FORBID_DATAURI_TYPES.filter(t => !capabilities.has(t));
+ if (!content.disposition &&
+ (!content.type || /^\s*(?:video|audio|application)\//.test(content.type))) {
+ debug(`Suspicious content type "%s" in request %o with capabilities %o`,
+ content.type, request, capabilities);
+ blockedTypes = CSP.types.filter(t => !capabilities.has(t));
+ } else if(!canScript) {
+ blockedTypes = ["script"];
+ forbidData.push("object"); // data: URIs loaded in objects may run scripts
+ }
+
+ for (let type of forbidData) { // object, font, media
+ // HTTP is blocked in onBeforeRequest, let's allow it only and block
+ // for instance data: and blob: URIs
+ let dataBlocker = {name: type, value: "http: https:"};
+ if (blockedTypes) blockedTypes.push(dataBlocker)
+ else blockedTypes = [dataBlocker];
+ }
+
+ debug("Blocked types", blockedTypes);
+ if (blockedTypes && blockedTypes.length) {
+ blocker = CSP.createBlocker(...blockedTypes);
+ }
+
+ if (canScript) {
+ if (!capabilities.has("webgl")) {
+ await RequestUtil.executeOnStart(request, {
+ file: "/content/webglHook.js"
+ });
+ }
+ if (!capabilities.has("media")) {
+ await RequestUtil.executeOnStart(request, {
+ code: "window.mediaBlocker = true;"
+ });
+ }
+ await RequestUtil.executeOnStart(request, {
+ file: "content/media.js"
+ });
+ }
+ }
+
+ debug(`CSP blocker:`, blocker);
+ if (blocker) {
+ if (header) {
+ header.value = CSP.inject(header.value, blocker);
+ } else {
+ header = {name: CSP.name, value: blocker};
+ responseHeaders.push(header);
+ }
+ }
+
+ if (header) return {responseHeaders};
+ } catch (e) {
+ error(e, "Error in onHeadersReceived", uneval(request));
+ }
+ return ALLOW;
+ },
+
+ onResponseStarted(request) {
+ if (request.type === "main_frame") {
+ TabStatus.initTab(request.tabId);
+ }
+ let scriptBlocked = request.responseHeaders.some(
+ h => CSP.isMine(h) && CSP.blocks(h.value, "script")
+ );
+ debug("%s scriptBlocked=%s setting noscriptFrame on ", request.url, scriptBlocked, request.tabId, request.frameId);
+ TabStatus.record(request, "noscriptFrame", scriptBlocked);
+ pendingRequests.get(request.requestId).scriptBlocked = scriptBlocked;
+ },
+
+ onCompleted(request) {
+ let {requestId} = request;
+ if (pendingRequests.has(requestId)) {
+ let r = pendingRequests.get(requestId);
+ pendingRequests.delete(requestId);
+ for (let callback of r.onCompleted) {
+ try {
+ callback(request, r);
+ } catch (e) {
+ error(e);
+ }
+ }
+ }
+ },
+
+ onErrorOccurred(request) {
+ pendingRequests.delete(request.requestId);
+ }
+ };
+
+ function fakeRequestFromCSP(report, request) {
+ let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script
+ if (type === "frame") type = "sub_frame";
+ let url = report['blocked-uri'];
+ if (url === 'self') url = request.documentUrl;
+ return Object.assign({}, request, {
+ url,
+ type,
+ });
+ }
+
+ async function onViolationReport(request) {
+ try {
+ let decoder = new TextDecoder("UTF-8");
+ const report = JSON.parse(decoder.decode(request.requestBody.raw[0].bytes))['csp-report'];
+ let csp = report["original-policy"]
+ debug("CSP report", report);
+ if (report['blocked-uri'] !== 'self') {
+ let r = fakeRequestFromCSP(report, request);
+ Content.reportTo(r, false, policyTypesMap[r.type]);
+ TabStatus.record(r, "blocked");
+ } else if (report["violated-directive"] === "script-src 'none'") {
+ let r = fakeRequestFromCSP(report, request);
+ TabStatus.record(r, "noscriptFrame", true);
+ }
+ } catch(e) {
+ error(e);
+ }
+ return ABORT;
+ }
+
+ const RequestGuard = {
+ async start() {
+ let wr = browser.webRequest;
+ let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
+
+ let allUrls = ["<all_urls>"];
+ let docTypes = ["main_frame", "sub_frame", "object"];
+
+ listen("onBeforeRequest",
+ {urls: allUrls, types: allTypes},
+ ["blocking"]
+ );
+ listen("onHeadersReceived",
+ {urls: allUrls, types: docTypes},
+ ["blocking", "responseHeaders"]
+ );
+ listen("onResponseStarted",
+ {urls: allUrls, types: docTypes},
+ ["responseHeaders"]
+ );
+ listen("onCompleted",
+ {urls: allUrls, types: allTypes},
+ );
+ listen("onErrorOccurred",
+ {urls: allUrls, types: allTypes},
+ );
+
+
+ wr.onBeforeRequest.addListener(onViolationReport,
+ {urls: [REPORT_URI], types: ["csp_report"]}, ["blocking", "requestBody"]);
+
+ TabStatus.probe();
+ },
+
+ stop() {
+ let wr = browser.webRequest;
+ for (let [name, listener] of Object.entries(this.listeners)) {
+ wr[name].removeListener(listener);
+ }
+ wr.onBeforeRequest.removeListener(onViolationReport);
+ }
+ };
+
+ return RequestGuard;
+})();
diff --git a/src/bg/RequestUtil.js b/src/bg/RequestUtil.js
new file mode 100644
index 0000000..1ebbbaa
--- /dev/null
+++ b/src/bg/RequestUtil.js
@@ -0,0 +1,130 @@
+'use strict';
+{
+ let runningScripts = new Map();
+
+ var RequestUtil = {
+ async executeOnStart(request, details) {
+ let {requestId, tabId, frameId} = request;
+ details = Object.assign({
+ runAt: "document_start",
+ frameId,
+ }, details);
+ browser.tabs.executeScript(tabId, details);
+ return;
+ let filter = browser.webRequest.filterResponseData(requestId);
+ filter.onstart = event => {
+ browser.tabs.executeScript(tabId, details);
+ debug("Execute on start", details);
+ filter.write(new Uint8Array());
+ };
+ filter.ondata = event => {
+ filter.write(event.data);
+ filter.disconnect();
+
+ }
+ },
+ async executeOnStartCS(request, details) {
+ let {url, requestId, tabId, frameId} = request;
+
+ let urlObj = new URL(url);
+ if (urlObj.hash || urlObj.port || urlObj.username) {
+ urlObj.hash = urlObj.port = urlObj.username = "";
+ url = urlObj.toString();
+ }
+ let wr = browser.webRequest;
+ let filter = {
+ urls: [`${urlObj.origin}/*`],
+ types: ["main_frame", "sub_frame", "object"]
+ };
+ let finalize;
+ let cleanup = r => {
+ if (cleanup && r.requestId === requestId) {
+ wr.onCompleted.removeListener(cleanup);
+ wr.onErrorOccurred.removeListener(cleanup);
+ cleanup = null;
+ if (finalize) {
+ finalize();
+ }
+ }
+ };
+ wr.onCompleted.addListener(cleanup, filter);
+ wr.onErrorOccurred.addListener(cleanup, filter);
+
+ details = Object.assign({
+ runAt: "document_start",
+ frameId,
+ }, details);
+
+ if (browser.contentScripts) {
+ let js = [{}];
+ if (details.file) js[0].file = details.file;
+ else if (details.code) js[0].code = details.code;
+ let settings = {
+ "runAt": details.runAt,
+ js,
+ matches: [url],
+ allFrames: frameId !== 0,
+ }
+ // let's try to avoid duplicates
+ let key = JSON.stringify(settings);
+ if (runningScripts.has(key)) {
+ let scriptRef = runningScripts.get(key);
+ scriptRef.count++;
+ return;
+ }
+ if (settings.allFrames) {
+ // let's check whether the same script is registered for top frames:
+ // if it is, let's unregister it first to avoid duplicates
+ settings.allFrames = false;
+ let topKey = JSON.stringify(settings);
+ settings.allFrames = true;
+ if (runningScripts.has(topKey)) {
+ let topScript = runningScripts.get(topKey);
+ try {
+ topScript.unregister();
+ } catch (e) {
+ error(e);
+ } finally {
+ runningScripts.delete(topKey);
+ }
+ }
+ }
+
+ let script = await browser.contentScripts.register(settings);
+ debug("Content script %o registered.", settings);
+ finalize = () => {
+ debug("Finalizing content script %o...", settings);
+ try {
+ script.unregister();
+ runningScripts.delete(key);
+ debug("Content script %o unregistered!", settings);
+ } finally {
+ finalize = null;
+ }
+ }
+ runningScripts.set(key, script);
+ if (!cleanup) { // the request has already been interrupted
+ finalize();
+ }
+
+ return;
+ }
+
+ function listener(r) {
+ if (r.requestId === requestId) {
+ browser.tabs.executeScript(tabId, details);
+ finalize();
+ finalize = null;
+ }
+ }
+ finalize = () => {
+ wr.onResponseStarted.removeListener(listener);
+ }
+ wr.onResponseStarted.addListener(listener, filter);
+ debug("Executing %o", details);
+
+ },
+
+
+ }
+}
diff --git a/src/bg/Settings.js b/src/bg/Settings.js
new file mode 100644
index 0000000..4fb656f
--- /dev/null
+++ b/src/bg/Settings.js
@@ -0,0 +1,125 @@
+var Settings = {
+
+ async import(data) {
+
+ // figure out whether it's just a whitelist, a legacy backup or a "Quantum" export
+ try {
+ let json = JSON.parse(data);
+ if (json.whitelist) {
+ return await this.importLegacy(json);
+ }
+ if (json.trusted) {
+ return await this.importPolicy(json);
+ }
+ if (json.policy) {
+ return await this.importSettings(json);
+ }
+ } catch (e) {
+ return await this.importLists(data);
+ }
+ },
+
+ async importLegacy(json) {
+ await include("/legacy/Legacy.js");
+ if (await Legacy.import(json)) {
+ try {
+ ns.policy = Legacy.migratePolicy();
+ await ns.savePolicy();
+ await Legacy.persist();
+ return true;
+ } catch (e) {
+ error(e, "Importing legacy settings");
+ Legacy.migrated = Legacy.undo;
+ }
+ }
+ return false;
+ },
+
+ async importLists(data) {
+ await include("/legacy/Legacy.js");
+ try {
+ let [trusted, untrusted] = Legacy.extractLists(data.split("[UNTRUSTED]"));
+ let policy = ns.policy;
+ for (let site of trusted) {
+ policy.set(site, policy.TRUSTED);
+ }
+ for (let site of untrusted) {
+ policy.set(site, policy.UNTRUSTED, true);
+ }
+ await ns.savePolicy();
+ } catch (e) {
+ error(e, "Importing white/black lists %s", data);
+ return false;
+ }
+ return true;
+ },
+
+ async importPolicy(json) {
+ try {
+ ns.policy = new Policy(json);
+ await ns.savePolicy();
+ return true;
+ } catch (e) {
+ error(e, "Importing policy %o", json);
+ }
+ },
+
+ async importSettings(json) {
+ try {
+ await this.update(json);
+ return true;
+ } catch (e) {
+ error(e, "Importing settings %o", json);
+ }
+ return false;
+ },
+
+ async update(settings) {
+ let {
+ policy,
+ xssUserChoices,
+ tabId,
+ unrestrictedTab,
+ reloadAffected,
+ } = settings;
+ if (xssUserChoices) await XSS.saveUserChoices(xssUserChoices);
+ if (policy) {
+ ns.policy = new Policy(policy);
+ await ns.savePolicy();
+ }
+
+ if (typeof unrestrictedTab === "boolean") {
+ ns.unrestrictedTabs[settings.unrestrictedTab ? "add" : "delete"](tabId);
+ }
+ if (reloadAffected) {
+ browser.tabs.reload(tabId);
+ }
+
+ let oldDebug = ns.local.debug;
+ await Promise.all(["local", "sync"].map(
+ storage => (settings[storage] || // changed or...
+ settings[storage] === null // ... needs reset to default
+ ) && ns.save(
+ ns[storage] = settings[storage] || ns.defaults[storage])
+ ));
+ if (ns.local.debug !== oldDebug) {
+ await include("/lib/log.js");
+ if (oldDebug) debug = () => {};
+ }
+ if (ns.sync.xss) {
+ XSS.start();
+ } else {
+ XSS.stop();
+ }
+ },
+
+ export() {
+ return JSON.stringify({
+ policy: ns.policy.dry(),
+ local: ns.local,
+ sync: ns.sync,
+ xssUserChoices: XSS.getUserChoices(),
+ }, null, 2);
+ },
+
+}
diff --git a/src/bg/defaults.js b/src/bg/defaults.js
new file mode 100644
index 0000000..220bd23
--- /dev/null
+++ b/src/bg/defaults.js
@@ -0,0 +1,37 @@
+'use strict';
+
+ns.defaults = (async () => {
+ let defaults = {
+ local: {
+ debug: false,
+ showCtxMenuItem: true,
+ showCountBadge: true,
+ showFullAddresses: false,
+ },
+ sync: {
+ "global": false,
+ "xss": true,
+ "clearclick": true
+ }
+ };
+ let defaultsClone = JSON.parse(JSON.stringify(defaults));
+
+ for (let [k, v] of Object.entries(defaults)) {
+ let store = await Storage.get(k, k);
+ if (k in store) {
+ Object.assign(v, store[k]);
+ }
+ v.storage = k;
+ }
+
+ Object.assign(ns, defaults);
+
+ // dynamic settings
+ if (!ns.local.uuid) {
+ await include("/lib/uuid.js");
+ ns.local.uuid = uuid();
+ await ns.save(ns.local);
+ }
+
+ return ns.defaults = defaultsClone;
+})();
diff --git a/src/bg/main.js b/src/bg/main.js
new file mode 100644
index 0000000..74df62d
--- /dev/null
+++ b/src/bg/main.js
@@ -0,0 +1,282 @@
+ var ns = (() => {
+ 'use strict';
+
+ const popupURL = browser.extension.getURL("/ui/popup.html");
+ let popupFor = tabId => `${popupURL}#tab${tabId}`;
+
+ let ctxMenuId = "noscript-ctx-menu";
+
+ async function toggleCtxMenuItem(show = ns.local.showCtxMenuItem) {
+ if (!"contextMenus" in browser) return;
+ let id = ctxMenuId;
+ try {
+ await browser.contextMenus.remove(id);
+ } catch (e) {}
+
+ if (show) {
+ browser.contextMenus.create({
+ id,
+ title: "NoScript",
+ contexts: ["all"]
+ });
+ }
+ }
+
+ async function init() {
+ let policyData = (await Storage.get("sync", "policy")).policy;
+ if (policyData && policyData.DEFAULT) {
+ ns.policy = new Policy(policyData);
+ } else {
+ await include("/legacy/Legacy.js");
+ ns.policy = await Legacy.createOrMigratePolicy();
+ ns.savePolicy();
+ }
+
+ await include("/bg/defaults.js");
+ await ns.defaults;
+ await include(["/bg/RequestGuard.js", "/bg/RequestUtil.js"]);
+ await RequestGuard.start();
+ await XSS.start(); // we must start it anyway to initialize sub-objects
+ if (!ns.sync.xss) {
+ XSS.stop();
+ }
+ Commands.install();
+ };
+
+ var Commands = {
+ openPageUI() {
+ try {
+ browser.browserAction.openPopup();
+ return;
+ } catch (e) {
+ debug(e);
+ }
+ browser.windows.create({
+ url: popupURL,
+ width: 800,
+ height: 600,
+ type: "panel"
+ });
+ },
+
+ togglePermissions() {},
+ install() {
+
+
+ if ("command" in browser) {
+ // keyboard shortcuts
+ browser.commands.onCommand.addListener(cmd => {
+ if (cmd in Commands) {
+ Commands[cmd]();
+ }
+ });
+ }
+
+ if ("contextMenus" in browser) {
+ toggleCtxMenuItem();
+ browser.contextMenus.onClicked.addListener((info, tab) => {
+ if (info.menuItemId == ctxMenuId) {
+ this.openPageUI();
+ }
+ });
+ }
+
+ // wiring main UI
+ let ba = browser.browserAction;
+ if ("setIcon" in ba) {
+ //desktop
+ ba.setPopup({
+ popup: popupURL
+ });
+ } else {
+ // mobile
+ ba.onClicked.addListener(async tab => {
+ try {
+ await browser.tabs.remove(await browser.tabs.query({
+ url: popupURL
+ }));
+ } catch (e) {}
+ await browser.tabs.create({
+ url: popupFor(tab.id)
+ });
+ });
+ }
+ }
+ }
+
+ var MessageHandler = {
+ responders: {
+
+ async updateSettings(settings, sender) {
+ await Settings.update(settings);
+ toggleCtxMenuItem();
+ },
+ async broadcastSettings({
+ tabId = -1
+ }) {
+ let policy = ns.policy.dry(true);
+ let seen = tabId !== -1 ? await ns.collectSeen(tabId) : null;
+ let xssUserChoices = await XSS.getUserChoices();
+ browser.runtime.sendMessage({
+ type: "settings",
+ policy,
+ seen,
+ xssUserChoices,
+ local: ns.local,
+ sync: ns.sync,
+ unrestrictedTab: ns.unrestrictedTabs.has(tabId),
+ });
+ },
+
+ exportSettings(m, sender, sendResponse) {
+ sendResponse(Settings.export());
+ return false;
+ },
+
+ async importSettings({
+ data
+ }) {
+ return await Settings.import(data);
+ },
+
+ async openStandalonePopup() {
+ let win = await browser.windows.getLastFocused({
+ windowTypes: ["normal"]
+ });
+ let [tab] = (await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true
+ }));
+
+ if (!tab || tab.id === -1) {
+ log("No tab found to open the UI for");
+ return;
+ }
+ browser.windows.create({
+ url: popupFor(tab.id),
+ width: 800,
+ height: 600,
+ top: win.top + 48,
+ left: win.left + 48,
+ type: "panel"
+ });
+ }
+ },
+ onMessage(m, sender, sendResponse) {
+ let {
+ type
+ } = m;
+ let {
+ responders
+ } = MessageHandler;
+
+
+ if (type && (type = type.replace(/^NoScript\./, '')) in responders) {
+ return responders[type](m, sender, sendResponse);
+ } else {
+ debug("Received unkown message", m, sender);
+ }
+ return false;
+ },
+
+ listen() {
+ browser.runtime.onMessage.addListener(this.onMessage);
+ },
+ }
+
+
+
+ return {
+ running: false,
+ policy: null,
+ local: null,
+ sync: null,
+ unrestrictedTabs: new Set(),
+ isEnforced(tabId = -1) {
+ return this.policy.enforced && (tabId === -1 || !this.unrestrictedTabs.has(tabId));
+ },
+
+ async start() {
+ if (this.running) return;
+ this.running = true;
+
+ let initializing = init();
+ let wr = browser.webRequest;
+ let waitForPolicy = async r => {
+ try {
+ await initializing;
+ } catch (e) {
+ error(e);
+ }
+ }
+ wr.onBeforeRequest.addListener(waitForPolicy, {
+ urls: ["<all_urls>"]
+ }, ["blocking"]);
+ await initializing;
+ wr.onBeforeRequest.removeListener(waitForPolicy);
+
+ await include("/bg/Settings.js");
+ MessageHandler.listen();
+
+ log("STARTED");
+
+ this.devMode = (await browser.management.getSelf()).installType === "development";
+ if (this.local.debug) {
+ if (this.devMode) {
+ include("/test/run.js");
+ }
+ } else {
+ debug = () => {}; // suppress verbosity
+ }
+ },
+
+ stop() {
+ if (!this.running) return;
+ this.running = false;
+ RequestGuard.stop();
+ log("STOPPED");
+ },
+
+ async savePolicy() {
+ if (this.policy) {
+ await Storage.set("sync", {
+ policy: this.policy.dry()
+ });
+ await browser.webRequest.handlerBehaviorChanged()
+ }
+ return this.policy;
+ },
+
+
+
+ async save(obj) {
+ if (obj && obj.storage) {
+ let toBeSaved = {
+ [obj.storage]: obj
+ };
+ Storage.set(obj.storage, toBeSaved);
+ }
+ return obj;
+ },
+
+ async collectSeen(tabId) {
+
+ try {
+ let seen = Array.from(await browser.tabs.sendMessage(tabId, {
+ type: "collect"
+ }, {
+ frameId: 0
+ }));
+ debug("Collected seen", seen);
+ return seen;
+ } catch (e) {
+ // probably a page where content scripts cannot run, let's open the options instead
+ error(e, "Cannot collect noscript activity data");
+ }
+
+ return null;
+ },
+ };
+ })();
+
+ ns.start();