diff options
Diffstat (limited to 'src/bg')
-rw-r--r-- | src/bg/RequestGuard.js | 549 | ||||
-rw-r--r-- | src/bg/RequestUtil.js | 130 | ||||
-rw-r--r-- | src/bg/Settings.js | 125 | ||||
-rw-r--r-- | src/bg/defaults.js | 37 | ||||
-rw-r--r-- | src/bg/main.js | 282 |
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(); |