/* A JavaScript implementation of the SHA family of hashes, as defined in FIPS PUB 180-4 and FIPS PUB 202, as well as the corresponding HMAC implementation as defined in FIPS PUB 198a Copyright Brian Turek 2008-2017 Distributed under the BSD License See http://caligatio.github.com/jsSHA/ for more information Several functions taken from Paul Johnston */ 'use strict';(function(I){function w(c,a,d){var l=0,b=[],g=0,f,n,k,e,h,q,y,p,m=!1,t=[],r=[],u,z=!1;d=d||{};f=d.encoding||"UTF8";u=d.numRounds||1;if(u!==parseInt(u,10)||1>u)throw Error("numRounds must a integer >= 1");if(0===c.lastIndexOf("SHA-",0))if(q=function(b,a){return A(b,a,c)},y=function(b,a,l,f){var g,e;if("SHA-224"===c||"SHA-256"===c)g=(a+65>>>9<<4)+15,e=16;else throw Error("Unexpected error in SHA-2 implementation");for(;b.length<=g;)b.push(0);b[a>>>5]|=128<<24-a%32;a=a+l;b[g]=a&4294967295; b[g-1]=a/4294967296|0;l=b.length;for(a=0;a>>3;g=e/4-1;if(eb/8){for(;a.length<=g;)a.push(0);a[g]&=4294967040}for(b=0;b<=g;b+=1)t[b]=a[b]^909522486,r[b]=a[b]^1549556828;n=q(t,n);l=h;m=!0};this.update=function(a){var c,f,e,d=0,p=h>>>5;c=k(a,b,g);a=c.binLen;f=c.value;c=a>>>5;for(e=0;e>> 5);g=a%h;z=!0};this.getHash=function(a,f){var d,h,k,q;if(!0===m)throw Error("Cannot call getHash after setting HMAC key");k=C(f);switch(a){case "HEX":d=function(a){return D(a,e,k)};break;case "B64":d=function(a){return E(a,e,k)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{h=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER"); }q=y(b.slice(),g,l,p(n));for(h=1;h>>2]>>>8*(3+b%4*-1),l+="0123456789abcdef".charAt(g>>>4&15)+"0123456789abcdef".charAt(g&15);return d.outputUpper?l.toUpperCase():l}function E(c,a,d){var l="",b=a/8,g,f,n;for(g=0;g>>2]:0,n=g+2>>2]:0,n=(c[g>>>2]>>>8*(3+g%4*-1)&255)<<16|(f>>>8*(3+(g+1)%4*-1)&255)<<8|n>>>8*(3+(g+2)%4*-1)&255,f=0;4>f;f+=1)8*g+6*f<=a?l+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(n>>> 6*(3-f)&63):l+=d.b64Pad;return l}function F(c,a){var d="",l=a/8,b,g;for(b=0;b>>2]>>>8*(3+b%4*-1)&255,d+=String.fromCharCode(g);return d}function G(c,a){var d=a/8,l,b=new ArrayBuffer(d),g;g=new Uint8Array(b);for(l=0;l>>2]>>>8*(3+l%4*-1)&255;return b}function C(c){var a={outputUpper:!1,b64Pad:"=",shakeLen:-1};c=c||{};a.outputUpper=c.outputUpper||!1;!0===c.hasOwnProperty("b64Pad")&&(a.b64Pad=c.b64Pad);if("boolean"!==typeof a.outputUpper)throw Error("Invalid outputUpper formatting option"); if("string"!==typeof a.b64Pad)throw Error("Invalid b64Pad formatting option");return a}function B(c,a){var d;switch(a){case "UTF8":case "UTF16BE":case "UTF16LE":break;default:throw Error("encoding must be UTF8, UTF16BE, or UTF16LE");}switch(c){case "HEX":d=function(a,b,c){var f=a.length,d,k,e,h,q;if(0!==f%2)throw Error("String of HEX type must be in byte increments");b=b||[0];c=c||0;q=c>>>3;for(d=0;d>>1)+q;for(e=h>>>2;b.length<=e;)b.push(0);b[e]|=k<<8*(3+h%4*-1)}return{value:b,binLen:4*f+c}};break;case "TEXT":d=function(c,b,d){var f,n,k=0,e,h,q,m,p,r;b=b||[0];d=d||0;q=d>>>3;if("UTF8"===a)for(r=3,e=0;ef?n.push(f):2048>f?(n.push(192|f>>>6),n.push(128|f&63)):55296>f||57344<=f?n.push(224|f>>>12,128|f>>>6&63,128|f&63):(e+=1,f=65536+((f&1023)<<10|c.charCodeAt(e)&1023),n.push(240|f>>>18,128|f>>>12&63,128|f>>>6&63,128|f&63)),h=0;h>>2;b.length<=m;)b.push(0);b[m]|=n[h]<<8*(r+p%4*-1);k+=1}else if("UTF16BE"===a||"UTF16LE"===a)for(r=2,n="UTF16LE"===a&&!0||"UTF16LE"!==a&&!1,e=0;e>>8);p=k+q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=f<<8*(r+p%4*-1);k+=2}return{value:b,binLen:8*k+d}};break;case "B64":d=function(a,b,c){var f=0,d,k,e,h,q,m,p;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");k=a.indexOf("=");a=a.replace(/\=/g, "");if(-1!==k&&k { var new_blocked_data; // Make sure the entry in unused_data exists var url = blocked_info["url"]; if(url === undefined){ console.error("No url passed to update_popup"); return 1; } if(unused_data[tab_id] === undefined){ unused_data[tab_id] = { "accepted":[], "blocked":[], "blacklisted":[], "whitelisted":[], "url": 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){ type = "accepted"; } if(blocked_info["blocked"] !== undefined){ type = "blocked"; } function get_sto(items){ function get_status(script_name,src_hash){ var temp = script_name.match(/\(.*?\)/g); if(temp == null){ return "none" } var src_hash = temp[temp.length-1].substr(1,temp[0].length-2); 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 == src_hash){ return items[i]; } } } if(default_whitelist[src_hash] !== undefined){ //console.log("Found script in default whitelist: "+default_whitelist[src_hash]); return "whitelist"; } else{ //console.log("script " + script_name + " not in default whitelist."); } return "none"; } function is_bl(script_name){ if(get_status(script_name) == "blacklist"){ return true; } return false; } function is_wl(script_name){ if(get_status(script_name) == "whitelist"){ return true; } return false; } // Search unused data for the given entry function not_duplicate(entry,key){ var flag = true; for(var i = 0; i < unused_data[tab_id][entry].length; i++){ if(unused_data[tab_id][entry][i][0] == key[0]){ flag = false; } } return flag; } 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])){ unused_data[tab_id][type_key].push(blocked_info[type]); resolve(res); } else{ resolve(res); } } webex.storage.local.get(get_sto); }); } function get_domain(url){ var domain = url.replace('http://','').replace('https://','').split(/[/?#]/)[0]; if(url.indexOf("http://") == 0){ domain = "http://" + domain; } else if(url.indexOf("https://") == 0){ domain = "https://" + domain; } domain = domain + "/"; domain = domain.replace(/ /g,""); return domain; } /** * * This is the callback where the content scripts of the browser action will contact the background script. * */ var portFromCS; function connected(p) { if(p["name"] == "contact_finder"){ // Send a message back with the relevant settings function cb(items){ p.postMessage(items); } webex.storage.local.get(cb); return; } p.onMessage.addListener(function(m) { /** * Updates the entry of the current URL in storage */ function set_script(script,val){ if(val != "whitelist" && val != "forget" && val != "blacklist"){ console.error("Key must be either 'whitelist', 'blacklist' or 'forget'"); } // (Remember that we do not trust the names of scripts.) var current_url = ""; function geturl(tabs) { current_url = tabs[0]["url"]; var domain = get_domain(current_url); // The space char is a valid delimiter because encodeURI() replaces it with %20 var scriptkey = m[val][0]; if(val == "forget"){ var prom = webex.storage.local.remove(scriptkey); // TODO: This should produce a "Refresh the page for this change to take effect" message } else{ var newitem = {}; newitem[scriptkey] = val; webex.storage.local.set(newitem); } } var querying = webex.tabs.query({active: true,currentWindow: true},geturl); return; } var update = false; var contact_finder = false; if(m["whitelist"] !== undefined){ set_script(m["whitelist"][0],"whitelist"); update = true; } if(m["blacklist"] !== undefined){ set_script(m["blacklist"][0],"blacklist"); update = true; } if(m["forget"] !== undefined){ set_script(m["forget"][0],"forget"); update = true; } // if(m["open_popup_tab"] !== undefined){ open_popup_tab(m["open_popup_tab"]); } // a debug feature if(m["printlocalstorage"] !== undefined){ debug_print_local(); } // invoke_contact_finder if(m["invoke_contact_finder"] !== undefined){ contact_finder = true; inject_contact_finder(); } // a debug feature (maybe give the user an option to do this?) if(m["deletelocalstorage"] !== undefined){ debug_delete_local(); } // Add this domain to the whitelist if(m["allow_all"] !== undefined){ var domain = get_domain(m["allow_all"]["url"]); add_csv_whitelist(domain); } // Remote this domain from the whitelist if(m["block_all"] !== undefined){ var domain = get_domain(m["block_all"]["url"]); remove_csv_whitelist(domain); } function logTabs(tabs) { if(contact_finder){ console.log("[TABID:"+tab_id+"] Injecting contact finder"); //inject_contact_finder(tabs[0]["id"]); } if(update){ console.log("%c updating tab "+tabs[0]["id"],"color: red;"); update_popup(tabs[0]["id"],unused_data[tabs[0]["id"]],true); active_connections[tabs[0]["id"]] = p; } for(var i = 0; i < tabs.length; i++) { var tab = tabs[i]; var tab_id = tab["id"]; if(unused_data[tab_id] !== undefined){ // If we have some data stored here for this tabID, send it console.log("[TABID:"+tab_id+"]"+"Sending stored data associated with browser action"); p.postMessage({"show_info":unused_data[tab_id]}); } else{ // create a new entry unused_data[tab_id] = {"url":tab["url"],"blocked":"","accepted":""}; p.postMessage({"show_info":unused_data[tab_id]}); console.log("[TABID:"+tab_id+"]"+"No data found, creating a new entry for this window."); } } } var querying = webex.tabs.query({active: true,currentWindow: true},logTabs); }); } /** * The callback for tab closings. * * Delete the info we are storing about this tab if there is any. * */ function delete_removed_tab_info(tab_id, remove_info){ console.log("[TABID:"+tab_id+"]"+"Deleting stored info about closed tab"); if(unused_data[tab_id] !== undefined){ delete unused_data[tab_id]; } if(active_connections[tab_id] !== undefined){ delete active_connections[tab_id]; } } /** * Makes it so we can return redirect requests to local blob URLs * * TODO: Make it so that it adds the website itself to the permissions of all keys * TODO: Is this neccessary now that we aren't loading blob URLs? */ function change_csp(e) { var index = 0; var csp_header = ""; /* for(var i = 0; i < e["responseHeaders"].length; i++){ console.log("%c"+e["responseHeaders"][i]["name"],"color:white"); console.log(e["responseHeaders"][i]["value"]); } console.log("done"); */ for(var i = 0; i < e["responseHeaders"].length; i++){ if(e["responseHeaders"][i]["name"].toLowerCase() == "content-security-policy"){ csp_header = e["responseHeaders"][i]["value"]; index = i; var keywords = csp_header.replace(/;/g,'","'); keywords = JSON.parse('["' + keywords.substr(0,keywords.length) + '"]'); // Iterates over the keywords inside the CSP header for(var j = 0; j < keywords.length; j++){ var matchres = keywords[j].match(/[\-\w]+/g); if(matchres != null && matchres[0] == "script-src"){ // Test to see if they have a hash and then delete it // TODO: Make sure this is a good idea. keywords[j] = keywords[j].replace(/\s?'sha256-[\w+/]+=+'/g,""); keywords[j] = keywords[j].replace(/\s?'sha384-[\w+/]+=+'/g,""); keywords[j] = keywords[j].replace(/\s?'sha512-[\w+/]+=+'/g,""); keywords[j] = keywords[j].replace(/'strict-dynamic'/g,""); keywords[j] = keywords[j].replace(/;/g,""); // This is the string that we add to every CSP keywords[j] += "'self' data: blob: 'report-sample'"; //console.log("%c new script-src section:","color:green;") //console.log(keywords[j]+ "; "); } } var csp_header = ""; for(var j = 0; j < keywords.length; j++){ csp_header = csp_header + keywords[j] + "; "; } e["responseHeaders"][i]["value"] = csp_header; } } if(csp_header == ""){ //console.log("%c no CSP.","color: red;"); }else{ //console.log("%c new CSP:","color: green;"); //console.log(e["responseHeaders"][index]["value"]); } return {responseHeaders: e.responseHeaders}; } /** * Turns a blob URL into a data URL * */ function get_data_url(blob,url){ return new Promise((resolve, reject) => { //var url = URL.createObjectURL(blob); var reader = new FileReader(); reader.addEventListener("load", function(){ //console.log("redirecting"); //console.log(url); //console.log("to"); //console.log(reader.result); resolve({"redirectUrl": reader.result}); }); reader.readAsDataURL(blob); }); } /** * 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"); return; } webex.storage.local.get(cb); }); } /* *********************************************************************************************** */ // (This is part of eval_test.js with a few console.logs/comments removed) function evaluate(script,name){ function reserved_object_regex(object){ var arith_operators = "\\+\\-\\*\\/\\%\\="; var scope_chars = "\{\}\]\[\(\)\,"; var trailing_chars = "\s*"+"\(\.\["; return new RegExp("(?:[^\\w\\d]|^|(?:"+arith_operators+"))"+object+'(?:\\s*?(?:[\\;\\,\\.\\(\\[])\\s*?)',"g"); } reserved_object_regex("window"); var all_strings = new RegExp('".*?"'+"|'.*?'","gm"); var ml_comment = /\/\*([\s\S]+?)\*\//g; var il_comment = /\/\/.+/gm; var bracket_pairs = /\[.+?\]/g; var temp = script.replace(/'.+?'+/gm,"'string'"); temp = temp.replace(/".+?"+/gm,'"string"'); temp = temp.replace(ml_comment,""); temp = temp.replace(il_comment,""); console.log("------evaluation results for "+ name +"------"); console.log("Script accesses reserved objects?"); var flag = true; var reason = "" // This is where individual "passes" are made over the code for(var i = 0; i < reserved_objects.length; i++){ var res = reserved_object_regex(reserved_objects[i]).exec(script); if(res != null){ console.log("%c fail","color:red;"); flag = false; reason = "Script uses a reserved object (" + reserved_objects[i] + ")"; } } if(flag){ console.log("%c pass","color:green;"); } // If flag is set true at this point, the script is trivial if(flag){ reason = "Script was determined to be trivial."; } return [flag,reason+"
"]; } function license_valid(matches){ if(matches.length != 4){ return [false, "malformed or unrecognized license tag"]; } if(matches[1] != "@license"){ return [false, "malformed or unrecognized license tag"]; } if(licenses[matches[3]] === undefined){ return [false, "malformed or unrecognized license tag"]; } if(licenses[matches[3]]["Magnet link"] != matches[2]){ return [false, "malformed or unrecognized license tag"]; } return [true,"Recognized license as '"+matches[3]+"'
"]; } /** * * Evaluates the content of a script (license, if it is non-trivial) * * Returns * [ * true (accepted) or false (denied), * edited content, * reason text * ] */ function license_read(script_src,name){ var reason_text = ""; var edited_src = ""; var unedited_src = script_src; var nontrivial_status; var parts_denied = false; var parts_accepted = false; while(true){ // TODO: support multiline comments var matches = /\/\s*?(@license)\s([\S]+)\s([\S]+$)/gm.exec(unedited_src); if(matches == null){ nontrivial_status = evaluate(unedited_src,name); if(nontrivial_status[0] == true){ parts_accepted = true; edited_src += unedited_src; } else{ parts_denied = true; edited_src += "\n/*\nLIBREJS BLOCKED:"+nontrivial_status[1]+"\n*/\n"; } reason_text += "\n" + nontrivial_status[1]; if(parts_denied == true && parts_accepted == true){ reason_text = "Script was determined partly non-trivial after editing. (check source for details)\n"+reason_text; } if(parts_denied == true && parts_accepted == false){ return [false,edited_src,reason_text]; } return [true,edited_src,reason_text]; } onsole.log("Found a license tag"); var before = unedited_src.substr(0,matches["index"]); nontrivial_status = evaluate(before,name); if(nontrivial_status[0] == true){ parts_accepted = true; edited_src += before; } else{ parts_denied = true; edited_src += "\n/*\nLIBREJS BLOCKED:"+nontrivial_status[1]+"\n*/\n"; } unedited_src = unedited_src.substr(matches["index"],unedited_src.length); // TODO: support multiline comments var matches_end = /\/\/\s*?(@license-end)/gm.exec(unedited_src); if(matches_end == null){ console.log("ERROR: @license with no @license-end"); return [false,"\n/*\n ERROR: @license with no @license-end \n*/\n","ERROR: @license with no @license-end"]; } var endtag_end_index = matches_end["index"]+matches_end[0].length; var license_res = license_valid(matches); if(license_res[0] == true){ edited_src = edited_src + unedited_src.substr(0,endtag_end_index); reason_text += "\n" + license_res[1]; } else{ edited_src = edited_src + "\n/*\n"+license_res[1]+"\n*/\n"; reason_text += "\n" + license_res[1]; } // trim off everything we just evaluated unedited_src = unedited_src.substr(endtag_end_index,unedited_src.length); } } /* *********************************************************************************************** */ // 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. */ 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 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"] = [[scriptname,"Page is whitelisted in preferences"]]; } else{ unused_data[tabid]["accepted"].push([scriptname,"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); } } var src_hash = hash(response); var edited = license_read(response,scriptname); var verdict = edited[0]; var popup_res; var domain = get_domain(url); //var badge_str = unused_data[tabid]["blocked"].length + unused_data[tabid]["blacklisted"].length; 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; } console.log("amt. blocked on page:"+badge_str); if(badge_str > 0){ webex.browserAction.setBadgeText({ text: " ", tabId: tabid }); webex.browserAction.setBadgeBackgroundColor({ color: "red", tabId: tabid }); } else{ webex.browserAction.setBadgeText({ text: " ", tabId: tabid }); webex.browserAction.setBadgeBackgroundColor({ color: "green", tabId: tabid }); } if(verdict == true){ popup_res = add_popup_entry(tabid,src_hash,{"url":domain,"accepted":[scriptname+" ("+src_hash+")",edited[2]]}); } else{ popup_res = add_popup_entry(tabid,src_hash,{"url":domain,"blocked":[scriptname+" ("+src_hash+")",edited[2]]}); } 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]); } } }); }); } function read_script(a){ var filter = webex.webRequest.filterResponseData(a.requestId); var decoder = new TextDecoder("utf-8"); var encoder = new TextEncoder(); // TODO: make sure this doesn't cause undeclared decoding filter.ondata = event => { var str = decoder.decode(event.data, {stream: true}); var res = test_url_whitelisted(a.url); res.then(function(whitelisted){ var edit_script; if(whitelisted == true){ // Doesn't matter if this is accepted or blocked, it will still be whitelisted edit_script = get_script(str,a.url,a["tabId"],true); } else{ edit_script = get_script(str,a.url,a["tabId"],false); } edit_script.then(function(edited){ filter.write(encoder.encode(edited)); filter.disconnect(); }); }); } return {}; } function source_replace(html,replace_this,with_this){ // This is absolutely neccessary. Not sure if it can cause bugs. html = html.replace(/\r\n|\r|\n/g,"\n"); replace_this = replace_this.replace(/\r\n|\r|\n/g,"\n"); // Get all match indices var match_indices = []; var watchdog = 0; var lower_index = 0; while(true){ var match_index = html.indexOf(replace_this,lower_index); if(match_index == -1){ break; } else{ match_indices.push(lower_index + match_index); lower_index += match_index + 1; } watchdog++; if(watchdog == 50){ debugger; return; } } function print_substr(orig,a,str){ //console.log(a+","+(a+str.length)); console.log(orig.substring(a+str.length,a)); } if(match_indices.length == 0){ console.log("%c No matches","color:red;"); debugger; } if(match_indices.length == 1){ console.log("%c Matched","color:green;"); html = html.replace(replace_this,with_this); } if(match_indices.length > 1){ console.log("%c Multiple matches","color:orange;"); } // for(var i = 0; i < match_indices.length; i++){ // console.log(match_indices[i]); // print_substr(html,match_indices[i],replace_this); // } return html; } //source_replace("ababababasdasdasbabababaabababdab","ab","xx"); function remove_noscripts(html){ // Remove noscript tags with name "librejs-path" // This leaves a trailing tag (shouldn't be a big deal) and also is vulnerable to a (harmless) injection return html; } function edit_html(html,url,tabid,wl){ return new Promise((resolve, reject) => { if(wl == true){ // Don't bother, page is whitelisted resolve(html); } var parser = new DOMParser(); var html_doc = parser.parseFromString(html, "text/html"); var amt_scripts = 0; var total_scripts = 0; var scripts = html_doc.scripts; for(var i = 0; i < scripts.length; i++){ if(scripts[i].src == ""){ total_scripts++; } } console.log("Analyzing "+total_scripts+" inline scripts..."); for(var i = 0; i < scripts.length; i++){ if(scripts[i].src == ""){ var edit_script = get_script(scripts[i].innerHTML,url,tabid,wl,i); edit_script.then(function(edited){ console.groupCollapsed("%c ------ not remote (document.scripts["+edited[1]+"]) ------","color:white"); var edited_source = edited[0]; var unedited_source = html_doc.scripts[edited[1]].innerHTML.trim(); html = source_replace(html,unedited_source,edited_source); html_doc.scripts[edited[1]].innerHTML = edited_source; amt_scripts++; console.groupEnd(); if(amt_scripts == total_scripts){ //resolve(remove_noscripts(html_doc.documentElement.innerHTML)); resolve(remove_noscripts(html)); } }); } } if(total_scripts == 0){ console.log("Nothing to analyze."); //resolve(remove_noscripts(html_doc.documentElement.innerHTML)); resolve(remove_noscripts(html)); } }); } function read_document(a){ var filter = webex.webRequest.filterResponseData(a.requestId); var decoder = new TextDecoder("utf-8"); var encoder = new TextEncoder(); // TODO: make sure this doesn't cause undeclared decoding filter.ondata = event => { var str = decoder.decode(event.data, {stream: true}); var res = test_url_whitelisted(a.url); res.then(function(whitelisted){ var edit_page; if(whitelisted == true){ // Doesn't matter if this is accepted or blocked, it will still be whitelisted filter.write(encoder.encode(str)); filter.disconnect(); return; } else{ edit_page = edit_html(str,a.url,a["tabId"],false); edit_page.then(function(edited){ filter.write(encoder.encode(edited)); filter.disconnect(); }); } }); } return {}; } /** * Initializes various add-on functions * only meant to be called once when the script starts */ function init_addon(){ set_webex(); webex.runtime.onConnect.addListener(connected); webex.storage.onChanged.addListener(options_listener); webex.tabs.onRemoved.addListener(delete_removed_tab_info); var targetPage = "https://developer.mozilla.org/en-US/Firefox/Developer_Edition"; // Updates the content security policy so we can redirect to local URLs webex.webRequest.onHeadersReceived.addListener( change_csp, {urls: [""]}, ["blocking", "responseHeaders"] ); // Analyzes remote scripts webex.webRequest.onBeforeRequest.addListener( read_script, {urls:[""], types:["script"]}, ["blocking"] ); // Analyzes the scripts inside of HTML webex.webRequest.onBeforeRequest.addListener( read_document, {urls:[""], types:["main_frame"]}, ["blocking"] ); } /** * 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. */ function inject_contact_finder(tab_id){ function executed(result) { console.log("[TABID:"+tab_id+"]"+"finished executing contact finder: " + result); } var executing = webex.tabs.executeScript(tab_id, {file: "/contact_finder.js"}, executed); } /** * Adds given domain to the whitelist in options */ function add_csv_whitelist(domain){ function storage_got(items){ if(items["pref_whitelist"] == ""){ items["pref_whitelist"] = domain + "*"; } else if(items["pref_whitelist"] == "undefined"){ items["pref_whitelist"] = domain + "*"; } else{ items["pref_whitelist"] += "," + domain + "*"; } console.log("New CSV whitelist:"); console.log(items["pref_whitelist"]); webex.storage.local.set({"pref_whitelist":items["pref_whitelist"]}); } webex.storage.local.get(storage_got); } /** * removes given domain from the whitelist in options */ function remove_csv_whitelist(domain){ function storage_got(items){ if(items["pref_whitelist"] != ""){ domain = domain + "\\*"; domain.replace(/\./g,"\."); // remove domain console.log(new RegExp(domain,"g")); items["pref_whitelist"] = items["pref_whitelist"].replace(new RegExp(domain,"g"),"") // if an entry was deleted, it will leave an extra comma items["pref_whitelist"] = items["pref_whitelist"].replace(/,+/g,","); // remove trailing comma if the last one was deleted if(items["pref_whitelist"].charAt(items["pref_whitelist"].length-1) == ","){ items["pref_whitelist"] = items["pref_whitelist"].substr(0,items["pref_whitelist"].length-2); } } console.log("New CSV whitelist:"); console.log(items["pref_whitelist"]); webex.storage.local.set({"pref_whitelist":items["pref_whitelist"]}); } webex.storage.local.get(storage_got); } init_addon();