From 1de1db3c29d11f49e130d44db359d1187f6c24a0 Mon Sep 17 00:00:00 2001 From: hackademix Date: Tue, 21 Aug 2018 23:54:04 +0200 Subject: Fixed possible surprises in background script message handling. --- src/bg/RequestGuard.js | 128 +++++------ src/bg/main.js | 516 +++++++++++++++++++++------------------------ src/content/PlaceHolder.js | 2 +- src/content/content.js | 6 +- src/lib/Messages.js | 31 +++ src/manifest.json | 1 + src/ui/ui.js | 8 +- 7 files changed, 351 insertions(+), 341 deletions(-) create mode 100644 src/lib/Messages.js diff --git a/src/bg/RequestGuard.js b/src/bg/RequestGuard.js index 69c6348..ccf3c70 100644 --- a/src/bg/RequestGuard.js +++ b/src/bg/RequestGuard.js @@ -213,71 +213,71 @@ var RequestGuard = (() => { 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 "docStatus": { - let {frameId, tab} = sender; - let {url} = message; - let tabId = tab.id; - let records = TabStatus.map.get(tabId); - let noscriptFrames = records && records.noscriptFrames; - let canScript = !(noscriptFrames && noscriptFrames[sender.frameId]); - let shouldScript = !ns.isEnforced(tabId) || !url.startsWith("http") || ns.policy.can(url, "script"); - debug("Frame %s %s of %o, canScript: %s, shouldScript: %s", frameId, url, noscriptFrames, canScript, shouldScript); - return {canScript, shouldScript}; + + + let messageHandler = { + async pageshow(message, sender) { + TabStatus.recordAll(sender.tab.id, message.seen); + return true; + }, + async enable(message, sender) { + 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); + await ns.savePolicy(); } + return true; }, + + async docStatus(message, sender) { + let {frameId, tab} = sender; + let {url} = message; + let tabId = tab.id; + let records = TabStatus.map.get(tabId); + let noscriptFrames = records && records.noscriptFrames; + let canScript = !(noscriptFrames && noscriptFrames[sender.frameId]); + let shouldScript = !ns.isEnforced(tabId) || !url.startsWith("http") || ns.policy.can(url, "script"); + debug("Frame %s %s of %o, canScript: %s, shouldScript: %s", frameId, url, noscriptFrames, canScript, shouldScript); + return {canScript, shouldScript}; + } + + } + + const Content = { async reportTo(request, allowed, policyType) { let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request; @@ -312,7 +312,6 @@ var RequestGuard = (() => { } } }; - browser.runtime.onMessage.addListener(Content.hearFrom); const pendingRequests = new Map(); function initPendingRequest(request) { @@ -551,6 +550,8 @@ var RequestGuard = (() => { const RequestGuard = { async start() { + Messages.addHandler(messageHandler); + let wr = browser.webRequest; let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args); @@ -608,6 +609,7 @@ var RequestGuard = (() => { } } wr.onBeforeRequest.removeListener(onViolationReport); + Messages.removeHandler(messageHandler); } }; diff --git a/src/bg/main.js b/src/bg/main.js index 75ea868..3882191 100644 --- a/src/bg/main.js +++ b/src/bg/main.js @@ -1,272 +1,248 @@ - 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); - await ChildPolicies.update(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"); - 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(); - 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)); - }, - - start() { - if (this.running) return; - this.running = true; - - deferWebTraffic(init(), - async () => { - - 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 ChildPolicies.update(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; - }, - }; - })(); + { + 'use strict'; + + let 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); + await ChildPolicies.update(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"); + await RequestGuard.start(); + await XSS.start(); // we must start it anyway to initialize sub-objects + if (!ns.sync.xss) { + XSS.stop(); + } + Commands.install(); + }; + + let 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) + }); + }); + } + } + } + + let messageHandler = { + 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), + }); + }, + + async exportSettings() { + return Settings.export(); + }, + + async importSettings({data}) { + return await Settings.import(data); + }, + + async openStandalonePopup() { + let win = await browser.windows.getLastFocused(); + 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" + }); + }, + }; + + + + var ns = { + running: false, + policy: null, + local: null, + sync: null, + unrestrictedTabs: new Set(), + isEnforced(tabId = -1) { + return this.policy.enforced && (tabId === -1 || !this.unrestrictedTabs.has(tabId)); + }, + + start() { + if (this.running) return; + this.running = true; + + deferWebTraffic(init(), + async () => { + + await include("/bg/Settings.js"); + Messages.addHandler(messageHandler); + + 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; + Messages.removeHandler(messageHandler); + RequestGuard.stop(); + log("STOPPED"); + }, + + async savePolicy() { + if (this.policy) { + await ChildPolicies.update(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(); diff --git a/src/content/PlaceHolder.js b/src/content/PlaceHolder.js index d8c422b..764b7bc 100644 --- a/src/content/PlaceHolder.js +++ b/src/content/PlaceHolder.js @@ -117,7 +117,7 @@ var PlaceHolder = (() => { async enable(replacement) { debug("Enabling %o", this.request, this.policyType); let ok = await browser.runtime.sendMessage({ - type: "enable", + action: "enable", url: this.request.url, policyType: this.policyType, documentUrl: document.URL diff --git a/src/content/content.js b/src/content/content.js index 8f772b1..d5adc94 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -88,7 +88,7 @@ function probe() { try { debug("Probing execution..."); let s = document.createElement("script"); - s.textContent=";"; + s.textContent = ";"; document.documentElement.appendChild(s); s.remove(); } catch(e) { @@ -157,7 +157,7 @@ let notifyPage = async () => { debug("Page %s shown, %s", document.URL, document.readyState); if (document.readyState === "complete") { try { - await browser.runtime.sendMessage({type: "pageshow", seen: seen.list, canScript}); + await browser.runtime.sendMessage({action: "pageshow", seen: seen.list, canScript}); return true; } catch (e) { debug(e); @@ -184,7 +184,7 @@ async function init(oldPage = false) { document.URL, document.contentType, document.readyState, window.frameElement && frameElement.data); try { - ({canScript, shouldScript} = await browser.runtime.sendMessage({type: "docStatus", url: document.URL})); + ({canScript, shouldScript} = await browser.runtime.sendMessage({action: "docStatus", url: document.URL})); debug(`document %s, canScript=%s, shouldScript=%s, readyState %s`, document.URL, canScript, shouldScript, document.readyState); if (canScript) { if (oldPage) { diff --git a/src/lib/Messages.js b/src/lib/Messages.js new file mode 100644 index 0000000..c87a8a8 --- /dev/null +++ b/src/lib/Messages.js @@ -0,0 +1,31 @@ +"use strict"; +{ + let handlers = new Set(); + + let dispatch = async (msg, sender) => { + let {action} = msg; + for (let h of handlers) { + let f = h[action]; + if (typeof f === "function") { + return await f(msg, sender); + } + } + }; + + var Messages = { + addHandler(handler) { + let originalSize = handlers.size; + handlers.add(handler); + if (originalSize === 0 && handlers.size === 1) { + browser.runtime.onMessage.addListener(dispatch); + } + }, + removeHandler(handler) { + let originalSize = handlers.size; + handlers.delete(handler); + if (originalSize === 1 && handlers.size === 0) { + browser.runtime.onMessage.remveListener(dispatch); + } + } + } +} diff --git a/src/manifest.json b/src/manifest.json index cc4bc08..5fafb63 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -40,6 +40,7 @@ "lib/punycode.js", "lib/tld.js", "lib/LastListener.js", + "lib/Messages.js", "common/Policy.js", "common/locale.js", "common/Entities.js", diff --git a/src/ui/ui.js b/src/ui/ui.js index f721042..be9063b 100644 --- a/src/ui/ui.js +++ b/src/ui/ui.js @@ -59,11 +59,11 @@ var UI = (() => { debug("Imported", Policy); }, async pullSettings() { - browser.runtime.sendMessage({type: "NoScript.broadcastSettings", tabId: UI.tabId}); + browser.runtime.sendMessage({action: "broadcastSettings", tabId: UI.tabId}); }, async updateSettings({policy, xssUserChoices, unrestrictedTab, local, sync, reloadAffected}) { if (policy) policy = policy.dry(true); - return await browser.runtime.sendMessage({type: "NoScript.updateSettings", + return await browser.runtime.sendMessage({action: "updateSettings", policy, xssUserChoices, unrestrictedTab, @@ -75,10 +75,10 @@ var UI = (() => { }, async exportSettings() { - return await browser.runtime.sendMessage({type: "NoScript.exportSettings"}); + return await browser.runtime.sendMessage({action: "exportSettings"}); }, async importSettings(data) { - return await browser.runtime.sendMessage({type: "NoScript.importSettings", data}); + return await browser.runtime.sendMessage({action: "importSettings", data}); }, async revokeTemp() { -- cgit v1.2.3