diff options
author | hackademix <giorgio@maone.net> | 2018-07-30 16:18:17 +0200 |
---|---|---|
committer | hackademix <giorgio@maone.net> | 2018-07-30 16:18:17 +0200 |
commit | 5e0ab1515ab1c399a6405f579c3b931563e4020d (patch) | |
tree | d163af155124b72fe3dc4c326d998bd6b2e2361a | |
parent | 096cd90010b093e7c211f3318cbe41711ecfdef9 (diff) |
Implement early whitelisting / blacklisting logic.
-rw-r--r-- | bg/ResponseProcessor.js | 24 | ||||
-rw-r--r-- | bg/Storage.js | 29 | ||||
-rw-r--r-- | main_background.js | 329 |
3 files changed, 181 insertions, 201 deletions
diff --git a/bg/ResponseProcessor.js b/bg/ResponseProcessor.js index 3f3151b..400cb42 100644 --- a/bg/ResponseProcessor.js +++ b/bg/ResponseProcessor.js @@ -52,6 +52,13 @@ class ResponseProcessor { } } +Object.assign(ResponseProcessor, { + // control flow values to be returned by handler.pre() callbacks + ACCEPT: {}, + REJECT: {cancel: true}, + CONTINUE: null +}); + class ResponseTextFilter { constructor(request) { this.request = request; @@ -64,9 +71,16 @@ class ResponseTextFilter { } process(handler) { - if (!this.canProcess) return {}; - let metaData = this.metaData; - let {requestId, responseHeaders} = this.request; + if (!this.canProcess) return ResponseProcessor.ACCEPT; + let {metaData, request} = this; + if (typeof handler.pre === "function") { + let res = handler.pre({request, metaData}); + if (res) return res; + if (handler.post) handler = handler.post; + if (typeof handler !== "function") ResponseProcessor.ACCEPT; + } + + let {requestId, responseHeaders} = request; let filter = browser.webRequest.filterResponseData(requestId); let buffer = []; @@ -83,7 +97,7 @@ class ResponseTextFilter { let editedText = null; try { let response = { - request: this.request, + request, metaData, text, }; @@ -103,7 +117,7 @@ class ResponseTextFilter { filter.disconnect(); } - return metaData.forceUTF8() ? {responseHeaders} : {}; + return metaData.forceUTF8() ? {responseHeaders} : ResponseProcessor.ACCEPT;; } } diff --git a/bg/Storage.js b/bg/Storage.js index 1386538..ecdc9e4 100644 --- a/bg/Storage.js +++ b/bg/Storage.js @@ -58,14 +58,35 @@ class ListStore { this.items = new Set(); } + static hashItem(hash) { + return hash.startsWith("(") ? hash : `(${hash})`; + } + static urlItem(url) { + let queryPos = url.indexOf("?"); + return queryPos === -1 ? url : url.substring(0, queryPos); + } + static siteItem(url) { + if (url.endsWith("/*")) return url; + try { + return `${new URL(url).origin}/*`; + } catch (e) { + return `${url}/*`; + } + } + async save() { return await this.storage.save(this.key, this.items); } async load() { - return await this.storage.load(this.key); + try { + this.items = await this.storage.load(this.key); + } catch (e) { + console.error(e); + } + return this.items; } - + async store(item) { let size = this.items.size; return (size !== this.items.add(item).size) && await this.save(); @@ -74,6 +95,10 @@ class ListStore { async remove(item) { return this.items.delete(item) && await this.save(); } + + contains(item) { + return this.items.has(item); + } } module.exports = { ListStore, Storage }; diff --git a/main_background.js b/main_background.js index 093aa86..c20a257 100644 --- a/main_background.js +++ b/main_background.js @@ -177,19 +177,31 @@ function options_listener(changes, area){ } + +var active_connections = {}; +var unused_data = {}; +function createReport(initializer = null) { + let template = { + "accepted": [], + "blocked": [], + "blacklisted": [], + "whitelisted": [], + url: "", + }; + return initializer ? Object.assign(template, initializer) : template; +} + /** * Executes the "Display this report in new tab" function * by opening a new tab with whatever HTML is in the popup * at the moment. */ -var active_connections = {}; -var unused_data = {}; function open_popup_tab(data){ dbg_print(data); function gotPopup(popupURL){ var creating = webex.tabs.create({"url":popupURL},function(a){ dbg_print("[TABID:"+a["id"]+"] creating unused data entry from parent window's content"); - unused_data[a["id"]] = data; + unused_data[a["id"]] = createReport(data); }); } @@ -288,13 +300,7 @@ function update_popup(tab_id,blocked_info,update=false){ } else return false; } - new_blocked_data = { - "accepted":[], - "blocked":[], - "blacklisted":[], - "whitelisted":[], - "url": url - }; + new_blocked_data = createReport({url}); for(var type in blocked_info){ for(var script_arr in blocked_info[type]){ if(is_bl(blocked_info[type][script_arr][0])){ @@ -368,19 +374,9 @@ function add_popup_entry(tab_id,src_hash,blocked_info,update=false){ } if(unused_data[tab_id] === undefined){ - unused_data[tab_id] = { - "accepted":[], - "blocked":[], - "blacklisted":[], - "whitelisted":[], - "url": url - }; + unused_data[tab_id] = createReport({url}); } - if(unused_data[tab_id]["accepted"] === undefined){unused_data[tab_id]["accepted"] = [];} - if(unused_data[tab_id]["blocked"] === undefined){unused_data[tab_id]["blocked"] = [];} - if(unused_data[tab_id]["blacklisted"] === undefined){unused_data[tab_id]["blacklisted"] = [];} - if(unused_data[tab_id]["whitelisted"] === undefined){unused_data[tab_id]["whitelisted"] = [];} - + var type = ""; if(blocked_info["accepted"] !== undefined){ @@ -443,29 +439,33 @@ function add_popup_entry(tab_id,src_hash,blocked_info,update=false){ } var type_key = ""; var res = ""; - if(is_bl(blocked_info[type][0])){ - type_key = "blacklisted"; - res = "bl"; - //console.log("Script " + blocked_info[type][0] + " is blacklisted"); - } - else if(is_wl(blocked_info[type][0])){ - type_key = "whitelisted"; - res = "wl"; - //console.log("Script " + blocked_info[type][0] + " is whitelisted"); - } else{ - type_key = type; - res = "none"; - //console.log("Script " + blocked_info[type][0] + " isn't whitelisted or blacklisted"); - } - if(not_duplicate(type_key,blocked_info[type])){ - dbg_print(unused_data); - dbg_print(unused_data[tab_id]); - dbg_print(type_key); - unused_data[tab_id][type_key].push(blocked_info[type]); - resolve(res); - } else{ - resolve(res); + try { + if(is_bl(blocked_info[type][0])){ + type_key = "blacklisted"; + res = "bl"; + //console.log("Script " + blocked_info[type][0] + " is blacklisted"); + } + else if(is_wl(blocked_info[type][0])){ + type_key = "whitelisted"; + res = "wl"; + //console.log("Script " + blocked_info[type][0] + " is whitelisted"); + } else{ + type_key = type; + res = "none"; + //console.log("Script " + blocked_info[type][0] + " isn't whitelisted or blacklisted"); + } + + if(not_duplicate(type_key,blocked_info[type])){ + dbg_print(unused_data); + dbg_print(unused_data[tab_id]); + dbg_print(type_key); + unused_data[tab_id][type_key].push(blocked_info[type]); + resolve(res); + } + } catch (e) { + console.error(e, "blocked_info %o, type %s, type_key %s", blocked_info, type, type_key); } + resolve(res); } webex.storage.local.get(get_sto); @@ -592,7 +592,7 @@ function connected(p) { p.postMessage({"show_info":unused_data[tab_id]}); } else{ // create a new entry - unused_data[tab_id] = {"url":tab["url"],"blocked":"","accepted":""}; + unused_data[tab_id] = createReport({"url": tab.url}); p.postMessage({"show_info":unused_data[tab_id]}); dbg_print("[TABID:"+tab_id+"]"+"No data found, creating a new entry for this window."); } @@ -623,23 +623,10 @@ function delete_removed_tab_info(tab_id, remove_info){ * Check whitelisted by hash * */ -function blocked_status(hash){ - return new Promise((resolve, reject) => { - function cb(items){ - var wl = items["pref_whitelist"]; - for(var i in items){ - var res = i.match(/\(.*?\)/g); - if(res != null){ - var test_hash = res[res.length-1].substr(1,res[0].length-2); - if(test_hash == hash){ - resolve(items[i]); - } - } - } - resolve("none"); - } - webex.storage.local.get(cb); - }); +function blocked_status(hash) { + let hashItem = ListStore.hashItem(hash); + return whitelist.contains(hashItem) ? + "whitelisted" : blacklist.contains(hashItem) ? "blacklisted" : "none"; } /* *********************************************************************************************** */ @@ -947,92 +934,44 @@ function license_read(script_src, name, external = false){ // TODO: Test if this script is being loaded from another domain compared to unused_data[tabid]["url"] /** -* -* Returns a promise that resolves with the final edited script as a string. +* Asynchronous function, returns the final edited script as a string, +* or an array containing it and the index, if the latter !== -1 */ -function get_script(response,url,tabid,wl,index=-1){ - return new Promise((resolve, reject) => { - if(unused_data[tabid] === undefined){ - unused_data[tabid] = {"url":url,"accepted":[],"blocked":[]}; - } - var edited; - var tok_index = url.split("/").length; - var scriptname = url.split("/")[tok_index-1]; - if(wl == true){ - // Accept without reading script, it was explicitly whitelisted - if(typeof(unused_data[tabid]["accepted"].push) != "function"){ - unused_data[tabid]["accepted"] = [[url,"Page is whitelisted in preferences"]]; - } else{ - unused_data[tabid]["accepted"].push([url,"Page is whitelisted in preferences"]); - } - resolve("\n/*\n LibreJS: Script whitelisted by user (From a URL found in comma seperated whitelist)\n*/\n"+response); - if(index != -1){ - resolve(["\n/*\n LibreJS: Script whitelisted by user (From a URL found in comma seperated whitelist)\n*/\n"+response,index]); - } else{ - resolve("\n/*\n LibreJS: Script whitelisted by user (From a URL found in comma seperated whitelist)\n*/\n"+response); - } - edited = [true,response,"Page is whitelisted in preferences"]; - }else{ - edited = license_read(response,scriptname,index == -2); - } - var src_hash = hash(response); - var verdict = edited[0]; - var popup_res; - var domain = get_domain(url); - - var badge_str = 0; - - if(unused_data[tabid]["blocked"] !== undefined){ - badge_str += unused_data[tabid]["blocked"].length; - } - - if(unused_data[tabid]["blacklisted"] !== undefined){ - badge_str += unused_data[tabid]["blacklisted"].length; - } - dbg_print("amt. blocked on page:"+badge_str); - if(badge_str > 0 || verdict == false){ - webex.browserAction.setBadgeText({ - text: "!", - tabId: tabid - }); - webex.browserAction.setBadgeBackgroundColor({ - color: "red", - tabId: tabid - }); - } - - if(verdict == true){ - popup_res = add_popup_entry(tabid,src_hash,{"url":domain,"accepted":[url,edited[2]]}); - } else{ - popup_res = add_popup_entry(tabid,src_hash,{"url":domain,"blocked":[url,edited[2]]}); - } +async function get_script(response, url, tabId, whitelisted = false, index = -1) { + function result(scriptSource) { + return index === -1 ? scriptSource : [scriptSource, index]; + } + let report = unused_data[tabId] || (unused_data[tabId] = createReport({url})); - popup_res.then(function(list_verdict){ - var blob; - if(list_verdict == "wl"){ - // redirect to the unedited version - if(index != -1){ - resolve(["/* LibreJS: Script whitelisted by user */\n"+response,index]); - } else{ - resolve("/* LibreJS: Script whitelisted by user */\n"+response); - } - }else if(list_verdict == "bl"){ - // Blank the entire script - if(index != -1){ - resolve(["/* LibreJS: Script blacklisted by user */\n",index]); - } else{ - resolve("/* LibreJS: Script blacklisted by user */\n"); - } - } else{ - // Return the edited (normal) version - if(index != -1){ - resolve(["/* LibreJS: Script acknowledged */\n"+edited[1],index]); - } else{ - resolve("/* LibreJS: Script acknowledged */\n"+edited[1]); - } - } + let scriptName = url.split("/").pop(); + if (whitelisted) { + // Accept without reading script, it was explicitly whitelisted + report.accepted.push([url, "Page is whitelisted in preferences"]); + return result(`/* LibreJS: script whitelisted by user preference. */\n${response}`); + } + let [verdict, editedSource, reason] = license_read(response, scriptName, index === -2); + let sourceHash = hash(response); + let domain = get_domain(url); + let blockedCount = report.blocked.length + report.blacklisted.length; + dbg_print(`amt. blocked on page: ${blockedCount}`); + if (blockedCount > 0 || !verdict) { + webex.browserAction.setBadgeText({ + text: "!", + tabId }); - }); + webex.browserAction.setBadgeBackgroundColor({ + color: "red", + tabId + }); + } + let listVerdict = await add_popup_entry(tabId, sourceHash, {"url":domain, [verdict ? "accepted" : "blocked"]: [url, reason]}); + switch(listVerdict) { + case "wl": case "bl": + let verdictText = listVerdict === "wl" ? "whitelisted" : "blacklisted"; + return result(`/* LibreJS: script ${verdictText} by user. */\n${response}`); + default: + return result(`/* LibreJS: script aknowledged. */\n${editedSource}`); + } } /** @@ -1073,15 +1012,53 @@ function block_ga(a){ /** * 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 -* and external script inclusion in search of non-free JavaScript +* content type and encoding, and therefore correctly parse HTML documents +* and external script inclusions in search of non-free JavaScript */ -async function responseHandler(response) { - let {url, type} = response.request; - let whitelisted = await test_url_whitelisted(url); - let handle_it = type === "script" ? handle_script : handle_html; - return await handle_it(response, whitelisted); +var ResponseHandler = { + /** + * Enforce white/black lists for url/site early (hashes will be handled later) + */ + pre(response) { + let {request} = response; + let {url, documentUrl, type, tabId} = request; + + let site = ListStore.siteItem(url); + + let blacklistedSite = blacklist.contains(site); + let blacklisted = blacklistedSite || blacklist.contains(url); + if (blacklisted) { + if (type === "script") { + // abort the request before the response gets fetched + add_popup_entry(tabId, url, {url, "blocked": [url, "Blacklisted by user"]}); + return ResponseProcessor.REJECT; + } + // use CSP to restrict JavaScript execution in the page + request.responseHeaders.unshift({ + name: `Content-security-policy`, + value: `script-src '${blacklistedSite ? 'self' : 'none'}';` + }); + } else if ( + response.whitelisted = (whitelist.contains(site) || whitelist.contains(url)) && + type === "script") { + // accept the script and stop processing + add_popup_entry(tabId, url, {url, "accepted": [url, "Whitelisted by user"]}); + return ResponseProcessor.ACCEPT; + } + // 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 {url, type} = response.request; + let handle_it = type === "script" ? handle_script : handle_html; + return await handle_it(response, response.whitelisted); + } } /** @@ -1273,14 +1250,15 @@ async function handle_html(response, whitelisted) { return await edit_html(text, url, tabId, false); } -var pageWhitelist = new ListStore("pref_whitelist", Storage.CSV); +var whitelist = new ListStore("pref_whitelist", Storage.CSV); +var blacklist = new ListStore("pref_blacklist", Storage.CSV); /** * Initializes various add-on functions * only meant to be called once when the script starts */ async function init_addon(){ - await pageWhitelist.load(); + await whitelist.load(); set_webex(); webex.runtime.onConnect.addListener(connected); webex.storage.onChanged.addListener(options_listener); @@ -1300,48 +1278,11 @@ async function init_addon(){ ); // Analyzes all the html documents and external scripts as they're loaded - ResponseProcessor.install(responseHandler); + ResponseProcessor.install(ResponseHandler); legacy_license_lib.init(); } -/** -* Test if a page is whitelisted/blacklisted. -* -* The input here is tested against the comma seperated string found in the options. -* -* It does NOT test against the individual entries created by hitting the "whitelist" -* button for a script in the browser action. -*/ -function test_url_whitelisted(url){ - return new Promise((resolve, reject) => { - function cb(items){ - var wl = items["pref_whitelist"]; - if(wl !== undefined && wl !== ""){ - wl = wl.split(","); - } else{ - resolve(false); - return; - } - var regex; - for(var i in wl){ - var s = wl[i].replace(/\*/g,"\\S*"); - s = s.replace(/\./g,"\\."); - regex = new RegExp(s, "g"); - if(url.match(regex)){ - //console.log("%c" + wl[i] + " matched " + url,"color: purple;"); - resolve(true); - return; - } else{ - //console.log("%c" + wl[i] + " didn't match " + url,"color: #dd0000;"); - } - } - resolve(false); - return; - } - webex.storage.local.get(cb); - }); -} /** * Loads the contact finder on the given tab ID. @@ -1357,14 +1298,14 @@ function inject_contact_finder(tab_id){ * Adds given domain to the whitelist in options */ async function add_csv_whitelist(domain){ - await pageWhitelist.store(`${domain}*`); + return await whitelist.store(`${domain}*`); } /** * removes given domain from the whitelist in options */ async function remove_csv_whitelist(domain) { - return pageWhitelist.remove(`${domain}*`); + return whitelist.remove(`${domain}*`); } init_addon(); |