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();  | 
