diff options
Diffstat (limited to 'src/content')
-rw-r--r-- | src/content/DocumentCSP.js | 29 | ||||
-rw-r--r-- | src/content/PlaceHolder.js | 26 | ||||
-rw-r--r-- | src/content/content.js | 211 | ||||
-rw-r--r-- | src/content/dynamicNS.js | 21 | ||||
-rw-r--r-- | src/content/embeddingDocument.js | 20 | ||||
-rw-r--r-- | src/content/media.js | 4 | ||||
-rw-r--r-- | src/content/staticNS.js | 99 | ||||
-rw-r--r-- | src/content/webglHook.js | 4 |
8 files changed, 224 insertions, 190 deletions
diff --git a/src/content/DocumentCSP.js b/src/content/DocumentCSP.js new file mode 100644 index 0000000..371e547 --- /dev/null +++ b/src/content/DocumentCSP.js @@ -0,0 +1,29 @@ +'use strict'; + +class DocumentCSP { + constructor(document) { + this.document = document; + this.builder = new CapsCSP(); + } + + apply(capabilities, embedding = CSP.isEmbedType(this.document.contentType)) { + let csp = this.builder; + let blocker = csp.buildFromCapabilities(capabilities, embedding); + if (!blocker) return; + + let document = this.document; + let header = csp.asHeader(blocker); + let meta = document.createElementNS("http://www.w3.org/1999/xhtml", "meta"); + meta.setAttribute("http-equiv", header.name); + meta.setAttribute("content", header.value); + let parent = document.head || document.documentElement; + try { + parent.insertBefore(meta, parent.firstChild); + debug(`Failsafe <meta> CSP inserted in the DOM: "%s"`, header.value); + if (capabilities.has("script")) meta.remove(); + } catch (e) { + error(e, "Error inserting CSP %s in the DOM", header && header.value); + } + } + +} diff --git a/src/content/PlaceHolder.js b/src/content/PlaceHolder.js index ec2ab5b..09f6767 100644 --- a/src/content/PlaceHolder.js +++ b/src/content/PlaceHolder.js @@ -1,6 +1,17 @@ var PlaceHolder = (() => { const HANDLERS = new Map(); - + + let checkStyle = async () => { + checkStyle = () => {}; + if (!ns.embeddingDocument) return; + let replacement = document.querySelector("a.__NoScript_PlaceHolder__"); + if (!replacement) return; + if (window.getComputedStyle(replacement, null).opacity !== "0.8") { + document.head.appendChild(createHTMLElement("style")).textContent = await + (await fetch(browser.extension.getURL("/content/content.css"))).text(); + } + } + class Handler { constructor(type, selector) { this.type = type; @@ -9,6 +20,7 @@ var PlaceHolder = (() => { HANDLERS.set(type, this); } filter(element, request) { + if (request.embeddingDocument) return true; let url = request.initialUrl || request.url; return "data" in element ? element.data === url : element.src === url; } @@ -77,10 +89,14 @@ var PlaceHolder = (() => { .filter(element => this.handler.filter(element, request)) .forEach(element => this.replace(element)); }; - if (this.replacements.size) PlaceHolder.listen(); + if (this.replacements.size) { + PlaceHolder.listen(); + checkStyle(); + } } replace(element) { + if (!element.parentElement) return; let { url } = this.request; @@ -108,10 +124,10 @@ var PlaceHolder = (() => { replacement._placeHolderObj = this; replacement._placeHolderElement = element; - this.replacements.add(replacement); + - if (element.parentNode) element.parentNode.replaceChild(replacement, element); - else document.body.appendChild(replacement); + element.parentNode.replaceChild(replacement, element); + this.replacements.add(replacement); } async enable(replacement) { diff --git a/src/content/content.js b/src/content/content.js index 8ab3654..c7fc045 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -1,105 +1,12 @@ 'use strict'; +// debug = () => {}; // REL_ONLY - // debug = () => {}; // REL_ONLY -{ - let listenersMap = new Map(); - let backlog = new Set(); - var ns = { - on(eventName, listener) { - let listeners = listenersMap.get(eventName); - if (!listeners) listenersMap.set(eventName, listeners = new Set()); - listeners.add(listener); - if (backlog.has(eventName)) this.fire(eventName, listener); - }, - detach(eventName, listener) { - let listeners = listenersMap.get(eventName); - if (listeners) listeners.delete(listener); - }, - fire(eventName, listener = null) { - if (listener) { - listener({type:eventName, source: this}); - return; - } - let listeners = listenersMap.get(eventName); - if (listeners) { - for (let l of listeners) { - this.fire(eventName, l); - } - } - backlog.add(eventName); - }, - setup(DEFAULT, MARKER) { - this.perms.DEFAULT = DEFAULT; - if(!this.perms.CURRENT) this.perms.CURRENT = DEFAULT; - - // ugly hack: since now we use registerContentScript instead of the - // filterRequest dynamic script injection hack, we use top.name - // to store per-tab information. We don't want web content to - // mess with it, though, so we wrap it around auto-hiding accessors - this.perms.MARKER = MARKER; - let eraseTabInfoRx = new RegExp(`[^]*${MARKER},?`); - if (eraseTabInfoRx.test(top.name)) { - let _name = top.name; - let tabInfoRx = new RegExp(`^${MARKER}\\[([^]*?)\\]${MARKER},`); - if (top === window) { // wrap to hide - Reflect.defineProperty(top.wrappedJSObject, "name", { - get: exportFunction(() => top.name.replace(eraseTabInfoRx, ""), top.wrappedJSObject), - set: exportFunction(value => { - let preamble = top.name.match(tabInfoRx); - top.name = `${preamble && preamble[0] || ""}${value}`; - return value; - }, top.wrappedJSObject) - }); - } - let tabInfoMatch = _name.match(tabInfoRx); - if (tabInfoMatch) try { - this.perms.tabInfo = JSON.parse(tabInfoMatch[1]); - } catch (e) { - error(e); - } - } - - if (!this.perms.DEFAULT || this.perms.tabInfo.unrestricted) { - this.allows = () => true; - } - ns.fire("perms"); - }, - perms: { DEFAULT: null, CURRENT: null, tabInfo: {}, MARKER: "" }, - allows(cap) { - let perms = this.perms.CURRENT; - return perms && perms.capabilities.includes(cap); - }, - getWindowName() { - return top !== window || !this.perms.MARKER ? window.name - : window.name.split(this.perms.MARKER + ",").pop(); - } - } -} - -var canScript = true, shouldScript = false; - -let now = () => performance.now() + performance.timeOrigin; +var _ = browser.i18n.getMessage; function createHTMLElement(name) { return document.createElementNS("http://www.w3.org/1999/xhtml", name); } -function probe() { - try { - debug("Probing execution..."); - let s = document.createElement("script"); - s.textContent = ";"; - document.documentElement.appendChild(s); - s.remove(); - } catch(e) { - debug(e); - } -} - -var _ = browser.i18n.getMessage; - -var embeddingDocument = false; - var seen = { _map: new Map(), _list: null, @@ -121,9 +28,8 @@ Messages.addHandler({ seen.record(event); } if (ownFrame) { - init(); if (!allowed && PlaceHolder.canReplace(policyType)) { - request.embeddingDocument = embeddingDocument; + request.embeddingDocument = ns.embeddingDocument; PlaceHolder.create(policyType, request); } } @@ -135,87 +41,38 @@ Messages.addHandler({ } }); -if (document.readyState !== "complete") { - let pageshown = e => { - removeEventListener("pageshow", pageshown); - init(); - }; - addEventListener("pageshow", pageshown); -} else { - init(true); -} -let notifyPage = async () => { + +debug(`Loading NoScript in document %s, scripting=%s, readyState %s`, + document.URL, ns.canScript, document.readyState); + +var notifyPage = async () => { debug("Page %s shown, %s", document.URL, document.readyState); if (document.readyState === "complete") { try { - await Messages.send("pageshow", {seen: seen.list, canScript}); + if (!("canScript" in ns)) { + let childPolicy = await Messages.send("fetchChildPolicy", {url: document.URL, contextUrl: top.location.href}); + ns.config.CURRENT = childPolicy.CURRENT; + ns.setup(childPolicy.DEFAULT, childPolicy.MARKER); + return; + } + + await Messages.send("pageshow", {seen: seen.list, canScript: ns.canScript}); return true; } catch (e) { debug(e); + if (/Receiving end does not exist/.test(e.message)) { + window.setTimeout(notifyPage, 2000); + } } } return false; } -var queryingStatus = false; - -function reload(noCache = false) { - init = () => {}; - location.reload(noCache); -} +notifyPage(); -async function init(oldPage = false) { - if (queryingStatus) return; - if (!document.URL.startsWith("http")) { - return; - } - queryingStatus = true; +window.addEventListener("pageshow", notifyPage); - debug(`init() called in document %s, contentType %s readyState %s, frameElement %o`, - document.URL, document.contentType, document.readyState, window.frameElement && frameElement.data); - - try { - ({canScript, shouldScript} = await Messages.send("queryDocStatus", {url: document.URL})); - debug(`document %s, canScript=%s, shouldScript=%s, readyState %s`, document.URL, canScript, shouldScript, document.readyState); - if (canScript) { - if (oldPage) { - probe(); - return; - } - if (!shouldScript && - (document.readyState !== "complete" || - now() - performance.timing.domContentLoadedEventStart < 5000)) { - // Something wrong: scripts can run, permissions say they shouldn't. - // Was webRequest bypassed by caching/session restore/service workers? - window.stop(); - let noCache = !!navigator.serviceWorker.controller; - if (noCache) { - for (let r of await navigator.serviceWorker.getRegistrations()) { - await r.unregister(); - } - } - debug("Reloading %s (%s)", document.URL, noCache ? "no cache" : "cached"); - reload(noCache); - return; - } - } - init = () => {}; - } catch (e) { - debug("Error querying docStatus", e); - if (!oldPage && - /Receiving end does not exist/.test(e.message)) { - // probably startup and bg page not ready yet, hence no CSP: reload! - debug("Reloading", document.URL); - reload(); - } else { - setTimeout(() => init(oldPage), 100); - } - return; - } finally { - queryingStatus = false; - } - - if (!canScript) onScriptDisabled(); +ns.on("capabilities", () => { seen.record({ request: { key: "noscript-probe", @@ -223,21 +80,13 @@ async function init(oldPage = false) { documentUrl: document.URL, type: window === window.top ? "main_frame" : "script", }, - allowed: canScript - } - ); - - debug(`Loading NoScript in document %s, scripting=%s, readyState %s`, - document.URL, canScript, document.readyState); - - if (/application|video|audio/.test(document.contentType)) { - debug("Embedding document detected"); - embeddingDocument = true; - window.addEventListener("pageshow", e => { - debug("Active content still in document %s: %o", document.url, document.querySelectorAll("embed,object,video,audio")); - }, true); - // document.write("<plaintext>"); + allowed: ns.canScript + }); + + if (!ns.canScript) { + if (document.readyState !== "loading") onScriptDisabled(); + window.addEventListener("DOMContentLoaded", onScriptDisabled); } + notifyPage(); - addEventListener("pageshow", notifyPage); -} +}); diff --git a/src/content/dynamicNS.js b/src/content/dynamicNS.js new file mode 100644 index 0000000..cdd7a0e --- /dev/null +++ b/src/content/dynamicNS.js @@ -0,0 +1,21 @@ +'use strict'; + +// ensure the order which manifest scripts and dynamically registered scripts +// are executed in doesn't matter for initialization, by using a stub. + +if (!this.ns) { + let deferredSetup = null; + let nsStub = this.ns = { + config: {}, + setup(DEFAULT, MARKER) { + deferredSetup = [DEFAULT, MARKER]; + }, + merge: ns => { + ns.config = Object.assign(ns.config, nsStub.config); + this.ns = ns; + if (deferredSetup) { + ns.setup(...deferredSetup); + } + } + } +} diff --git a/src/content/embeddingDocument.js b/src/content/embeddingDocument.js new file mode 100644 index 0000000..75b0db0 --- /dev/null +++ b/src/content/embeddingDocument.js @@ -0,0 +1,20 @@ +if (ns.embeddingDocument) { + ns.on("capabilities", () => { + for (let policyType of ["object", "media"]) { + if (!ns.allows(policyType)) { + let request = { + id: `noscript-${policyType}-doc`, + type: policyType, + url: document.URL, + documentUrl: document.URL, + embeddingDocument: true, + }; + let ph = PlaceHolder.create(policyType, request); + if (ph.replacements.size > 0) { + debug(`Created placeholder for ${policyType} at ${document.URL}`); + seen.record({policyType, request, allowed: false}); + } + } + } + }); +} diff --git a/src/content/media.js b/src/content/media.js index 910fd27..770a43f 100644 --- a/src/content/media.js +++ b/src/content/media.js @@ -1,5 +1,5 @@ -ns.on("perms", event => { - debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY +ns.on("capabilities", event => { + debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.capabilities); // DEV_ONLY let mediaBlocker = !ns.allows("media"); let unpatched = new Map(); function patch(obj, methodName, replacement) { diff --git a/src/content/staticNS.js b/src/content/staticNS.js new file mode 100644 index 0000000..817351a --- /dev/null +++ b/src/content/staticNS.js @@ -0,0 +1,99 @@ +'use strict'; +{ + let listenersMap = new Map(); + let backlog = new Set(); + + let ns = { + debug: true, // DEV_ONLY + get embeddingDocument() { + delete this.embeddingDocument; + return this.embeddingDocument = CSP.isEmbedType(document.contentType); + }, + on(eventName, listener) { + let listeners = listenersMap.get(eventName); + if (!listeners) listenersMap.set(eventName, listeners = new Set()); + listeners.add(listener); + if (backlog.has(eventName)) this.fire(eventName, listener); + }, + detach(eventName, listener) { + let listeners = listenersMap.get(eventName); + if (listeners) listeners.delete(listener); + }, + fire(eventName, listener = null) { + if (listener) { + listener({type:eventName, source: this}); + return; + } + let listeners = listenersMap.get(eventName); + if (listeners) { + for (let l of listeners) { + this.fire(eventName, l); + } + } + backlog.add(eventName); + }, + + setup(DEFAULT, MARKER) { + this.config.DEFAULT = DEFAULT; + if(!this.config.CURRENT) this.config.CURRENT = DEFAULT; + + // ugly hack: since now we use registerContentScript instead of the + // filterRequest dynamic script injection hack, we use top.name + // to store per-tab information. We don't want web content to + // mess with it, though, so we wrap it around auto-hiding accessors + this.config.MARKER = MARKER; + let eraseTabInfoRx = new RegExp(`[^]*${MARKER},?`); + if (eraseTabInfoRx.test(top.name)) { + let _name = top.name; + let tabInfoRx = new RegExp(`^${MARKER}\\[([^]*?)\\]${MARKER},`); + if (top === window) { // wrap to hide + Reflect.defineProperty(top.wrappedJSObject, "name", { + get: exportFunction(() => top.name.replace(eraseTabInfoRx, ""), top.wrappedJSObject), + set: exportFunction(value => { + let preamble = top.name.match(tabInfoRx); + top.name = `${preamble && preamble[0] || ""}${value}`; + return value; + }, top.wrappedJSObject) + }); + } + let tabInfoMatch = _name.match(tabInfoRx); + if (tabInfoMatch) try { + this.config.tabInfo = JSON.parse(tabInfoMatch[1]); + } catch (e) { + error(e); + } + } + + if (!this.config.DEFAULT || this.config.tabInfo.unrestricted) { + this.allows = () => true; + this.capabilities = Object.assign( + new Set(["script"]), { has() { return true; } }); + } else { + let perms = this.config.CURRENT; + this.capabilities = new Set(perms.capabilities); + new DocumentCSP(document).apply(this.capabilities, this.embeddingDocument); + } + + this.canScript = this.allows("script"); + this.fire("capabilities"); + }, + config: { DEFAULT: null, CURRENT: null, tabInfo: {}, MARKER: "" }, + + allows(cap) { + return this.capabilities && this.capabilities.has(cap); + }, + + getWindowName() { + let marker = this.config.MARKER; + return (top === window && marker) ? + window.name.split(`${marker},`).pop() + : window.name; + } + }; + + if (this.ns) { + this.ns.merge(ns); + } else { + this.ns = ns; + } +} diff --git a/src/content/webglHook.js b/src/content/webglHook.js index efafcd5..4475585 100644 --- a/src/content/webglHook.js +++ b/src/content/webglHook.js @@ -1,5 +1,5 @@ -ns.on("perms", event => { - debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY +ns.on("capabilities", event => { + debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.capabilities); // DEV_ONLY if (ns.allows("webgl")) return; let proto = HTMLCanvasElement.prototype; let getContext = proto.getContext; |