"use strict"; /** * Fetch and parse XML to be loaded as RuleSets. * * @param url: a relative URL to local XML */ function loadExtensionFile(url, returnType) { var xhr = new XMLHttpRequest(); // Use blocking XHR to ensure everything is loaded by the time // we return. xhr.open("GET", chrome.extension.getURL(url), false); xhr.send(null); // Get file contents if (xhr.readyState != 4) { return; } if (returnType === 'xml') { return xhr.responseXML; } return xhr.responseText; } // Rules are loaded here var all_rules = new RuleSets(navigator.userAgent, LRUCache, localStorage); var rule_list = 'rules/default.rulesets'; all_rules.addFromXml(loadExtensionFile(rule_list, 'xml')); var USER_RULE_KEY = 'userRules'; // Records which tabId's are active in the HTTPS Switch Planner (see // devtools-panel.js). var switchPlannerEnabledFor = {}; // Detailed information recorded when the HTTPS Switch Planner is active. // Structure is: // switchPlannerInfo[tabId]["rw"/"nrw"][resource_host][active_content][url]; // rw / nrw stand for "rewritten" versus "not rewritten" var switchPlannerInfo = {}; // Load prefs about whether http nowhere is on. Structure is: // { httpNowhere: true/false } var httpNowhereOn = false; chrome.storage.sync.get({httpNowhere: false}, function(item) { httpNowhereOn = item.httpNowhere; setIconColor(); }); chrome.storage.onChanged.addListener(function(changes, areaName) { if (areaName === 'sync') { for (var key in changes) { if (key === 'httpNowhere') { httpNowhereOn = changes[key].newValue; setIconColor(); } } } }); /** * Load stored user rules **/ var getStoredUserRules = function() { var oldUserRuleString = localStorage.getItem(USER_RULE_KEY); var oldUserRules = []; if (oldUserRuleString) { oldUserRules = JSON.parse(oldUserRuleString); } return oldUserRules; }; var wr = chrome.webRequest; /** * Load all stored user rules */ var loadStoredUserRules = function() { var rules = getStoredUserRules(); var i; for (i = 0; i < rules.length; ++i) { all_rules.addUserRule(rules[i]); } log('INFO', 'loaded ' + i + ' stored user rules'); }; loadStoredUserRules(); /** * Set the icon color correctly * Depending on http-nowhere it should be red/default */ var setIconColor = function() { var newIconPath = httpNowhereOn ? './icon38-red.png' : './icon38.png'; chrome.browserAction.setIcon({ path: newIconPath }); }; /* for (var v in localStorage) { log(DBUG, "localStorage["+v+"]: "+localStorage[v]); } var rs = all_rules.potentiallyApplicableRulesets("www.google.com"); for (r in rs) { log(DBUG, rs[r].name +": "+ rs[r].active); log(DBUG, rs[r].name +": "+ rs[r].default_state); } */ /** * Adds a new user rule * @param params: params defining the rule * @param cb: Callback to call after success/fail * */ var addNewRule = function(params, cb) { if (all_rules.addUserRule(params)) { // If we successfully added the user rule, save it in local // storage so it's automatically applied when the extension is // reloaded. var oldUserRules = getStoredUserRules(); // TODO: there's a race condition here, if this code is ever executed from multiple // client windows in different event loops. oldUserRules.push(params); // TODO: can we exceed the max size for storage? localStorage.setItem(USER_RULE_KEY, JSON.stringify(oldUserRules)); cb(true); } else { cb(false); } }; /** * Adds a listener for removed tabs * */ function AppliedRulesets() { this.active_tab_rules = {}; var that = this; chrome.tabs.onRemoved.addListener(function(tabId, info) { that.removeTab(tabId); }); } AppliedRulesets.prototype = { addRulesetToTab: function(tabId, ruleset) { if (tabId in this.active_tab_rules) { this.active_tab_rules[tabId][ruleset.name] = ruleset; } else { this.active_tab_rules[tabId] = {}; this.active_tab_rules[tabId][ruleset.name] = ruleset; } }, getRulesets: function(tabId) { if (tabId in this.active_tab_rules) { return this.active_tab_rules[tabId]; } return null; }, removeTab: function(tabId) { delete this.active_tab_rules[tabId]; } }; // FIXME: change this name var activeRulesets = new AppliedRulesets(); var urlBlacklist = {}; var domainBlacklist = {}; // redirect counter workaround // TODO: Remove this code if they ever give us a real counter var redirectCounter = {}; /** * Called before a HTTP(s) request. Does the heavy lifting * Cancels the request/redirects it to HTTPS. URL modification happens in here. * @param details of the handler, see Chrome doc * */ function onBeforeRequest(details) { var uri = document.createElement('a'); uri.href = details.url; // Should the request be canceled? var shouldCancel = (httpNowhereOn && uri.protocol === 'http:'); // Normalise hosts such as "www.example.com." var canonical_host = uri.hostname; if (canonical_host.charAt(canonical_host.length - 1) == ".") { while (canonical_host.charAt(canonical_host.length - 1) == ".") canonical_host = canonical_host.slice(0,-1); uri.hostname = canonical_host; } // If there is a username / password, put them aside during the ruleset // analysis process var using_credentials_in_url = false; if (uri.password || uri.username) { using_credentials_in_url = true; var tmp_user = uri.username; var tmp_pass = uri.password; uri.username = null; uri.password = null; } var canonical_url = uri.href; if (details.url != canonical_url && !using_credentials_in_url) { log(INFO, "Original url " + details.url + " changed before processing to " + canonical_url); } if (canonical_url in urlBlacklist) { return {cancel: shouldCancel}; } if (details.type == "main_frame") { activeRulesets.removeTab(details.tabId); } var rs = all_rules.potentiallyApplicableRulesets(uri.hostname); // If no rulesets could apply, let's get out of here! if (rs.length === 0) { return {cancel: shouldCancel}; } if (redirectCounter[details.requestId] >= 8) { log(NOTE, "Redirect counter hit for " + canonical_url); urlBlacklist[canonical_url] = true; var hostname = uri.hostname; domainBlacklist[hostname] = true; log(WARN, "Domain blacklisted " + hostname); return {cancel: shouldCancel}; } var newuristr = null; for(var i = 0; i < rs.length; ++i) { activeRulesets.addRulesetToTab(details.tabId, rs[i]); if (rs[i].active && !newuristr) { newuristr = rs[i].apply(canonical_url); } } if (newuristr && using_credentials_in_url) { // re-insert userpass info which was stripped temporarily var uri_with_credentials = document.createElement('a'); uri_with_credentials.href = newuristr; uri_with_credentials.username = tmp_user; uri_with_credentials.password = tmp_pass; newuristr = uri_with_credentials.href; } // In Switch Planner Mode, record any non-rewriteable // HTTP URIs by parent hostname, along with the resource type. if (switchPlannerEnabledFor[details.tabId] && uri.protocol !== "https:") { writeToSwitchPlanner(details.type, details.tabId, canonical_host, details.url, newuristr); } if (httpNowhereOn) { if (newuristr && newuristr.substring(0, 5) === "http:") { // Abort early if we're about to redirect to HTTP in HTTP Nowhere mode return {cancel: true}; } } if (newuristr) { return {redirectUrl: newuristr}; } else { return {cancel: shouldCancel}; } } // Map of which values for the `type' enum denote active vs passive content. // https://developer.chrome.com/extensions/webRequest.html#event-onBeforeRequest var activeTypes = { stylesheet: 1, script: 1, object: 1, other: 1}; // We consider sub_frame to be passive even though it can contain JS or flash. // This is because code running in the sub_frame cannot access the main frame's // content, by same-origin policy. This is true even if the sub_frame is on the // same domain but different protocol - i.e. HTTP while the parent is HTTPS - // because same-origin policy includes the protocol. This also mimics Chrome's // UI treatment of insecure subframes. var passiveTypes = { main_frame: 1, sub_frame: 1, image: 1, xmlhttprequest: 1}; /** * Record a non-HTTPS URL loaded by a given hostname in the Switch Planner, for * use in determining which resources need to be ported to HTTPS. * (Reminder: Switch planner is the pro-tool enabled by switching into debug-mode) * * @param type: type of the resource (see activeTypes and passiveTypes arrays) * @param tab_id: The id of the tab * @param resource_host: The host of the original url * @param resource_url: the original url * @param rewritten_url: The url rewritten to * */ function writeToSwitchPlanner(type, tab_id, resource_host, resource_url, rewritten_url) { var rw = "rw"; if (rewritten_url == null) rw = "nrw"; var active_content = 0; if (activeTypes[type]) { active_content = 1; } else if (passiveTypes[type]) { active_content = 0; } else { log(WARN, "Unknown type from onBeforeRequest details: `" + type + "', assuming active"); active_content = 1; } if (!switchPlannerInfo[tab_id]) { switchPlannerInfo[tab_id] = {}; switchPlannerInfo[tab_id]["rw"] = {}; switchPlannerInfo[tab_id]["nrw"] = {}; } if (!switchPlannerInfo[tab_id][rw][resource_host]) switchPlannerInfo[tab_id][rw][resource_host] = {}; if (!switchPlannerInfo[tab_id][rw][resource_host][active_content]) switchPlannerInfo[tab_id][rw][resource_host][active_content] = {}; switchPlannerInfo[tab_id][rw][resource_host][active_content][resource_url] = 1; } /** * Return the number of properties in an object. For associative maps, this is * their size. * @param obj: object to calc the size for * */ function objSize(obj) { if (typeof obj == 'undefined') return 0; var size = 0, key; for (key in obj) { if (obj.hasOwnProperty(key)) size++; } return size; } /** * Make an array of asset hosts by score so we can sort them, * presenting the most important ones first. * */ function sortSwitchPlanner(tab_id, rewritten) { var asset_host_list = []; if (typeof switchPlannerInfo[tab_id] === 'undefined' || typeof switchPlannerInfo[tab_id][rewritten] === 'undefined') { return []; } var tabInfo = switchPlannerInfo[tab_id][rewritten]; for (var asset_host in tabInfo) { var ah = tabInfo[asset_host]; var activeCount = objSize(ah[1]); var passiveCount = objSize(ah[0]); var score = activeCount * 100 + passiveCount; asset_host_list.push([score, activeCount, passiveCount, asset_host]); } asset_host_list.sort(function(a,b){return a[0]-b[0];}); return asset_host_list; } /** * Format the switch planner output for presentation to a user. * */ function switchPlannerSmallHtmlSection(tab_id, rewritten) { var asset_host_list = sortSwitchPlanner(tab_id, rewritten); if (asset_host_list.length == 0) { return "none"; } var output = ""; for (var i = asset_host_list.length - 1; i >= 0; i--) { var host = asset_host_list[i][3]; var activeCount = asset_host_list[i][1]; var passiveCount = asset_host_list[i][2]; output += "" + host + ": "; if (activeCount > 0) { output += activeCount + " active"; if (passiveCount > 0) output += ", "; } if (passiveCount > 0) { output += passiveCount + " passive"; } output += "
"; } return output; } /** * Create switch planner sections * */ function switchPlannerRenderSections(tab_id, f) { return "Unrewritten HTTP resources loaded from this tab (enable HTTPS on " + "these domains and add them to HTTPS Everywhere):
" + f(tab_id, "nrw") + "
Resources rewritten successfully from this tab (update these " + "in your source code):
" + f(tab_id, "rw"); } /** * Generate the small switch planner html content * */ function switchPlannerSmallHtml(tab_id) { return switchPlannerRenderSections(tab_id, switchPlannerSmallHtmlSection); } /** * Generate a HTML link from urls in map * map: the map containing the urls * */ function linksFromKeys(map) { if (typeof map == 'undefined') return ""; var output = ""; for (var key in map) { if (map.hasOwnProperty(key)) { output += "" + key + "
"; } } return output; } /** * Generate the detailed html fot the switch planner * */ function switchPlannerDetailsHtml(tab_id) { return switchPlannerRenderSections(tab_id, switchPlannerDetailsHtmlSection); } /** * Generate the detailed html fot the switch planner, by section * */ function switchPlannerDetailsHtmlSection(tab_id, rewritten) { var asset_host_list = sortSwitchPlanner(tab_id, rewritten); var output = ""; for (var i = asset_host_list.length - 1; i >= 0; i--) { var host = asset_host_list[i][3]; var activeCount = asset_host_list[i][1]; var passiveCount = asset_host_list[i][2]; output += "" + host + ": "; if (activeCount > 0) { output += activeCount + " active
"; output += linksFromKeys(switchPlannerInfo[tab_id][rewritten][host][1]); } if (passiveCount > 0) { output += "
" + passiveCount + " passive
"; output += linksFromKeys(switchPlannerInfo[tab_id][rewritten][host][0]); } output += "
"; } return output; } /** * monitor cookie changes. Automatically convert them to secure cookies * @param changeInfo Cookie changed info, see Chrome doc * */ function onCookieChanged(changeInfo) { if (!changeInfo.removed && !changeInfo.cookie.secure) { if (all_rules.shouldSecureCookie(changeInfo.cookie, false)) { var cookie = {name:changeInfo.cookie.name, value:changeInfo.cookie.value, path:changeInfo.cookie.path, httpOnly:changeInfo.cookie.httpOnly, expirationDate:changeInfo.cookie.expirationDate, storeId:changeInfo.cookie.storeId, secure: true}; // Host-only cookies don't set the domain field. if (!changeInfo.cookie.hostOnly) { cookie.domain = changeInfo.cookie.domain; } // The cookie API is magical -- we must recreate the URL from the domain and path. if (changeInfo.cookie.domain[0] == ".") { cookie.url = "https://www" + changeInfo.cookie.domain + cookie.path; } else { cookie.url = "https://" + changeInfo.cookie.domain + cookie.path; } // We get repeated events for some cookies because sites change their // value repeatedly and remove the "secure" flag. log(DBUG, "Securing cookie " + cookie.name + " for " + changeInfo.cookie.domain + ", was secure=" + changeInfo.cookie.secure); chrome.cookies.set(cookie); } } } /** * handling redirects, breaking loops * @param details details for the redirect (see chrome doc) * */ function onBeforeRedirect(details) { // Catch redirect loops (ignoring about:blank, etc. caused by other extensions) var prefix = details.redirectUrl.substring(0, 5); if (prefix === "http:" || prefix === "https") { if (details.requestId in redirectCounter) { redirectCounter[details.requestId] += 1; log(DBUG, "Got redirect id "+details.requestId+ ": "+redirectCounter[details.requestId]); } else { redirectCounter[details.requestId] = 1; } } } // Registers the handler for requests // We listen to all HTTP hosts, because RequestFilter can't handle tons of url restrictions. wr.onBeforeRequest.addListener(onBeforeRequest, {urls: ["http://*/*"]}, ["blocking"]); // TODO: Listen only to the tiny subset of HTTPS hosts that we rewrite/downgrade. var httpsUrlsWeListenTo = ["https://*/*"]; // See: https://developer.chrome.com/extensions/match_patterns wr.onBeforeRequest.addListener(onBeforeRequest, {urls: httpsUrlsWeListenTo}, ["blocking"]); // Try to catch redirect loops on URLs we've redirected to HTTPS. wr.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["https://*/*"]}); // Listen for cookies set/updated and secure them if applicable. This function is async/nonblocking. chrome.cookies.onChanged.addListener(onCookieChanged); /** * disable switch Planner * @param tabId the Tab to disable for */ function disableSwitchPlannerFor(tabId) { delete switchPlannerEnabledFor[tabId]; // Clear stored URL info. delete switchPlannerInfo[tabId]; } /** * Enable switch planner for specific tab * @param tabId the tab to enable it for */ function enableSwitchPlannerFor(tabId) { switchPlannerEnabledFor[tabId] = true; } // Listen for connection from the DevTools panel so we can set up communication. chrome.runtime.onConnect.addListener(function (port) { if (port.name == "devtools-page") { chrome.runtime.onMessage.addListener(function(message, sender, sendResponse){ var tabId = message.tabId; var disableOnCloseCallback = function(port) { log(DBUG, "Devtools window for tab " + tabId + " closed, clearing data."); disableSwitchPlannerFor(tabId); }; if (message.type === "enable") { enableSwitchPlannerFor(tabId); port.onDisconnect.addListener(disableOnCloseCallback); } else if (message.type === "disable") { disableSwitchPlannerFor(tabId); } else if (message.type === "getSmallHtml") { sendResponse({html: switchPlannerSmallHtml(tabId)}); } }); } });