summaryrefslogtreecommitdiff
path: root/data/extensions/https-everywhere@eff.org/background.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/https-everywhere@eff.org/background.js')
-rw-r--r--data/extensions/https-everywhere@eff.org/background.js647
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);