diff options
Diffstat (limited to 'data/extensions/https-everywhere@eff.org/background.js')
-rw-r--r-- | data/extensions/https-everywhere@eff.org/background.js | 647 |
1 files changed, 647 insertions, 0 deletions
diff --git a/data/extensions/https-everywhere@eff.org/background.js b/data/extensions/https-everywhere@eff.org/background.js new file mode 100644 index 0000000..7b77731 --- /dev/null +++ b/data/extensions/https-everywhere@eff.org/background.js @@ -0,0 +1,647 @@ +"use strict"; + +(function(exports) { + +const rules = require('./rules'), + store = require('./store'), + incognito = require('./incognito'), + util = require('./util'); + + +let all_rules = new rules.RuleSets(); + +async function initialize() { + await store.initialize(); + await store.performMigrations(); + await initializeStoredGlobals(); + await all_rules.loadFromBrowserStorage(store); + await incognito.onIncognitoDestruction(destroy_caches); +} +initialize(); + +async function initializeAllRules() { + const r = new rules.RuleSets(); + await r.loadFromBrowserStorage(store); + Object.assign(all_rules, r); +} + +/** + * Load preferences. Structure is: + * { + * httpNowhere: Boolean, + * showCounter: Boolean, + * isExtensionEnabled: Boolean + * } + */ +var httpNowhereOn = false; +var showCounter = true; +var isExtensionEnabled = true; + +function initializeStoredGlobals(){ + return new Promise(resolve => { + store.get({ + httpNowhere: false, + showCounter: true, + globalEnabled: true, + enableMixedRulesets: false + }, function(item) { + httpNowhereOn = item.httpNowhere; + showCounter = item.showCounter; + isExtensionEnabled = item.globalEnabled; + updateState(); + + rules.settings.enableMixedRulesets = item.enableMixedRulesets; + + resolve(); + }); + }); +} + +chrome.storage.onChanged.addListener(async function(changes, areaName) { + if (areaName === 'sync' || areaName === 'local') { + if ('httpNowhere' in changes) { + httpNowhereOn = changes.httpNowhere.newValue; + updateState(); + } + if ('showCounter' in changes) { + showCounter = changes.showCounter.newValue; + updateState(); + } + if ('globalEnabled' in changes) { + isExtensionEnabled = changes.globalEnabled.newValue; + updateState(); + } + if ('debugging_rulesets' in changes) { + initializeAllRules(); + } + } +}); + +if (chrome.tabs) { + chrome.tabs.onActivated.addListener(function() { + updateState(); + }); +} +if (chrome.windows) { + chrome.windows.onFocusChanged.addListener(function() { + updateState(); + }); +} +chrome.webNavigation.onCompleted.addListener(function() { + updateState(); +}); + +// 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 = {}; + +function getActiveRulesetCount(id) { + const applied = activeRulesets.getRulesets(id); + + if (!applied) + { + return 0; + } + + let activeCount = 0; + + for (const key in applied) { + if (applied[key].active) { + activeCount++; + } + } + + return activeCount; +} + +/** + * Set the icon color correctly + * active: extension is enabled. + * blocking: extension is in "block all HTTP requests" mode. + * disabled: extension is disabled from the popup menu. + */ + +function updateState () { + if (!chrome.tabs) return; + + let iconState = 'active'; + + if (!isExtensionEnabled) { + iconState = 'disabled'; + } else if (httpNowhereOn) { + iconState = 'blocking'; + } + + if ('setIcon' in chrome.browserAction) { + chrome.browserAction.setIcon({ + path: { + 38: 'icons/icon-' + iconState + '-38.png' + } + }); + } + + chrome.browserAction.setTitle({ + title: 'HTTPS Everywhere' + ((iconState === 'active') ? '' : ' (' + iconState + ')') + }); + + chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { + if (!tabs || tabs.length === 0) { + return; + } + const tabId = tabs[0].id; + const activeCount = getActiveRulesetCount(tabId); + + if ('setBadgeBackgroundColor' in chrome.browserAction) { + chrome.browserAction.setBadgeBackgroundColor({ color: '#666666', tabId }); + } + + const showBadge = activeCount > 0 && isExtensionEnabled && showCounter; + + if ('setBadgeText' in chrome.browserAction) { + chrome.browserAction.setBadgeText({ text: showBadge ? String(activeCount) : '', tabId }); + } + }); +} + +/** + * The following allows fennec to interact with the popup ui + * */ +chrome.browserAction.onClicked.addListener(e => { + const url = chrome.extension.getURL("popup.html?tabId=" + e.id); + chrome.tabs.create({ + url + }); +}); + +/** + * Adds a listener for removed tabs + * */ +function AppliedRulesets() { + this.active_tab_rules = {}; + + var that = this; + if (chrome.tabs) { + chrome.tabs.onRemoved.addListener(function(tabId) { + 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 = new Set(); + +// redirect counter workaround +// TODO: Remove this code if they ever give us a real counter +var redirectCounter = new Map(); + +/** + * 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) { + // If HTTPSe has been disabled by the user, return immediately. + if (!isExtensionEnabled) { + return; + } + + let uri = new URL(details.url); + + // Normalise hosts with tailing dots, e.g. "www.example.com." + let canonical_host = uri.hostname; + while (canonical_host.charAt(canonical_host.length - 1) == ".") { + canonical_host = canonical_host.slice(0, -1); + uri.hostname = canonical_host; + } + + // Should the request be canceled? + const shouldCancel = httpNowhereOn && + uri.protocol === 'http:' && + uri.hostname.slice(-6) !== '.onion' && + uri.hostname !== 'localhost' && + !/^127(\.[0-9]{1,3}){3}$/.test(canonical_host) && + !/^0\.0\.0\.0$/.test(canonical_host) && + uri.hostname !== '[::1]'; + + // If there is a username / password, put them aside during the ruleset + // analysis process + let using_credentials_in_url = false; + let tmp_user; + let tmp_pass; + if (uri.password || uri.username) { + using_credentials_in_url = true; + tmp_user = uri.username; + tmp_pass = uri.password; + uri.username = ''; + uri.password = ''; + } + + var canonical_url = uri.href; + if (details.url != canonical_url && !using_credentials_in_url) { + util.log(util.INFO, "Original url " + details.url + + " changed before processing to " + canonical_url); + } + if (urlBlacklist.has(canonical_url)) { + return {cancel: shouldCancel}; + } + + if (details.type == "main_frame") { + activeRulesets.removeTab(details.tabId); + } + + var potentiallyApplicable = all_rules.potentiallyApplicableRulesets(canonical_host); + + if (redirectCounter.get(details.requestId) >= 8) { + util.log(util.NOTE, "Redirect counter hit for " + canonical_url); + urlBlacklist.add(canonical_url); + rules.settings.domainBlacklist.add(canonical_host); + util.log(util.WARN, "Domain blacklisted " + canonical_host); + return {cancel: shouldCancel}; + } + + var newuristr = null; + + for (let ruleset of potentiallyApplicable) { + activeRulesets.addRulesetToTab(details.tabId, ruleset); + if (ruleset.active && !newuristr) { + newuristr = ruleset.apply(canonical_url); + } + } + + // re-insert userpass info which was stripped temporarily + if (using_credentials_in_url) { + if (newuristr) { + const uri_with_credentials = new URL(newuristr); + uri_with_credentials.username = tmp_user; + uri_with_credentials.password = tmp_pass; + newuristr = uri_with_credentials.href; + } else { + const canonical_url_with_credentials = new URL(canonical_url); + canonical_url_with_credentials.username = tmp_user; + canonical_url_with_credentials.password = tmp_pass; + canonical_url = canonical_url_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 loading a main frame, try the HTTPS version as an alternative to + // failing. + if (shouldCancel) { + if (!newuristr) { + return {redirectUrl: canonical_url.replace(/^http:/, "https:")}; + } else { + return {redirectUrl: newuristr.replace(/^http:/, "https:")}; + } + } + 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 +const mixedContentTypes = { + object: 1, other: 1, script: 1, stylesheet: 1, sub_frame: 1, xmlhttprequest: 1, + image: 0, main_frame: 0 +}; + +/** + * 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) { + let rw = rewritten_url ? "rw" : "nrw"; + + let active_content = 1; + if (mixedContentTypes.hasOwnProperty(type)) { + active_content = mixedContentTypes[type]; + } else { + util.log(util.WARN, "Unknown type from onBeforeRequest details: `" + type + "', assuming active"); + } + + 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; +} + +/** + * 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 && isExtensionEnabled) { + if (all_rules.shouldSecureCookie(changeInfo.cookie)) { + 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. + util.log(util.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) + let prefix = details.redirectUrl.substring(0, 5); + if (prefix === "http:" || prefix === "https") { + let count = redirectCounter.get(details.requestId); + if (count) { + redirectCounter.set(details.requestId, count + 1); + util.log(util.DBUG, "Got redirect id "+details.requestId+ + ": "+count); + } else { + redirectCounter.set(details.requestId, 1); + } + } +} + +/** + * handle webrequest.onCompleted, cleanup redirectCounter + * @param details details for the chrome.webRequest (see chrome doc) + */ +function onCompleted(details) { + if (redirectCounter.has(details.requestId)) { + redirectCounter.delete(details.requestId); + } +} + +/** + * handle webrequest.onErrorOccurred, cleanup redirectCounter + * @param details details for the chrome.webRequest (see chrome doc) + */ +function onErrorOccurred(details) { + if (redirectCounter.has(details.requestId)) { + redirectCounter.delete(details.requestId); + } +} + +// Registers the handler for requests +// See: https://github.com/EFForg/https-everywhere/issues/10039 +chrome.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["*://*/*"]}, ["blocking"]); + + +// Try to catch redirect loops on URLs we've redirected to HTTPS. +chrome.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["https://*/*"]}); + +// Cleanup redirectCounter if neccessary +chrome.webRequest.onCompleted.addListener(onCompleted, {urls: ["*://*/*"]}); + +// Cleanup redirectCounter if neccessary +chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, {urls: ["*://*/*"]}) + +// 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() { + util.log(util.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 === "getHosts") { + sendResponse({ + nrw: sortSwitchPlanner(tabId, "nrw"), + rw: sortSwitchPlanner(tabId, "rw") + }); + } + }); + } +}); + +// This is necessary for communication with the popup in Firefox Private +// Browsing Mode, see https://bugzilla.mozilla.org/show_bug.cgi?id=1329304 +chrome.runtime.onMessage.addListener(function(message, sender, sendResponse){ + if (message.type == "get_option") { + store.get(message.object, sendResponse); + return true; + } else if (message.type == "set_option") { + store.set(message.object, item => { + if (sendResponse) { + sendResponse(item); + } + }); + } else if (message.type == "delete_from_ruleset_cache") { + all_rules.ruleCache.delete(message.object); + } else if (message.type == "get_active_rulesets") { + sendResponse(activeRulesets.getRulesets(message.object)); + } else if (message.type == "set_ruleset_active_status") { + var ruleset = activeRulesets.getRulesets(message.object.tab_id)[message.object.name]; + ruleset.active = message.object.active; + if (ruleset.default_state == message.object.active) { + message.object.active = undefined; + } + all_rules.setRuleActiveState(message.object.name, message.object.active).then(() => { + sendResponse(true); + }); + return true; + } else if (message.type == "add_new_rule") { + all_rules.addNewRuleAndStore(message.object).then(() => { + sendResponse(true); + }); + return true; + } else if (message.type == "remove_rule") { + all_rules.removeRuleAndStore(message.object); + } else if (message.type == "import_settings") { + // This is used when importing settings from the options ui + import_settings(message.object).then(() => { + sendResponse(true); + }); + } +}); + +/** + * Import extension settings (custom rulesets, ruleset toggles, globals) from an object + * @param settings the settings object + */ +async function import_settings(settings) { + if (settings && settings.changed) { + let ruleActiveStates = {}; + // Load all the ruleset toggles into memory and store + for (const ruleset_name in settings.rule_toggle) { + ruleActiveStates[ruleset_name] = (settings.rule_toggle[ruleset_name] == "true"); + } + + // Save settings + await new Promise(resolve => { + store.set({ + legacy_custom_rulesets: settings.custom_rulesets, + httpNowhere: settings.prefs.http_nowhere_enabled, + showCounter: settings.prefs.show_counter, + globalEnabled: settings.prefs.global_enabled, + ruleActiveStates + }, resolve); + }); + + initializeAllRules(); + } +} + +/** + * Clear any cache/ blacklist we have. + */ +function destroy_caches() { + util.log(util.DBUG, "Destroying caches."); + all_rules.cookieHostCache.clear(); + all_rules.ruleCache.clear(); + rules.settings.domainBlacklist.clear(); + urlBlacklist.clear(); +} + +Object.assign(exports, { + all_rules, + urlBlacklist, + sortSwitchPlanner, + switchPlannerInfo +}); + +})(typeof exports == 'undefined' ? require.scopes.background = {} : exports); |