aboutsummaryrefslogtreecommitdiff
path: root/background/ResponseHandler.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'background/ResponseHandler.mjs')
-rw-r--r--background/ResponseHandler.mjs257
1 files changed, 257 insertions, 0 deletions
diff --git a/background/ResponseHandler.mjs b/background/ResponseHandler.mjs
new file mode 100644
index 0000000..6b979e6
--- /dev/null
+++ b/background/ResponseHandler.mjs
@@ -0,0 +1,257 @@
+/**
+* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript.
+* *
+* Copyright (C) 2017, 2018 Nathan Nichols
+* Copyright (C) 2018 Ruben Rodriguez <ruben@gnu.org>
+*
+* This file is part of GNU LibreJS.
+*
+* GNU LibreJS is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* GNU LibreJS is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/**
+* This listener gets called as soon as we've got all the HTTP headers, can guess
+* content type and encoding, and therefore correctly parse HTML documents
+* and external script inclusions in search of crappy JavaScript
+*/
+
+import inject_scripts from './script_injector.mjs';
+import {ResponseProcessor} from './ResponseProcessor.mjs';
+
+"use strict";
+
+var ResponseHandler = {
+ /**
+ * Enforce white/black lists for url/site early (hashes will be handled later)
+ */
+ async pre(response) {
+ // TODO: reimplement blacklisting/whitelisting later
+ if (true) return ResponseProcessor.CONTINUE;
+
+ let {request} = response;
+ let {url, type, tabId, frameId, documentUrl} = request;
+
+ let fullUrl = url;
+ url = ListStore.urlItem(url);
+ let site = ListStore.siteItem(url);
+
+ let blacklistedSite = ListManager.siteMatch(site, blacklist);
+ let blacklisted = blacklistedSite || blacklist.contains(url);
+ let topUrl = type === "sub_frame" && request.frameAncestors && request.frameAncestors.pop() || documentUrl;
+
+ if (blacklisted) {
+ if (type === "script") {
+ // this shouldn't happen, because we intercept earlier in blockBlacklistedScripts()
+ return ResponseProcessor.REJECT;
+ }
+ if (type === "main_frame") { // we handle the page change here too, since we won't call edit_html()
+ activityReports[tabId] = await createReport({url: fullUrl, tabId});
+ // Go on without parsing the page: it was explicitly blacklisted
+ let reason = blacklistedSite
+ ? `All ${blacklistedSite} blacklisted by user`
+ : "Address blacklisted by user";
+ await addReportEntry(tabId, url, {"blacklisted": [blacklistedSite || url, reason], url: fullUrl});
+ }
+ // use CSP to restrict JavaScript execution in the page
+ request.responseHeaders.unshift({
+ name: `Content-security-policy`,
+ value: `script-src 'none';`
+ });
+ return {responseHeaders: request.responseHeaders}; // let's skip the inline script parsing, since we block by CSP
+ } else {
+
+ let whitelistedSite = ListManager.siteMatch(site, whitelist);
+ let whitelisted = response.whitelisted = whitelistedSite || whitelist.contains(url);
+ if (type === "script") {
+ if (whitelisted) {
+ // accept the script and stop processing
+ addReportEntry(tabId, url, {url: topUrl,
+ "whitelisted": [url, whitelistedSite ? `User whitelisted ${whitelistedSite}` : "Whitelisted by user"]});
+ return ResponseProcessor.ACCEPT;
+ } else {
+ let scriptInfo = await ExternalLicenses.check({url: fullUrl, tabId, frameId, documentUrl});
+ if (scriptInfo) {
+ let verdict, ret;
+ let msg = scriptInfo.toString();
+ if (scriptInfo.free) {
+ verdict = "accepted";
+ ret = ResponseProcessor.ACCEPT;
+ } else {
+ verdict = "blocked";
+ ret = ResponseProcessor.REJECT;
+ }
+ addReportEntry(tabId, url, {url, [verdict]: [url, msg]});
+ return ret;
+ }
+ }
+ }
+ }
+ // it's a page (it's too early to report) or an unknown script:
+ // let's keep processing
+ return ResponseProcessor.CONTINUE;
+ },
+
+ /**
+ * Here we do the heavylifting, analyzing unknown scripts
+ */
+ async post(response) {
+ let {type} = response.request;
+ return await handle_html(response, response.whitelisted);
+ }
+}
+
+/**
+* Serializes HTMLDocument objects including the root element and
+* the DOCTYPE declaration
+*/
+function doc2HTML(doc) {
+ let s = doc.documentElement.outerHTML;
+ if (doc.doctype) {
+ let dt = doc.doctype;
+ let sDoctype = `<!DOCTYPE ${dt.name || "html"}`;
+ if (dt.publicId) sDoctype += ` PUBLIC "${dt.publicId}"`;
+ if (dt.systemId) sDoctype += ` "${dt.systemId}"`;
+ s = `${sDoctype}>\n${s}`;
+ }
+ return s;
+}
+
+/**
+* Shortcut to create a correctly namespaced DOM HTML elements
+*/
+function createHTMLElement(doc, name) {
+ return doc.createElementNS("http://www.w3.org/1999/xhtml", name);
+}
+
+/**
+* Replace any element with a span having the same content (useful to force
+* NOSCRIPT elements to visible the same way as NoScript and uBlock do)
+*/
+function forceElement(doc, element) {
+ let replacement = createHTMLElement(doc, "span");
+ replacement.innerHTML = element.innerHTML;
+ element.replaceWith(replacement);
+ return replacement;
+}
+
+/**
+* Forces displaying any element having the "data-librejs-display" attribute and
+* <noscript> elements on pages where LibreJS disabled inline scripts (unless
+* they have the "data-librejs-nodisplay" attribute).
+*/
+function forceNoscriptElements(doc) {
+ let shown = 0;
+ // inspired by NoScript's onScriptDisabled.js
+ for (let noscript of doc.querySelectorAll("noscript:not([data-librejs-nodisplay])")) {
+ let replacement = forceElement(doc, noscript);
+ // emulate meta-refresh
+ let meta = replacement.querySelector('meta[http-equiv="refresh"]');
+ if (meta) {
+ refresh = true;
+ doc.head.appendChild(meta);
+ }
+ shown++;
+ }
+ return shown;
+}
+
+/**
+* Forces displaying any element having the "data-librejs-display" attribute and
+* <noscript> elements on pages where LibreJS disabled inline scripts (unless
+* they have the "data-librejs-nodisplay" attribute).
+*/
+function showConditionalElements(doc) {
+ let shown = 0;
+ for (let element of document.querySelectorAll("[data-librejs-display]")) {
+ forceElement(doc, element);
+ shown++;
+ }
+ return shown;
+}
+
+/**
+
+* Reads/changes the HTML of a page and the scripts within it.
+*/
+async function editHtml(html, documentUrl, tabId, frameId, whitelisted){
+
+ var parser = new DOMParser();
+ var html_doc = parser.parseFromString(html, "text/html");
+
+ if (whitelisted) { // don't bother rewriting
+ return null;
+ }
+
+ var scripts = html_doc.scripts;
+
+ let findLine = finder => finder.test(html) && html.substring(0, finder.lastIndex).split(/\n/).length || 0;
+
+ let modified = false;
+ // Deal with intrinsic events
+ let intrinsicFinder = /<[a-z][^>]*\b(on\w+|href\s*=\s*['"]?javascript:)/gi;
+ for (let element of html_doc.all) {
+ let line = -1;
+ for (let attr of element.attributes) {
+ let {name, value} = attr;
+ value = value.trim();
+ if (name.startsWith("on")) {
+ attr.value = "console.log(\"event script blocked by myext\")";
+ } else if (name === "href" && value.toLowerCase().startsWith("javascript:")){
+ if (line === -1) {
+ line = findLine(intrinsicFinder);
+ }
+ try {
+ attr.value = `view-source:${documentUrl}#line${line}`;
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ }
+
+ let modifiedInline = false;
+ let scriptFinder = /<script\b/ig;
+ for(let i = 0, len = scripts.length; i < len; i++) {
+ let script = scripts[i];
+ let line = findLine(scriptFinder);
+ if (!script.src) {
+ script.textContent = `//script blocked, you can examine it at view-source:${documentUrl}#line${line}`;
+ } else {
+ let src = script.src;
+ script.removeAttribute("src");
+ script.setAttribute("blocked-src", src);
+ script.textContent = "//script blocked";
+ }
+ }
+
+ showConditionalElements(html_doc);
+ forceNoscriptElements(html_doc);
+ await inject_scripts(documentUrl, html_doc);
+ return doc2HTML(html_doc);
+}
+
+/**
+* Here we handle html document responses
+*/
+async function handle_html(response, whitelisted) {
+ let {text, request} = response;
+ let {url, tabId, frameId, type} = request;
+ if (type === "main_frame") {
+ //activityReports[tabId] = await createReport({url, tabId});
+ //updateBadge(tabId);
+ }
+ return await editHtml(text, url, tabId, frameId, whitelisted);
+}
+
+export default ResponseHandler;