/** * GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. * * * Copyright (C) 2017, 2018 Nathan Nichols * Copyright (C) 2018 Ruben Rodriguez * * 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 . */ var acorn = require('acorn'); var acornLoose = require('acorn-loose'); var jssha = require('jssha'); var legacy_license_lib = require("./legacy_license_check.js"); var {ResponseProcessor} = require("./bg/ResponseProcessor"); var {Storage, ListStore} = require("./common/Storage"); var {ListManager} = require("./bg/ListManager"); var {ExternalLicenses} = require("./bg/ExternalLicenses"); console.log("main_background.js"); /** * If this is true, it evaluates entire scripts instead of returning as soon as it encounters a violation. * * Also, it controls whether or not this part of the code logs to the console. * */ var DEBUG = false; // debug the JS evaluation var PRINT_DEBUG = false; // Everything else var time = Date.now(); function dbg_print(a,b){ if(PRINT_DEBUG == true){ console.log("Time spent so far: " + (Date.now() - time)/1000 + " seconds"); if(b === undefined){ console.log(a); } else{ console.log(a,b); } } } /** * Wrapper around crypto lib * */ function hash(source){ var shaObj = new jssha("SHA-256","TEXT") shaObj.update(source); return shaObj.getHash("HEX"); } /* NONTRIVIAL THINGS: - Fetch - XMLhttpRequest - eval() - ? JAVASCRIPT CAN BE FOUND IN: - Event handlers (onclick, onload, onsubmit, etc.) - - WAYS TO DETERMINE PASS/FAIL: - "// @license [magnet link] [identifier]" then "// @license-end" (may also use /* comments) - Automatic whitelist: (http://bzr.savannah.gnu.org/lh/librejs/dev/annotate/head:/data/script_libraries/script-libraries.json_ */ var licenses = require("./licenses.json").licenses; // These are objects that it will search for in an initial regex pass over non-free scripts. var reserved_objects = [ //"document", //"window", "fetch", "XMLHttpRequest", "chrome", // only on chrome "browser", // only on firefox "eval" ]; // Generates JSON key for local storage function get_storage_key(script_name,src_hash){ return script_name; } /* * * Called when something changes the persistent data of the add-on. * * The only things that should need to change this data are: * a) The "Whitelist this page" button * b) The options screen * * When the actual blocking is implemented, this will need to comminicate * with its code to update accordingly * */ function options_listener(changes, area){ // The cache must be flushed when settings are changed // TODO: See if this can be minimized function flushed(){ dbg_print("cache flushed"); } //var flushingCache = browser.webRequest.handlerBehaviorChanged(flushed); dbg_print("Items updated in area" + area +": "); var changedItems = Object.keys(changes); var changed_items = ""; for (var i = 0; i < changedItems.length; i++){ var item = changedItems[i]; changed_items += item + ","; } dbg_print(changed_items); } var activeMessagePorts = {}; var activityReports = {}; async function createReport(initializer) { if (!(initializer && (initializer.url || initializer.tabId))) { throw new Error("createReport() needs an URL or a tabId at least"); } let template = { "accepted": [], "blocked": [], "blacklisted": [], "whitelisted": [], "unknown": [], }; template = Object.assign(template, initializer); let [url] = (template.url || (await browser.tabs.get(initializer.tabId)).url).split("#"); template.url = url; template.site = ListStore.siteItem(url); template.siteStatus = listManager.getStatus(template.site); return 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. */ async function openReportInTab(data) { let popupURL = await browser.browserAction.getPopup({}); let tab = await browser.tabs.create({url: `${popupURL}#fromTab=${data.tabId}`}); activityReports[tab.id] = await createReport(data); } /** * * Clears local storage (the persistent data) * */ function debug_delete_local(){ browser.storage.local.clear(); dbg_print("Local storage cleared"); } /** * * Prints local storage (the persistent data) as well as the temporary popup object * */ function debug_print_local(){ function storage_got(items){ console.log("%c Local storage: ", 'color: red;'); for(var i in items){ console.log("%c "+i+" = "+items[i], 'color: blue;'); } } console.log("%c Variable 'activityReports': ", 'color: red;'); console.log(activityReports); browser.storage.local.get(storage_got); } /** * * * Sends a message to the content script that sets the popup entries for a tab. * * var example_blocked_info = { * "accepted": [["REASON 1","SOURCE 1"],["REASON 2","SOURCE 2"]], * "blocked": [["REASON 1","SOURCE 1"],["REASON 2","SOURCE 2"]], * "url": "example.com" * } * * NOTE: This WILL break if you provide inconsistent URLs to it. * Make sure it will use the right URL when refering to a certain script. * */ async function updateReport(tabId, oldReport, updateUI = false){ let {url} = oldReport; let newReport = await createReport({url, tabId}); for (let property of Object.keys(oldReport)) { let entries = oldReport[property]; if (!Array.isArray(entries)) continue; let defValue = property === "accepted" || property === "blocked" ? property : "unknown"; for (let script of entries) { let status = listManager.getStatus(script[0], defValue); if (Array.isArray(newReport[status])) newReport[status].push(script); } } activityReports[tabId] = newReport; if (browser.sessions) browser.sessions.setTabValue(tabId, url, newReport); dbg_print(newReport); if (updateUI && activeMessagePorts[tabId]) { dbg_print(`[TABID: ${tabId}] Sending script blocking report directly to browser action.`); activeMessagePorts[tabId].postMessage({show_info: newReport}); } } /** * * This is what you call when a page gets changed to update the info box. * * Sends a message to the content script that adds a popup entry for a tab. * * The action argument is an object with two properties: one named either * "accepted","blocked", "whitelisted", "blacklisted" or "unknown", whose value * is the array [scriptName, reason], and another named "url". Example: * action = { * "accepted": ["jquery.js (someHash)","Whitelisted by user"], * "url": "https://example.com/js/jquery.js" * } * * Returns either "whitelisted, "blacklisted", "blocked", "accepted" or "unknown" * * NOTE: This WILL break if you provide inconsistent URLs to it. * Make sure it will use the right URL when refering to a certain script. * */ async function addReportEntry(tabId, scriptHashOrUrl, action) { let report = activityReports[tabId]; if (!report) report = activityReports[tabId] = await createReport({tabId}); let type, actionValue; for (type of ["accepted", "blocked", "whitelisted", "blacklisted"]) { if (type in action) { actionValue = action[type]; break; } } if (!actionValue) { console.debug("Something wrong with action", action); return ""; } // Search unused data for the given entry function isNew(entries, item) { for (let e of entries) { if (e[0] === item) return false; } return true; } let entryType; let scriptName = actionValue[0]; try { entryType = listManager.getStatus(scriptName, type); let entries = report[entryType]; if(isNew(entries, scriptName)){ dbg_print(activityReports); dbg_print(activityReports[tabId]); dbg_print(entryType); entries.push(actionValue); } } catch (e) { console.error("action %o, type %s, entryType %s", action, type, entryType, e); entryType = "unknown"; } if (activeMessagePorts[tabId]) { try { activeMessagePorts[tabId].postMessage({show_info: report}); } catch(e) { } } if (browser.sessions) browser.sessions.setTabValue(tabId, report.url, report); updateBadge(tabId, report); return entryType; } 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; async function connected(p) { if(p.name === "contact_finder"){ // style the contact finder panel await browser.tabs.insertCSS(p.sender.tab.id, { file: "/content/dialog.css", cssOrigin: "user", matchAboutBlank: true, allFrames: true }); // Send a message back with the relevant settings p.postMessage(await browser.storage.local.get(["prefs_subject", "prefs_body"])); return; } p.onMessage.addListener(async function(m) { var update = false; var contact_finder = false; for (let action of ["whitelist", "blacklist", "forget"]) { if (m[action]) { let [key] = m[action]; if (m.site) { key = ListStore.siteItem(key); } else { key = ListStore.viewSourceItem(key) || key; } await listManager[action](key); update = true; } } if(m.report_tab){ openReportInTab(m.report_tab); } // a debug feature if(m["printlocalstorage"] !== undefined){ console.log("Print local storage"); debug_print_local(); } // invoke_contact_finder if(m["invoke_contact_finder"] !== undefined){ contact_finder = true; await injectContactFinder(); } // a debug feature (maybe give the user an option to do this?) if(m["deletelocalstorage"] !== undefined){ console.log("Delete local storage"); debug_delete_local(); } let tabs = await browser.tabs.query({active: true, currentWindow: true}); if(contact_finder){ let tab = tabs.pop(); dbg_print(`[TABID:${tab.id}] Injecting contact finder`); //inject_contact_finder(tabs[0]["id"]); } if (update || m.update && activityReports[m.tabId]) { let tabId = "tabId" in m ? m.tabId : tabs.pop().id; dbg_print(`%c updating tab ${tabId}`, "color: red;"); activeMessagePorts[tabId] = p; await updateReport(tabId, activityReports[tabId], true); } else { for(let tab of tabs) { if(activityReports[tab.id]){ // If we have some data stored here for this tabID, send it dbg_print(`[TABID: ${tab.id}] Sending stored data associated with browser action'`); p.postMessage({"show_info": activityReports[tab.id]}); } else{ // create a new entry let report = activityReports[tab.id] = await createReport({"url": tab.url, tabId: tab.id}); p.postMessage({show_info: report}); dbg_print(`[TABID: ${tab.id}] No data found, creating a new entry for this window.`); } } } }); } /** * 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){ dbg_print("[TABID:"+tab_id+"]"+"Deleting stored info about closed tab"); if(activityReports[tab_id] !== undefined){ delete activityReports[tab_id]; } if(activeMessagePorts[tab_id] !== undefined){ delete activeMessagePorts[tab_id]; } ExternalLicenses.purgeCache(tab_id); } /** * Called when the tab gets updated / activated * * Here we check if new tab's url matches activityReports[tabId].url, and if * it doesn't we use the session cached value (if any). * */ async function onTabUpdated(tabId, changedInfo, tab) { let [url] = tab.url.split("#"); let report = activityReports[tabId]; if (!(report && report.url === url)) { let cache = browser.sessions && await browser.sessions.getTabValue(tabId, url) || null; // on session restore tabIds may change if (cache && cache.tabId !== tabId) cache.tabId = tabId; updateBadge(tabId, activityReports[tabId] = cache); } } async function onTabActivated({tabId}) { await onTabUpdated(tabId, {}, await browser.tabs.get(tabId)); } /* *********************************************************************************************** */ var fname_data = require("./fname_data.json").fname_data; //************************this part can be tested in the HTML file index.html's script test.js**************************** function full_evaluate(script){ var res = true; if(script === undefined || script == ""){ return [true,"Harmless null script"]; } var ast = acornLoose.parse(script).body[0]; var flag = false; var amtloops = 0; var loopkeys = {"for":true,"if":true,"while":true,"switch":true}; var operators = {"||":true,"&&":true,"=":true,"==":true,"++":true,"--":true,"+=":true,"-=":true,"*":true}; try{ var tokens = acorn.tokenizer(script); }catch(e){ console.warn("Tokenizer could not be initiated (probably invalid code)"); return [false,"Tokenizer could not be initiated (probably invalid code)"]; } try{ var toke = tokens.getToken(); }catch(e){ console.log(script); console.log(e); console.warn("couldn't get first token (probably invalid code)"); console.warn("Continuing evaluation"); } /** * Given the end of an identifer token, it tests for bracket suffix notation */ function being_called(end){ var i = 0; while(script.charAt(end+i).match(/\s/g) !== null){ i++; if(i >= script.length-1){ return false; } } return script.charAt(end+i) == "("; } /** * Given the end of an identifer token, it tests for parentheses */ function is_bsn(end){ var i = 0; while(script.charAt(end+i).match(/\s/g) !== null){ i++; if(i >= script.length-1){ return false; } } return script.charAt(end+i) == "["; } var error_count = 0; var defines_functions = false; while(toke !== undefined && toke.type != acorn.tokTypes.eof){ if(toke.type.keyword !== undefined){ //dbg_print("Keyword:"); //dbg_print(toke); // This type of loop detection ignores functional loop alternatives and ternary operators if(toke.type.keyword == "function"){ dbg_print("%c NOTICE: Function declaration.","color:green"); defines_functions = true; } if(loopkeys[toke.type.keyword] !== undefined){ amtloops++; if(amtloops > 3){ dbg_print("%c NONTRIVIAL: Too many loops/conditionals.","color:red"); if(DEBUG == false){ return [false,"NONTRIVIAL: Too many loops/conditionals."]; } } } }else if(toke.value !== undefined && operators[toke.value] !== undefined){ // It's just an operator. Javascript doesn't have operator overloading so it must be some // kind of primitive (I.e. a number) }else if(toke.value !== undefined){ var status = fname_data[toke.value]; if(status === true){ // is the identifier banned? dbg_print("%c NONTRIVIAL: nontrivial token: '"+toke.value+"'","color:red"); if(DEBUG == false){ return [false,"NONTRIVIAL: nontrivial token: '"+toke.value+"'"]; } }else if(status === false){// is the identifier not banned? // Is there bracket suffix notation? if(is_bsn(toke.end)){ dbg_print("%c NONTRIVIAL: Bracket suffix notation on variable '"+toke.value+"'","color:red"); if(DEBUG == false){ return [false,"%c NONTRIVIAL: Bracket suffix notation on variable '"+toke.value+"'"]; } } }else if(status === undefined){// is the identifier user defined? // Is there bracket suffix notation? if(is_bsn(toke.end)){ dbg_print("%c NONTRIVIAL: Bracket suffix notation on variable '"+toke.value+"'","color:red"); if(DEBUG == false){ return [false,"NONTRIVIAL: Bracket suffix notation on variable '"+toke.value+"'"]; } } }else{ dbg_print("trivial token:"+toke.value); } } // If not a keyword or an identifier it's some kind of operator, field parenthesis, brackets try{ toke = tokens.getToken(); }catch(e){ dbg_print("Denied script because it cannot be parsed."); return [false,"NONTRIVIAL: Cannot be parsed. This could mean it is a 404 error."]; } } dbg_print("%cAppears to be trivial.","color:green;"); if (defines_functions === true) return [true,"Script appears to be trivial but defines functions."]; else return [true,"Script appears to be trivial."]; } //**************************************************************************************************** /** * This is the entry point for full code evaluation. * * Performs the initial pass on code to see if it needs to be completely parsed * * This can only determine if a script is bad, not if it's good * * If it passes the intitial pass, it runs the full pass and returns the result * It returns an array of [flag (boolean, false if "bad"), reason (string, human readable report)] * */ 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,""); dbg_print("%c ------evaluation results for "+ name +"------","color:white"); dbg_print("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(temp); if(res != null){ dbg_print("%c fail","color:red;"); flag = false; reason = "Script uses a reserved object (" + reserved_objects[i] + ")"; } } if(flag){ dbg_print("%c pass","color:green;"); } else{ return [flag,reason]; } return full_evaluate(script); } function validateLicense(matches) { if (!(Array.isArray(matches) && matches.length >= 4)){ return [false, "Malformed or unrecognized license tag."]; } let [all, tag, link, id] = matches; let license = null; if (licenses[id]) license = licenses[id]; for (let key in licenses){ if (licenses[key]["Magnet link"] === link) license = licenses[key]; if (licenses[key]["URL"] === link) license = licenses[key]; } if(!license){ return [false, `Unrecognized license "${id}"`]; } if (!(license["Magnet link"] === link || license["URL"] === link)){ return [false, `License magnet link does not match for "${id}".`]; } return [true, `Recognized license: "${id}".`]; } /** * * 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(scriptSrc, name, external = false){ let license = legacy_license_lib.check(scriptSrc); if (license){ return [true, scriptSrc, `Licensed under: ${license}`]; } if (listManager.builtInHashes.has(hash(scriptSrc))){ return [true, scriptSrc, "Common script known to be free software."]; } let editedSrc = ""; let uneditedSrc = scriptSrc.trim(); let reason = uneditedSrc ? "" : "Empty source."; let partsDenied = false; let partsAccepted = false; function checkTriviality(s) { if (!s.trim()) { return true; // empty, ignore it } let [trivial, message] = external ? [false, "External script with no known license"] : evaluate(s, name); if (trivial) { partsAccepted = true; editedSrc += s; } else { partsDenied = true; if (s.startsWith("javascript:")) editedSrc += `# LIBREJS BLOCKED: ${message}`; else editedSrc += `/*\nLIBREJS BLOCKED: ${message}\n*/`; } reason += `\n${message}`; return trivial; } while (uneditedSrc) { let openingMatch = /\/[\/\*]\s*?(@license)\s+(\S+)\s+(\S+)\s*$/mi.exec(uneditedSrc); if (!openingMatch) { // no license found, check for triviality checkTriviality(uneditedSrc); break; } let openingIndex = openingMatch.index; if (openingIndex) { // let's check the triviality of the code before the license tag, if any checkTriviality(uneditedSrc.substring(0, openingIndex)); } // let's check the actual license uneditedSrc = uneditedSrc.substring(openingIndex); let closureMatch = /\/([*/])\s*@license-end\b[^*/\n]*/i.exec(uneditedSrc); if (!closureMatch) { let msg = "ERROR: @license with no @license-end"; return [false, `\n/*\n ${msg} \n*/\n`, msg]; } let closureEndIndex = closureMatch.index + closureMatch[0].length; let commentEndOffset = uneditedSrc.substring(closureEndIndex).indexOf(closureMatch[1] === "*" ? "*/" : "\n"); if (commentEndOffset !== -1) { closureEndIndex += commentEndOffset; } let [licenseOK, message] = validateLicense(openingMatch); if(licenseOK) { editedSrc += uneditedSrc.substr(0, closureEndIndex); partsAccepted = true; } else { editedSrc += `\n/*\n${message}\n*/\n`; partsDenied = true; } reason += `\n${message}`; // trim off everything we just evaluated uneditedSrc = uneditedSrc.substring(closureEndIndex).trim(); } if(partsDenied) { if (partsAccepted) { reason = `Some parts of the script have been disabled (check the source for details).\n^--- ${reason}`; } return [false, editedSrc, reason]; } return [true, scriptSrc, reason]; } /* *********************************************************************************************** */ // TODO: Test if this script is being loaded from another domain compared to activityReports[tabid]["url"] /** * Asynchronous function, returns the final edited script as a string, * or an array containing it and the index, if the latter !== -1 */ async function get_script(response, url, tabId = -1, whitelisted = false, index = -1) { function result(scriptSource) { return index === -1 ? scriptSource : [scriptSource, index]; } let scriptName = url.split("/").pop(); if (whitelisted) { if (tabId !== -1) { let site = ListStore.siteItem(url); // Accept without reading script, it was explicitly whitelisted let reason = whitelist.contains(site) ? `All ${site} whitelisted by user` : "Address whitelisted by user"; addReportEntry(tabId, url, {"whitelisted": [url, reason], url}); } if (response.startsWith("javascript:")) return result(response); else return result(`/* LibreJS: script whitelisted by user preference. */\n${response}`); } let [verdict, editedSource, reason] = license_read(response, scriptName, index === -2); if (tabId < 0) { return result(verdict ? response : editedSource); } let sourceHash = hash(response); let domain = get_domain(url); let report = activityReports[tabId] || (activityReports[tabId] = await createReport({tabId})); updateBadge(tabId, report, !verdict); let category = await addReportEntry(tabId, sourceHash, {"url": domain, [verdict ? "accepted" : "blocked"]: [url, reason]}); switch(category) { case "blacklisted": editedSource = `/* LibreJS: script ${category} by user. */`; return result(response.startsWith("javascript:") ? `javascript:void(${encodeURIComponent(editedSource)})` : editedSource); case "whitelisted": return result(response.startsWith("javascript:") ? response : `/* LibreJS: script ${category} by user. */\n${response}`); default: let scriptSource = verdict ? response : editedSource; return result(response.startsWith("javascript:") ? (verdict ? scriptSource : `javascript:void(/* ${scriptSource} */)`) : `/* LibreJS: script ${category}. */\n${scriptSource}` ); } } function updateBadge(tabId, report = null, forceRed = false) { let blockedCount = report ? report.blocked.length + report.blacklisted.length : 0; let [text, color] = blockedCount > 0 || forceRed ? [blockedCount && blockedCount.toString() || "!" , "red"] : ["✓", "green"] let {browserAction} = browser; if ("setBadgeText" in browserAction) { browserAction.setBadgeText({text, tabId}); browserAction.setBadgeBackgroundColor({color, tabId}); } else { // Mobile browserAction.setTitle({title: `LibreJS (${text})`, tabId}); } } /** * Tests if a request is google analytics or not */ function test_GA(a){ // TODO: DRY me // This is just an HTML page if(a.url == 'https://www.google.com/analytics/#?modal_active=none'){ return false; } else if(a.url.match(/https:\/\/www\.google\.com\/analytics\//g)){ dbg_print("%c Google analytics (1)","color:red"); return {cancel: true}; } else if(a.url == 'https://www.google-analytics.com/analytics.js'){ dbg_print("%c Google analytics (2)","color:red"); return {cancel: true}; } else if(a.url == 'https://www.google.com/analytics/js/analytics.min.js'){ dbg_print("%c Google analytics (3)","color:red"); return {cancel: true}; } else return false; } /** * A callback that every type of request invokes. */ function block_ga(a){ var GA = test_GA(a); if(GA != false){ return GA; } else return {}; } /** * 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 non-free JavaScript */ var ResponseHandler = { /** * Enforce white/black lists for url/site early (hashes will be handled later) */ async pre(response) { let {request} = response; let {url, type, tabId, frameId, documentUrl} = request; let fullUrl = url; url = ListStore.urlItem(url); let site = ListStore.siteItem(url); let blacklistedSite = blacklist.contains(site); let blacklisted = blacklistedSite || blacklist.contains(url); let topUrl = request.frameAncestors && request.frameAncestors.pop() || documentUrl; if (blacklisted) { if (type === "script") { // abort the request before the response gets fetched addReportEntry(tabId, url, {url: topUrl, "blacklisted": [url, blacklistedSite ? `User blacklisted ${site}` : "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 { let whitelistedSite = whitelist.contains(site); 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 ${site}` : "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; let handle_it = type === "script" ? handle_script : handle_html; return await handle_it(response, response.whitelisted); } } /** * Here we handle external script requests */ async function handle_script(response, whitelisted){ let {text, request} = response; let {url, tabId, frameId} = request; url = ListStore.urlItem(url); let edited = await get_script(text, url, tabId, whitelisted, -2); return Array.isArray(edited) ? edited[0] : edited; } /** * 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 = `\n${s}`; } return s; } /** * Removes noscript tags with name "librejs-path" leaving the inner content to load. */ function remove_noscripts(html_doc){ for(var i = 0; i < html_doc.getElementsByName("librejs-path").length; i++){ if(html_doc.getElementsByName("librejs-path")[i].tagName == "NOSCRIPT"){ html_doc.getElementsByName("librejs-path")[i].outerHTML = html_doc.getElementsByName("librejs-path")[i].innerHTML; } } return doc2HTML(html_doc); } /** * Tests to see if the intrinsic events on the page are free or not. * returns true if they are, false if they're not */ function read_metadata(meta_element){ if(meta_element === undefined || meta_element === null){ return; } console.log("metadata found"); var metadata = {}; try{ metadata = JSON.parse(meta_element.innerHTML); }catch(error){ console.log("Could not parse metadata on page.") return false; } var license_str = metadata["intrinsic-events"]; if(license_str === undefined){ console.log("No intrinsic events license"); return false; } console.log(license_str); var parts = license_str.split(" "); if(parts.length != 2){ console.log("invalid (>2 tokens)"); return false; } // this should be adequete to escape the HTML escaping parts[0] = parts[0].replace(/&/g, '&'); try{ if(licenses[parts[1]]["Magnet link"] == parts[0]){ return true; }else{ console.log("invalid (doesn't match licenses)"); return false; } } catch(error){ console.log("invalid (threw error, key didn't exist)"); return false; } } /** * 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"); // moves external licenses reference, if any, before any