summaryrefslogtreecommitdiff
path: root/data/extensions/https-everywhere@eff.org/background-scripts
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/https-everywhere@eff.org/background-scripts')
-rw-r--r--data/extensions/https-everywhere@eff.org/background-scripts/background.js905
-rw-r--r--data/extensions/https-everywhere@eff.org/background-scripts/bootstrap.js9
-rw-r--r--data/extensions/https-everywhere@eff.org/background-scripts/incognito.js73
-rw-r--r--data/extensions/https-everywhere@eff.org/background-scripts/rules.js690
-rw-r--r--data/extensions/https-everywhere@eff.org/background-scripts/store.js103
-rw-r--r--data/extensions/https-everywhere@eff.org/background-scripts/update.js280
-rw-r--r--data/extensions/https-everywhere@eff.org/background-scripts/update_channels.js23
-rw-r--r--data/extensions/https-everywhere@eff.org/background-scripts/util.js93
8 files changed, 2176 insertions, 0 deletions
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/background.js b/data/extensions/https-everywhere@eff.org/background-scripts/background.js
new file mode 100644
index 0000000..8a82be3
--- /dev/null
+++ b/data/extensions/https-everywhere@eff.org/background-scripts/background.js
@@ -0,0 +1,905 @@
+"use strict";
+
+(function(exports) {
+
+const rules = require('./rules'),
+ store = require('./store'),
+ incognito = require('./incognito'),
+ util = require('./util'),
+ update = require('./update'),
+ { update_channels } = require('./update_channels');
+
+
+let all_rules = new rules.RuleSets();
+
+async function initialize() {
+ await store.initialize();
+ await store.performMigrations();
+ await initializeStoredGlobals();
+ await getUpgradeToSecureAvailable();
+ await update.initialize(store, initializeAllRules);
+ await all_rules.loadFromBrowserStorage(store, update.applyStoredRulesets);
+ await incognito.onIncognitoDestruction(destroy_caches);
+}
+initialize();
+
+async function initializeAllRules() {
+ const r = new rules.RuleSets();
+ await r.loadFromBrowserStorage(store, update.applyStoredRulesets);
+ 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();
+ });
+ });
+}
+
+let upgradeToSecureAvailable;
+
+function getUpgradeToSecureAvailable() {
+ if (typeof browser !== 'undefined') {
+ return browser.runtime.getBrowserInfo().then(function(info) {
+ let version = info.version.match(/^(\d+)/)[1];
+ if (info.name == "Firefox" && version >= 59) {
+ upgradeToSecureAvailable = true;
+ } else {
+ upgradeToSecureAvailable = false;
+ }
+ });
+ } else {
+ return new Promise(resolve => {
+ upgradeToSecureAvailable = false;
+ 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 ('enableMixedRulesets' in changes) {
+ // Don't require users to restart the browsers
+ rules.settings.enableMixedRulesets = changes.enableMixedRulesets.newValue;
+ initializeAllRules();
+ }
+ 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
+// pages/devtools/panel-ux.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 = {};
+
+/**
+ * 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: 'images/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 = appliedRulesets.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("/pages/popup/index.html?tabId=" + e.id);
+ chrome.tabs.create({
+ url
+ });
+});
+
+
+
+/**
+ * Add a listener for removed tabs
+ */
+function AppliedRulesets() {
+ this.active_tab_rules = new Map();
+ this.active_tab_main_frames = new Map();
+
+ let that = this;
+ if (chrome.tabs) {
+ chrome.tabs.onRemoved.addListener(function(tabId) {
+ that.removeTab(tabId);
+ });
+ }
+}
+
+AppliedRulesets.prototype = {
+ addRulesetToTab: function(tabId, type, ruleset) {
+ if (!this.active_tab_main_frames.has(tabId)) {
+ this.active_tab_main_frames.set(tabId, false);
+ }
+
+ // always show main_frame ruleset on the top
+ if (type == "main_frame") {
+ this.active_tab_main_frames.set(tabId, true);
+ this.active_tab_rules.set(tabId, [ruleset,]);
+ return ;
+ }
+
+ if (this.active_tab_rules.has(tabId)) {
+ let rulesets = this.active_tab_rules.get(tabId);
+ let insertIndex = 0;
+
+ const ruleset_name = ruleset.name.toLowerCase();
+
+ for (const item of rulesets) {
+ const item_name = item.name.toLowerCase();
+
+ if (item_name == ruleset_name) {
+ return ;
+ } else if (insertIndex == 0 && this.active_tab_main_frames.get(tabId)) {
+ insertIndex = 1;
+ } else if (item_name < ruleset_name) {
+ insertIndex++;
+ }
+ }
+ rulesets.splice(insertIndex, 0, ruleset);
+ } else {
+ this.active_tab_rules.set(tabId, [ruleset,]);
+ }
+ },
+
+ getRulesets: function(tabId) {
+ if (this.active_tab_rules.has(tabId)) {
+ return this.active_tab_rules.get(tabId);
+ } else {
+ return null;
+ }
+ },
+
+ removeTab: function(tabId) {
+ this.active_tab_rules.delete(tabId);
+ this.active_tab_main_frames.delete(tabId);
+ },
+
+ getActiveRulesetCount: function (tabId) {
+ let activeCount = 0;
+
+ const rulesets = this.getRulesets(tabId);
+ if (rulesets) {
+ for (const ruleset of rulesets) {
+ if (ruleset.active) {
+ activeCount++;
+ }
+ }
+ }
+ return activeCount;
+ }
+};
+
+var appliedRulesets = 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();
+
+const cancelUrl = chrome.extension.getURL("/pages/cancel/index.html");
+
+function redirectOnCancel(shouldCancel){
+ return shouldCancel ? {redirectUrl: cancelUrl} : {cancel: false};
+}
+
+/**
+ * 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."
+ while (uri.hostname[uri.hostname.length - 1] === '.' && uri.hostname !== '.') {
+ uri.hostname = uri.hostname.slice(0, -1);
+ }
+
+ // Should the request be canceled?
+ // true if the URL is a http:// connection to a remote canonical host, and not
+ // a tor hidden service
+ const shouldCancel = httpNowhereOn &&
+ (uri.protocol === 'http:' || uri.protocol === 'ftp:') &&
+ uri.hostname.slice(-6) !== '.onion' &&
+ uri.hostname !== 'localhost' &&
+ !/^127(\.[0-9]{1,3}){3}$/.test(uri.hostname) &&
+ uri.hostname !== '0.0.0.0' &&
+ 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 = '';
+ }
+
+ if (details.url != uri.href && !using_credentials_in_url) {
+ util.log(util.INFO, "Original url " + details.url +
+ " changed before processing to " + uri.href);
+ }
+ if (urlBlacklist.has(uri.href)) {
+ return redirectOnCancel(shouldCancel);
+ }
+
+ if (details.type == "main_frame") {
+ appliedRulesets.removeTab(details.tabId);
+ }
+
+ let potentiallyApplicable = all_rules.potentiallyApplicableRulesets(uri.hostname);
+
+ if (redirectCounter.get(details.requestId) >= 8) {
+ util.log(util.NOTE, "Redirect counter hit for " + uri.href);
+ urlBlacklist.add(uri.href);
+ rules.settings.domainBlacklist.add(uri.hostname);
+ util.log(util.WARN, "Domain blacklisted " + uri.hostname);
+ return redirectOnCancel(shouldCancel);
+ }
+
+ // whether to use mozilla's upgradeToSecure BlockingResponse if available
+ let upgradeToSecure = false;
+ let newuristr = null;
+
+ for (let ruleset of potentiallyApplicable) {
+ appliedRulesets.addRulesetToTab(details.tabId, details.type, ruleset);
+ if (ruleset.active && !newuristr) {
+ newuristr = ruleset.apply(uri.href);
+ }
+ }
+
+ // only use upgradeToSecure for trivial rewrites
+ if (upgradeToSecureAvailable && newuristr) {
+ // check rewritten URIs against the trivially upgraded URI
+ const trivialUpgradeUri = uri.href.replace(/^http:/, "https:");
+ upgradeToSecure = (newuristr == trivialUpgradeUri);
+ }
+
+ // 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 url_with_credentials = new URL(uri.href);
+ url_with_credentials.username = tmp_user;
+ url_with_credentials.password = tmp_pass;
+ uri.href = 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,
+ uri.hostname,
+ details.url,
+ newuristr);
+ }
+
+ if (httpNowhereOn) {
+ // If loading a main frame, try the HTTPS version as an alternative to
+ // failing.
+ if (shouldCancel) {
+ if (!newuristr) {
+ newuristr = uri.href.replace(/^http:/, "https:");
+ upgradeToSecure = true;
+ } else {
+ newuristr = newuristr.replace(/^http:/, "https:");
+ }
+ }
+ if (
+ newuristr &&
+ (
+ newuristr.substring(0, 5) === "http:" ||
+ newuristr.substring(0, 4) === "ftp:"
+ )
+ ) {
+ // Abort early if we're about to redirect to HTTP or FTP in HTTP Nowhere mode
+ return {redirectUrl: cancelUrl};
+ }
+ }
+
+ if (upgradeToSecureAvailable && upgradeToSecure) {
+ util.log(util.INFO, 'onBeforeRequest returning upgradeToSecure: true');
+ return {upgradeToSecure: true};
+ } else if (newuristr) {
+ util.log(util.INFO, 'onBeforeRequest returning redirectUrl: ' + newuristr);
+ return {redirectUrl: newuristr};
+ } else {
+ util.log(util.INFO, 'onBeforeRequest returning shouldCancel: ' + shouldCancel);
+ return redirectOnCancel(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)) {
+ let 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;
+ }
+
+ // Chromium cookie sameSite status, see https://tools.ietf.org/html/draft-west-first-party-cookies
+ if (changeInfo.cookie.sameSite) {
+ cookie.sameSite = changeInfo.cookie.sameSite;
+ }
+
+ // Firefox first-party isolation
+ if (changeInfo.cookie.firstPartyDomain) {
+ cookie.firstPartyDomain = changeInfo.cookie.firstPartyDomain;
+ }
+
+ // 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);
+ }
+}
+
+/**
+ * handle webrequest.onHeadersReceived, insert upgrade-insecure-requests directive and
+ * rewrite access-control-allow-origin if presented in HTTP Nowhere mode
+ * @param details details for the chrome.webRequest (see chrome doc)
+ */
+function onHeadersReceived(details) {
+ if (isExtensionEnabled && httpNowhereOn) {
+ // Do not upgrade the .onion requests in HTTP Nowhere Mode,
+ // See https://github.com/EFForg/https-everywhere/pull/14600#discussion_r168072480
+ const uri = new URL(details.url);
+ if (uri.hostname.slice(-6) == '.onion') {
+ return {};
+ }
+
+ let responseHeadersChanged = false;
+ let cspHeaderFound = false;
+
+ for (const idx in details.responseHeaders) {
+ if (details.responseHeaders[idx].name.match(/Content-Security-Policy/i)) {
+ // Existing CSP headers found
+ cspHeaderFound = true;
+ const value = details.responseHeaders[idx].value;
+
+ // Prepend if no upgrade-insecure-requests directive exists
+ if (!value.match(/upgrade-insecure-requests/i)) {
+ details.responseHeaders[idx].value = "upgrade-insecure-requests; " + value;
+ responseHeadersChanged = true;
+ }
+ }
+
+ if (details.responseHeaders[idx].name.match(/Access-Control-Allow-Origin/i)) {
+ // Existing access-control-allow-origin header found
+ const value = details.responseHeaders[idx].value;
+
+ // If HTTP protocol is used, change it to HTTPS
+ if (value.match(/http:/)) {
+ details.responseHeaders[idx].value = value.replace(/http:/g, "https:");
+ responseHeadersChanged = true;
+ }
+ }
+ }
+
+ if (!cspHeaderFound) {
+ // CSP headers not found
+ const upgradeInsecureRequests = {
+ name: 'Content-Security-Policy',
+ value: 'upgrade-insecure-requests'
+ }
+ details.responseHeaders.push(upgradeInsecureRequests);
+ responseHeadersChanged = true;
+ }
+
+ if (responseHeadersChanged) {
+ return {responseHeaders: details.responseHeaders};
+ }
+ }
+ return {};
+}
+
+// Registers the handler for requests
+// See: https://github.com/EFForg/https-everywhere/issues/10039
+chrome.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["*://*/*", "ftp://*/*"]}, ["blocking"]);
+
+// Try to catch redirect loops on URLs we've redirected to HTTPS.
+chrome.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["https://*/*"]});
+
+// Cleanup redirectCounter if necessary
+chrome.webRequest.onCompleted.addListener(onCompleted, {urls: ["*://*/*"]});
+
+// Cleanup redirectCounter if necessary
+chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, {urls: ["*://*/*"]})
+
+// Insert upgrade-insecure-requests directive in httpNowhere mode
+chrome.webRequest.onHeadersReceived.addListener(onHeadersReceived, {urls: ["https://*/*"]}, ["blocking", "responseHeaders"]);
+
+// 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);
+ };
+
+ const responses = {
+ enable: () => {
+ enableSwitchPlannerFor(tabId);
+ port.onDisconnect.addListener(disableOnCloseCallback);
+ },
+ disable: () => {
+ disableSwitchPlannerFor(tabId);
+ },
+ getHosts: () => {
+ sendResponse({
+ nrw: sortSwitchPlanner(tabId, "nrw"),
+ rw: sortSwitchPlanner(tabId, "rw")
+ });
+ return true;
+ }
+ };
+ if (message.type in responses) {
+ return responses[message.type]();
+ }
+ });
+ }
+});
+
+// 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){
+
+ function get_update_channels_generic(update_channels){
+ let last_updated_promises = [];
+ for(let update_channel of update_channels) {
+ last_updated_promises.push(new Promise(resolve => {
+ store.local.get({['rulesets-timestamp: ' + update_channel.name]: 0}, item => {
+ resolve([update_channel.name, item['rulesets-timestamp: ' + update_channel.name]]);
+ });
+ }));
+ }
+ Promise.all(last_updated_promises).then(results => {
+ const last_updated = results.reduce((obj, item) => {
+ obj[item[0]] = item[1];
+ return obj;
+ }, {});
+ sendResponse({update_channels, last_updated});
+ });
+ }
+
+ const responses = {
+ get_option: () => {
+ store.get(message.object, sendResponse);
+ return true;
+ },
+ set_option: () => {
+ store.set(message.object, item => {
+ if (sendResponse) {
+ sendResponse(item);
+ }
+ });
+ },
+ delete_from_ruleset_cache: () => {
+ all_rules.ruleCache.delete(message.object);
+ },
+ get_active_rulesets: () => {
+ sendResponse(appliedRulesets.getRulesets(message.object));
+ return true;
+ },
+ set_ruleset_active_status: () => {
+ let rulesets = appliedRulesets.getRulesets(message.object.tab_id);
+
+ for (let ruleset of rulesets) {
+ if (ruleset.name == message.object.name) {
+ ruleset.active = message.object.active;
+ if (ruleset.default_state == message.object.active) {
+ message.object.active = undefined;
+ }
+ break;
+ }
+ }
+
+ all_rules.setRuleActiveState(message.object.name, message.object.active).then(() => {
+ sendResponse(true);
+ });
+
+ return true;
+ },
+ reset_to_defaults: () => {
+ // restore the 'default states' of the rulesets
+ store.set_promise('ruleActiveStates', {}).then(() => {
+ // clear the caches such that it becomes stateless
+ destroy_caches();
+ // re-activate all rules according to the new states
+ initializeAllRules();
+ // reload tabs when operations completed
+ chrome.tabs.reload();
+ });
+ },
+ add_new_rule: () => {
+ all_rules.addNewRuleAndStore(message.object).then(() => {
+ sendResponse(true);
+ });
+ return true;
+ },
+ remove_rule: () => {
+ all_rules.removeRuleAndStore(message.object);
+ },
+ get_ruleset_timestamps: () => {
+ update.getRulesetTimestamps().then(timestamps => sendResponse(timestamps));
+ return true;
+ },
+ get_pinned_update_channels: () => {
+ get_update_channels_generic(update_channels);
+ return true;
+ },
+ get_stored_update_channels: () => {
+ store.get({update_channels: []}, item => {
+ get_update_channels_generic(item.update_channels);
+ });
+ return true;
+ },
+ create_update_channel: () => {
+
+ store.get({update_channels: []}, item => {
+
+ const update_channel_names = update_channels.concat(item.update_channels).reduce((obj, item) => {
+ obj.add(item.name);
+ return obj;
+ }, new Set());
+
+ if(update_channel_names.has(message.object)){
+ return sendResponse(false);
+ }
+
+ item.update_channels.push({
+ name: message.object,
+ jwk: {},
+ update_path_prefix: ''
+ });
+
+ store.set({update_channels: item.update_channels}, () => {
+ sendResponse(true);
+ });
+
+ });
+ return true;
+ },
+ delete_update_channel: () => {
+ store.get({update_channels: []}, item => {
+ store.set({update_channels: item.update_channels.filter(update_channel => {
+ return (update_channel.name != message.object);
+ })}, () => {
+ store.local.remove([
+ 'rulesets-timestamp: ' + message.object,
+ 'rulesets-stored-timestamp: ' + message.object,
+ 'rulesets: ' + message.object
+ ], () => {
+ initializeAllRules();
+ sendResponse(true);
+ });
+ });
+ });
+ return true;
+ },
+ update_update_channel: () => {
+ store.get({update_channels: []}, item => {
+ for(let i = 0; i < item.update_channels.length; i++){
+ if(item.update_channels[i].name == message.object.name){
+ item.update_channels[i] = message.object;
+ }
+ }
+
+ store.set({update_channels: item.update_channels}, () => {
+ sendResponse(true);
+ });
+
+ });
+ return true;
+ },
+ get_last_checked: () => {
+ store.local.get({'last-checked': false}, item => {
+ sendResponse(item['last-checked']);
+ });
+ return true;
+ }
+ };
+ if (message.type in responses) {
+ return responses[message.type]();
+ }
+});
+
+/**
+ * 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);
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/bootstrap.js b/data/extensions/https-everywhere@eff.org/background-scripts/bootstrap.js
new file mode 100644
index 0000000..65d7a7a
--- /dev/null
+++ b/data/extensions/https-everywhere@eff.org/background-scripts/bootstrap.js
@@ -0,0 +1,9 @@
+"use strict";
+
+function require(module) {
+ if (module.startsWith('./') && require.scopes.hasOwnProperty(module.slice(2))) {
+ return require.scopes[module.slice(2)];
+ }
+ throw new Error('module: ' + module + ' not found.');
+}
+require.scopes = {};
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/incognito.js b/data/extensions/https-everywhere@eff.org/background-scripts/incognito.js
new file mode 100644
index 0000000..ca52177
--- /dev/null
+++ b/data/extensions/https-everywhere@eff.org/background-scripts/incognito.js
@@ -0,0 +1,73 @@
+"use strict";
+
+(function(exports) {
+
+// This file keeps track of incognito sessions, and clears any caches after
+// an entire incognito session is closed (i.e. all incognito windows are closed).
+
+let state = {
+ incognito_session_exists: false,
+};
+
+function Incognito(onIncognitoDestruction) {
+ Object.assign(this, {onIncognitoDestruction});
+ // Listen to window creation, so we can detect if an incognito window is created
+ if (chrome.windows) {
+ chrome.windows.onCreated.addListener(this.detect_incognito_creation);
+ }
+
+ // Listen to window destruction, so we can clear caches if all incognito windows are destroyed
+ if (chrome.windows) {
+ chrome.windows.onRemoved.addListener(this.detect_incognito_destruction);
+ }
+}
+
+Incognito.prototype = {
+ /**
+ * Detect if an incognito session is created, so we can clear caches when it's destroyed.
+ *
+ * @param window: A standard Window object.
+ */
+ detect_incognito_creation: function(window_) {
+ if (window_.incognito === true) {
+ state.incognito_session_exists = true;
+ }
+ },
+
+ // If a window is destroyed, and an incognito session existed, see if it still does.
+ detect_incognito_destruction: async function() {
+ if (state.incognito_session_exists) {
+ if (!(await any_incognito_windows())) {
+ state.incognito_session_exists = false;
+ this.onIncognitoDestruction();
+ }
+ }
+ },
+}
+
+/**
+ * Check if any incognito window still exists
+ */
+function any_incognito_windows() {
+ return new Promise(resolve => {
+ chrome.windows.getAll(arrayOfWindows => {
+ for (let window_ of arrayOfWindows) {
+ if (window_.incognito === true) {
+ return resolve(true);
+ }
+ }
+ resolve(false);
+ });
+ });
+}
+
+function onIncognitoDestruction(callback) {
+ return new Incognito(callback);
+};
+
+Object.assign(exports, {
+ onIncognitoDestruction,
+ state,
+});
+
+})(typeof exports == 'undefined' ? require.scopes.incognito = {} : exports);
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/rules.js b/data/extensions/https-everywhere@eff.org/background-scripts/rules.js
new file mode 100644
index 0000000..e2dac94
--- /dev/null
+++ b/data/extensions/https-everywhere@eff.org/background-scripts/rules.js
@@ -0,0 +1,690 @@
+"use strict";
+
+(function(exports) {
+
+const util = require('./util');
+
+let settings = {
+ enableMixedRulesets: false,
+ domainBlacklist: new Set(),
+};
+
+// To reduce memory usage for the numerous rules/cookies with trivial rules
+const trivial_cookie_rule_c = /.+/;
+
+// Empty iterable singleton to reduce memory usage
+const nullIterable = Object.create(null, {
+ [Symbol.iterator]: {
+ value: function* () {
+ // do nothing
+ }
+ },
+
+ size: {
+ value: 0
+ },
+});
+
+/**
+ * Constructs a single rule
+ * @param from
+ * @param to
+ * @constructor
+ */
+function Rule(from, to) {
+ this.from_c = new RegExp(from);
+ this.to = to;
+}
+
+// To reduce memory usage for the numerous rules/cookies with trivial rules
+const trivial_rule = new Rule("^http:", "https:");
+
+/**
+ * Returns a common trivial rule or constructs a new one.
+ */
+function getRule(from, to) {
+ if (from === "^http:" && to === "https:") {
+ // This is a trivial rule, rewriting http->https with no complex RegExp.
+ return trivial_rule;
+ } else {
+ // This is a non-trivial rule.
+ return new Rule(from, to);
+ }
+}
+
+/**
+ * Generates a CookieRule
+ * @param host The host regex to compile
+ * @param cookiename The cookie name Regex to compile
+ * @constructor
+ */
+function CookieRule(host, cookiename) {
+ if (host === '.+') {
+ // Some cookie rules trivially match any host.
+ this.host_c = trivial_cookie_rule_c;
+ } else {
+ this.host_c = new RegExp(host);
+ }
+
+ if (cookiename === '.+') {
+ // About 50% of cookie rules trivially match any name.
+ this.name_c = trivial_cookie_rule_c;
+ } else {
+ this.name_c = new RegExp(cookiename);
+ }
+}
+
+/**
+ *A collection of rules
+ * @param set_name The name of this set
+ * @param default_state activity state
+ * @param note Note will be displayed in popup
+ * @constructor
+ */
+function RuleSet(set_name, default_state, note) {
+ this.name = set_name;
+ this.rules = [];
+ this.exclusions = null;
+ this.cookierules = null;
+ this.active = default_state;
+ this.default_state = default_state;
+ this.note = note;
+}
+
+RuleSet.prototype = {
+ /**
+ * Check if a URI can be rewritten and rewrite it
+ * @param urispec The uri to rewrite
+ * @returns {*} null or the rewritten uri
+ */
+ apply: function(urispec) {
+ var returl = null;
+ // If we're covered by an exclusion, go home
+ if (this.exclusions !== null && this.exclusions.test(urispec)) {
+ util.log(util.DBUG, "excluded uri " + urispec);
+ return null;
+ }
+
+ // Okay, now find the first rule that triggers
+ for (let rule of this.rules) {
+ returl = urispec.replace(rule.from_c,
+ rule.to);
+ if (returl != urispec) {
+ return returl;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Deep equivalence comparison
+ * @param ruleset The ruleset to compare with
+ * @returns true or false, depending on whether it's deeply equivalent
+ */
+ isEquivalentTo: function(ruleset) {
+ if(this.name != ruleset.name ||
+ this.note != ruleset.note ||
+ this.state != ruleset.state ||
+ this.default_state != ruleset.default_state) {
+ return false;
+ }
+
+ try {
+ var this_exclusions_source = this.exclusions.source;
+ } catch(e) {
+ var this_exclusions_source = null;
+ }
+
+ try {
+ var ruleset_exclusions_source = ruleset.exclusions.source;
+ } catch(e) {
+ var ruleset_exclusions_source = null;
+ }
+
+ try {
+ var this_rules_length = this.rules.length;
+ } catch(e) {
+ var this_rules_length = 0;
+ }
+
+ try {
+ var ruleset_rules_length = ruleset.rules.length;
+ } catch(e) {
+ var ruleset_rules_length = 0;
+ }
+
+ if(this_rules_length != ruleset_rules_length) {
+ return false;
+ }
+
+ if (this_exclusions_source != ruleset_exclusions_source) {
+ return false;
+ }
+
+ if(this_rules_length > 0) {
+ for(let x = 0; x < this.rules.length; x++){
+ if(this.rules[x].to != ruleset.rules[x].to) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+};
+
+/**
+ * Initialize Rule Sets
+ * @param ruleActiveStates default state for rules
+ * @constructor
+ */
+function RuleSets() {
+ // Load rules into structure
+ this.targets = new Map();
+
+ // A cache for potentiallyApplicableRulesets
+ this.ruleCache = new Map();
+
+ // A cache for cookie hostnames.
+ this.cookieHostCache = new Map();
+
+ // A hash of rule name -> active status (true/false).
+ this.ruleActiveStates = {};
+
+ // The key to retrieve user rules from the storage api
+ this.USER_RULE_KEY = 'userRules';
+
+ return this;
+}
+
+
+RuleSets.prototype = {
+
+ /**
+ * Load packaged rulesets, and rulesets in browser storage
+ * @param store object from store.js
+ */
+ loadFromBrowserStorage: async function(store, applyStoredFunc) {
+ this.store = store;
+ this.ruleActiveStates = await this.store.get_promise('ruleActiveStates', {});
+ await applyStoredFunc(this);
+ this.loadStoredUserRules();
+ await this.addStoredCustomRulesets();
+ },
+
+ /**
+ * Iterate through data XML and load rulesets
+ */
+ addFromXml: function(ruleXml) {
+ var sets = ruleXml.getElementsByTagName("ruleset");
+ for (let s of sets) {
+ try {
+ this.parseOneXmlRuleset(s);
+ } catch (e) {
+ util.log(util.WARN, 'Error processing ruleset:' + e);
+ }
+ }
+ },
+
+ addFromJson: function(ruleJson) {
+ for (let ruleset of ruleJson) {
+ try {
+ this.parseOneJsonRuleset(ruleset);
+ } catch(e) {
+ util.log(util.WARN, 'Error processing ruleset:' + e);
+ }
+ }
+ },
+
+ parseOneJsonRuleset: function(ruletag) {
+ var default_state = true;
+ var note = "";
+ var default_off = ruletag["default_off"];
+ if (default_off) {
+ default_state = false;
+ note += default_off + "\n";
+ }
+
+ // If a ruleset declares a platform, and we don't match it, treat it as
+ // off-by-default. In practice, this excludes "mixedcontent" rules.
+ var platform = ruletag["platform"]
+ if (platform) {
+ default_state = false;
+ if (platform == "mixedcontent" && settings.enableMixedRulesets) {
+ default_state = true;
+ }
+ note += "Platform(s): " + platform + "\n";
+ }
+
+ var rule_set = new RuleSet(ruletag["name"], default_state, note.trim());
+
+ // Read user prefs
+ if (rule_set.name in this.ruleActiveStates) {
+ rule_set.active = this.ruleActiveStates[rule_set.name];
+ }
+
+ var rules = ruletag["rule"];
+ for (let rule of rules) {
+ if (rule["from"] != null && rule["to"] != null) {
+ rule_set.rules.push(getRule(rule["from"], rule["to"]));
+ }
+ }
+
+ var exclusions = ruletag["exclusion"];
+ if (exclusions != null) {
+ rule_set.exclusions = new RegExp(exclusions.join("|"));
+ }
+
+ var cookierules = ruletag["securecookie"];
+ if (cookierules != null) {
+ for (let cookierule of cookierules) {
+ if (cookierule["host"] != null && cookierule["name"] != null) {
+ if (!rule_set.cookierules) {
+ rule_set.cookierules = [];
+ }
+ rule_set.cookierules.push(new CookieRule(cookierule["host"], cookierule["name"]));
+ }
+ }
+ }
+
+ var targets = ruletag["target"];
+ for (let target of targets) {
+ if (target != null) {
+ if (!this.targets.has(target)) {
+ this.targets.set(target, []);
+ }
+ this.targets.get(target).push(rule_set);
+ }
+ }
+ },
+
+ /**
+ * Load a user rule
+ * @param params
+ * @returns {boolean}
+ */
+ addUserRule : function(params) {
+ util.log(util.INFO, 'adding new user rule for ' + JSON.stringify(params));
+ var new_rule_set = new RuleSet(params.host, true, "user rule");
+ var new_rule = getRule(params.urlMatcher, params.redirectTo);
+ new_rule_set.rules.push(new_rule);
+ if (!this.targets.has(params.host)) {
+ this.targets.set(params.host, []);
+ }
+ this.ruleCache.delete(params.host);
+ // TODO: maybe promote this rule?
+ this.targets.get(params.host).push(new_rule_set);
+ if (new_rule_set.name in this.ruleActiveStates) {
+ new_rule_set.active = this.ruleActiveStates[new_rule_set.name];
+ }
+ util.log(util.INFO, 'done adding rule');
+ return true;
+ },
+
+ /**
+ * Remove a user rule
+ * @param params
+ * @returns {boolean}
+ */
+ removeUserRule: function(ruleset) {
+ util.log(util.INFO, 'removing user rule for ' + JSON.stringify(ruleset));
+ this.ruleCache.delete(ruleset.name);
+
+
+ var tmp = this.targets.get(ruleset.name).filter(r =>
+ !(r.isEquivalentTo(ruleset))
+ );
+ this.targets.set(ruleset.name, tmp);
+
+ if (this.targets.get(ruleset.name).length == 0) {
+ this.targets.delete(ruleset.name);
+ }
+
+ util.log(util.INFO, 'done removing rule');
+ return true;
+ },
+
+ /**
+ * Retrieve stored user rules from storage api
+ **/
+ getStoredUserRules: async function() {
+ return await this.store.get_promise(this.USER_RULE_KEY, []);
+ },
+
+ /**
+ * Load all stored user rules into this RuleSet object
+ */
+ loadStoredUserRules: async function() {
+ const user_rules = await this.getStoredUserRules();
+ for (let user_rule of user_rules) {
+ this.addUserRule(user_rule);
+ }
+ util.log(util.INFO, 'loaded ' + user_rules.length + ' stored user rules');
+ },
+
+ /**
+ * Adds a new user rule
+ * @param params: params defining the rule
+ * @param cb: Callback to call after success/fail
+ * */
+ addNewRuleAndStore: async function(params) {
+ if (this.addUserRule(params)) {
+ // If we successfully added the user rule, save it in the storage
+ // api so it's automatically applied when the extension is
+ // reloaded.
+ let userRules = await this.getStoredUserRules();
+ // TODO: there's a race condition here, if this code is ever executed from multiple
+ // client windows in different event loops.
+ userRules.push(params);
+ // TODO: can we exceed the max size for storage?
+ await this.store.set_promise(this.USER_RULE_KEY, userRules);
+ }
+ },
+
+ /**
+ * Removes a user rule
+ * @param ruleset: the ruleset to remove
+ * */
+ removeRuleAndStore: async function(ruleset) {
+ if (this.removeUserRule(ruleset)) {
+ // If we successfully removed the user rule, remove it in local storage too
+ let userRules = await this.getStoredUserRules();
+ userRules = userRules.filter(r =>
+ !(r.host == ruleset.name &&
+ r.redirectTo == ruleset.rules[0].to)
+ );
+ await this.store.set_promise(this.USER_RULE_KEY, userRules);
+ }
+ },
+
+ addStoredCustomRulesets: function(){
+ return new Promise(resolve => {
+ this.store.get({
+ legacy_custom_rulesets: [],
+ debugging_rulesets: ""
+ }, item => {
+ this.loadCustomRulesets(item.legacy_custom_rulesets);
+ this.loadCustomRuleset("<root>" + item.debugging_rulesets + "</root>");
+ resolve();
+ });
+ });
+ },
+
+ // Load in the legacy custom rulesets, if any
+ loadCustomRulesets: function(legacy_custom_rulesets){
+ for(let legacy_custom_ruleset of legacy_custom_rulesets){
+ this.loadCustomRuleset(legacy_custom_ruleset);
+ }
+ },
+
+ loadCustomRuleset: function(ruleset_string){
+ this.addFromXml((new DOMParser()).parseFromString(ruleset_string, 'text/xml'));
+ },
+
+ setRuleActiveState: async function(ruleset_name, active){
+ if (active == undefined) {
+ delete this.ruleActiveStates[ruleset_name];
+ } else {
+ this.ruleActiveStates[ruleset_name] = active;
+ }
+ await this.store.set_promise('ruleActiveStates', this.ruleActiveStates);
+ },
+
+ /**
+ * Does the loading of a ruleset.
+ * @param ruletag The whole <ruleset> tag to parse
+ */
+ parseOneXmlRuleset: function(ruletag) {
+ var default_state = true;
+ var note = "";
+ var default_off = ruletag.getAttribute("default_off");
+ if (default_off) {
+ default_state = false;
+ note += default_off + "\n";
+ }
+
+ // If a ruleset declares a platform, and we don't match it, treat it as
+ // off-by-default. In practice, this excludes "mixedcontent" rules.
+ var platform = ruletag.getAttribute("platform");
+ if (platform) {
+ default_state = false;
+ if (platform == "mixedcontent" && settings.enableMixedRulesets) {
+ default_state = true;
+ }
+ note += "Platform(s): " + platform + "\n";
+ }
+
+ var rule_set = new RuleSet(ruletag.getAttribute("name"),
+ default_state,
+ note.trim());
+
+ // Read user prefs
+ if (rule_set.name in this.ruleActiveStates) {
+ rule_set.active = (this.ruleActiveStates[rule_set.name] == "true");
+ }
+
+ var rules = ruletag.getElementsByTagName("rule");
+ for (let rule of rules) {
+ rule_set.rules.push(getRule(rule.getAttribute("from"),
+ rule.getAttribute("to")));
+ }
+
+ var exclusions = Array();
+ for (let exclusion of ruletag.getElementsByTagName("exclusion")) {
+ exclusions.push(exclusion.getAttribute("pattern"));
+ }
+ if (exclusions.length > 0) {
+ rule_set.exclusions = new RegExp(exclusions.join("|"));
+ }
+
+ var cookierules = ruletag.getElementsByTagName("securecookie");
+ if (cookierules.length > 0) {
+ rule_set.cookierules = [];
+ for (let cookierule of cookierules) {
+ rule_set.cookierules.push(
+ new CookieRule(cookierule.getAttribute("host"),
+ cookierule.getAttribute("name")));
+ }
+ }
+
+ var targets = ruletag.getElementsByTagName("target");
+ for (let target of targets) {
+ var host = target.getAttribute("host");
+ if (!this.targets.has(host)) {
+ this.targets.set(host, []);
+ }
+ this.targets.get(host).push(rule_set);
+ }
+ },
+
+ /**
+ * Return a list of rulesets that apply to this host
+ * @param host The host to check
+ * @returns {*} (empty) list
+ */
+ potentiallyApplicableRulesets: function(host) {
+ // Have we cached this result? If so, return it!
+ if (this.ruleCache.has(host)) {
+ let cached_item = this.ruleCache.get(host);
+ util.log(util.DBUG, "Ruleset cache hit for " + host + " items:" + cached_item.size);
+ return cached_item;
+ } else {
+ util.log(util.DBUG, "Ruleset cache miss for " + host);
+ }
+
+ // Let's begin search
+ // Copy the host targets so we don't modify them.
+ let results = (this.targets.has(host) ?
+ new Set([...this.targets.get(host)]) :
+ new Set());
+
+ // Ensure host is well-formed (RFC 1035)
+ if (host.length <= 0 || host.length > 255 || host.indexOf("..") != -1) {
+ util.log(util.WARN, "Malformed host passed to potentiallyApplicableRulesets: " + host);
+ return nullIterable;
+ }
+
+ // Replace each portion of the domain with a * in turn
+ let segmented = host.split(".");
+ for (let i = 0; i < segmented.length; i++) {
+ let tmp = segmented[i];
+ segmented[i] = "*";
+
+ results = (this.targets.has(segmented.join(".")) ?
+ new Set([...results, ...this.targets.get(segmented.join("."))]) :
+ results);
+
+ segmented[i] = tmp;
+ }
+
+ // now eat away from the left, with *, so that for x.y.z.google.com we
+ // check *.z.google.com and *.google.com (we did *.y.z.google.com above)
+ for (let i = 2; i <= segmented.length - 2; i++) {
+ let t = "*." + segmented.slice(i, segmented.length).join(".");
+
+ results = (this.targets.has(t) ?
+ new Set([...results, ...this.targets.get(t)]) :
+ results);
+ }
+
+ // Clean the results list, which may contain duplicates or undefined entries
+ results.delete(undefined);
+
+ util.log(util.DBUG,"Applicable rules for " + host + ":");
+ if (results.size == 0) {
+ util.log(util.DBUG, " None");
+ results = nullIterable;
+ } else {
+ results.forEach(result => util.log(util.DBUG, " " + result.name));
+ }
+
+ // Insert results into the ruleset cache
+ this.ruleCache.set(host, results);
+
+ // Cap the size of the cache. (Limit chosen somewhat arbitrarily)
+ if (this.ruleCache.size > 1000) {
+ // Map.prototype.keys() returns keys in insertion order, so this is a FIFO.
+ this.ruleCache.delete(this.ruleCache.keys().next().value);
+ }
+
+ return results;
+ },
+
+ /**
+ * Check to see if the Cookie object c meets any of our cookierule criteria for being marked as secure.
+ * @param cookie The cookie to test
+ * @returns {*} ruleset or null
+ */
+ shouldSecureCookie: function(cookie) {
+ var hostname = cookie.domain;
+ // cookie domain scopes can start with .
+ while (hostname.charAt(0) == ".") {
+ hostname = hostname.slice(1);
+ }
+
+ if (!this.safeToSecureCookie(hostname)) {
+ return null;
+ }
+
+ var potentiallyApplicable = this.potentiallyApplicableRulesets(hostname);
+ for (let ruleset of potentiallyApplicable) {
+ if (ruleset.cookierules !== null && ruleset.active) {
+ for (let cookierules of ruleset.cookierules) {
+ var cr = cookierules;
+ if (cr.host_c.test(cookie.domain) && cr.name_c.test(cookie.name)) {
+ return ruleset;
+ }
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Check if it is secure to secure the cookie (=patch the secure flag in).
+ * @param domain The domain of the cookie
+ * @returns {*} true or false
+ */
+ safeToSecureCookie: function(domain) {
+ // Check if the domain might be being served over HTTP. If so, it isn't
+ // safe to secure a cookie! We can't always know this for sure because
+ // observing cookie-changed doesn't give us enough context to know the
+ // full origin URI.
+
+ // First, if there are any redirect loops on this domain, don't secure
+ // cookies. XXX This is not a very satisfactory heuristic. Sometimes we
+ // would want to secure the cookie anyway, because the URLs that loop are
+ // not authenticated or not important. Also by the time the loop has been
+ // observed and the domain blacklisted, a cookie might already have been
+ // flagged as secure.
+
+ if (settings.domainBlacklist.has(domain)) {
+ util.log(util.INFO, "cookies for " + domain + "blacklisted");
+ return false;
+ }
+ var cached_item = this.cookieHostCache.get(domain);
+ if (cached_item !== undefined) {
+ util.log(util.DBUG, "Cookie host cache hit for " + domain);
+ return cached_item;
+ }
+ util.log(util.DBUG, "Cookie host cache miss for " + domain);
+
+ // If we passed that test, make up a random URL on the domain, and see if
+ // we would HTTPSify that.
+
+ var nonce_path = "/" + Math.random().toString();
+ var test_uri = "http://" + domain + nonce_path + nonce_path;
+
+ // Cap the size of the cookie cache (limit chosen somewhat arbitrarily)
+ if (this.cookieHostCache.size > 250) {
+ // Map.prototype.keys() returns keys in insertion order, so this is a FIFO.
+ this.cookieHostCache.delete(this.cookieHostCache.keys().next().value);
+ }
+
+ util.log(util.INFO, "Testing securecookie applicability with " + test_uri);
+ var potentiallyApplicable = this.potentiallyApplicableRulesets(domain);
+ for (let ruleset of potentiallyApplicable) {
+ if (!ruleset.active) {
+ continue;
+ }
+ if (ruleset.apply(test_uri)) {
+ util.log(util.INFO, "Cookie domain could be secured.");
+ this.cookieHostCache.set(domain, true);
+ return true;
+ }
+ }
+ util.log(util.INFO, "Cookie domain could NOT be secured.");
+ this.cookieHostCache.set(domain, false);
+ return false;
+ },
+
+ /**
+ * Rewrite an URI
+ * @param urispec The uri to rewrite
+ * @param host The host of this uri
+ * @returns {*} the new uri or null
+ */
+ rewriteURI: function(urispec, host) {
+ var newuri = null;
+ var potentiallyApplicable = this.potentiallyApplicableRulesets(host);
+ for (let ruleset of potentiallyApplicable) {
+ if (ruleset.active && (newuri = ruleset.apply(urispec))) {
+ return newuri;
+ }
+ }
+ return null;
+ }
+};
+
+Object.assign(exports, {
+ nullIterable,
+ settings,
+ trivial_rule,
+ Rule,
+ RuleSet,
+ RuleSets,
+ getRule
+});
+
+})(typeof exports == 'undefined' ? require.scopes.rules = {} : exports);
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/store.js b/data/extensions/https-everywhere@eff.org/background-scripts/store.js
new file mode 100644
index 0000000..3b555c7
--- /dev/null
+++ b/data/extensions/https-everywhere@eff.org/background-scripts/store.js
@@ -0,0 +1,103 @@
+"use strict";
+
+(function(exports) {
+
+const rules = require('./rules');
+
+function initialize() {
+ return new Promise(resolve => {
+ if (chrome.storage.sync) {
+ chrome.storage.sync.set({"sync-set-test": true}, () => {
+ if(chrome.runtime.lastError){
+ setStorage(chrome.storage.local);
+ } else {
+ setStorage(chrome.storage.sync);
+ }
+ resolve();
+ });
+ } else {
+ setStorage(chrome.storage.local);
+ resolve();
+ }
+ });
+}
+
+/* Storage promise setters and getters */
+
+function generic_get_promise(key, default_val, storage) {
+ return new Promise(res => storage.get({[key]: default_val}, data => res(data[key])));
+}
+
+function generic_set_promise(key, value, storage) {
+ return new Promise(res => storage.set({[key]: value}, res));
+}
+
+function get_promise(key, default_val) {
+ return generic_get_promise(key, default_val, exports);
+}
+
+function set_promise(key, value) {
+ return generic_set_promise(key, value, exports);
+}
+
+function local_get_promise(key, default_val) {
+ return generic_get_promise(key, default_val, chrome.storage.local);
+}
+
+function local_set_promise(key, value) {
+ return generic_set_promise(key, value, chrome.storage.local);
+}
+
+
+
+async function performMigrations() {
+ const migration_version = await get_promise('migration_version', 0);
+
+ if (migration_version < 1) {
+
+ let ls;
+ try {
+ ls = localStorage;
+ } catch(e) {}
+
+ let ruleActiveStates = {};
+ for (const key in ls) {
+ if (ls.hasOwnProperty(key)) {
+ if (key == rules.RuleSets().USER_RULE_KEY){
+ await set_promise(rules.RuleSets().USER_RULE_KEY, JSON.parse(ls[key]));
+ } else {
+ ruleActiveStates[key] = (ls[key] == "true");
+ }
+ }
+ }
+ await set_promise('ruleActiveStates', ruleActiveStates);
+ }
+
+ await set_promise('migration_version', 1);
+}
+
+const local = {
+ get: chrome.storage.local.get,
+ set: chrome.storage.local.set,
+ remove: chrome.storage.local.remove,
+ get_promise: local_get_promise,
+ set_promise: local_set_promise
+};
+
+function setStorage(store) {
+ Object.assign(exports, {
+ get: store.get.bind(store),
+ set: store.set.bind(store),
+ remove: store.remove.bind(store),
+ get_promise,
+ set_promise,
+ local
+ });
+}
+
+Object.assign(exports, {
+ initialize,
+ performMigrations
+});
+
+})(typeof exports == 'undefined' ? require.scopes.store = {} : exports);
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/update.js b/data/extensions/https-everywhere@eff.org/background-scripts/update.js
new file mode 100644
index 0000000..795f968
--- /dev/null
+++ b/data/extensions/https-everywhere@eff.org/background-scripts/update.js
@@ -0,0 +1,280 @@
+/* global pako */
+
+"use strict";
+
+let combined_update_channels;
+const { update_channels } = require('./update_channels');
+
+// Determine if we're in the tests. If so, define some necessary components.
+if (typeof window === "undefined") {
+ var WebCrypto = require("node-webcrypto-ossl"),
+ crypto = new WebCrypto(),
+ atob = require("atob"),
+ btoa = require("btoa"),
+ pako = require('../external/pako-1.0.5/pako_inflate.min.js'),
+ { TextDecoder } = require('text-encoding'),
+ chrome = require("sinon-chrome"),
+ window = { atob, btoa, chrome, crypto, pako, TextDecoder };
+
+ combined_update_channels = update_channels;
+}
+
+(function(exports) {
+
+const util = require('./util');
+
+let store,
+ background_callback;
+
+// how often we should check for new rulesets
+const periodicity = 86400;
+
+let imported_keys;
+
+// update channels are loaded from `background-scripts/update_channels.js` as well as the storage api
+async function loadUpdateChannelsKeys() {
+ util.log(util.NOTE, 'Loading update channels and importing associated public keys.');
+
+ const stored_update_channels = await store.get_promise('update_channels', []);
+ const combined_update_channels_preflight = update_channels.concat(stored_update_channels);
+
+ imported_keys = {};
+ combined_update_channels = [];
+
+ for(let update_channel of combined_update_channels_preflight){
+
+ try{
+ imported_keys[update_channel.name] = await window.crypto.subtle.importKey(
+ "jwk",
+ update_channel.jwk,
+ {
+ name: "RSA-PSS",
+ hash: {name: "SHA-256"},
+ },
+ false,
+ ["verify"]
+ );
+ combined_update_channels.push(update_channel);
+ } catch(err) {
+ util.log(util.WARN, update_channel.name + ': Could not import key. Aborting.');
+ }
+ }
+}
+
+
+// Determine the time until we should check for new rulesets
+async function timeToNextCheck() {
+ const last_checked = await store.local.get_promise('last-checked', false);
+ if(last_checked) {
+ const current_timestamp = Date.now() / 1000;
+ const secs_since_last_checked = current_timestamp - last_checked;
+ return Math.max(0, periodicity - secs_since_last_checked);
+ } else {
+ return 0;
+ }
+}
+
+// Check for new rulesets. If found, return the timestamp. If not, return false
+async function checkForNewRulesets(update_channel) {
+ let timestamp_result = await fetch(update_channel.update_path_prefix + "/latest-rulesets-timestamp");
+ if(timestamp_result.status == 200) {
+ let rulesets_timestamp = Number(await timestamp_result.text());
+
+ if((await store.local.get_promise('rulesets-timestamp: ' + update_channel.name, 0)) < rulesets_timestamp){
+ return rulesets_timestamp;
+ }
+ }
+ return false;
+}
+
+// Retrieve the timestamp for when a stored ruleset bundle was published
+async function getRulesetTimestamps(){
+ let timestamp_promises = [];
+ for(let update_channel of combined_update_channels){
+ timestamp_promises.push(new Promise(async resolve => {
+ let timestamp = await store.local.get_promise('rulesets-stored-timestamp: ' + update_channel.name, 0);
+ resolve([update_channel.name, timestamp]);
+ }));
+ }
+ let timestamps = await Promise.all(timestamp_promises);
+ return timestamps;
+}
+
+// Download and return new rulesets
+async function getNewRulesets(rulesets_timestamp, update_channel) {
+
+ store.local.set_promise('rulesets-timestamp: ' + update_channel.name, rulesets_timestamp);
+
+ let signature_promise = fetch(update_channel.update_path_prefix + "/rulesets-signature." + rulesets_timestamp + ".sha256");
+ let rulesets_promise = fetch(update_channel.update_path_prefix + "/default.rulesets." + rulesets_timestamp + ".gz");
+
+ let responses = await Promise.all([
+ signature_promise,
+ rulesets_promise
+ ]);
+
+ let resolutions = await Promise.all([
+ responses[0].arrayBuffer(),
+ responses[1].arrayBuffer()
+ ]);
+
+ return {
+ signature_array_buffer: resolutions[0],
+ rulesets_array_buffer: resolutions[1]
+ };
+}
+
+// Returns a promise which verifies that the rulesets have a valid EFF
+// signature, and if so, stores them and returns true.
+// Otherwise, it throws an exception.
+function verifyAndStoreNewRulesets(new_rulesets, rulesets_timestamp, update_channel){
+ return new Promise((resolve, reject) => {
+ window.crypto.subtle.verify(
+ {
+ name: "RSA-PSS",
+ saltLength: 32
+ },
+ imported_keys[update_channel.name],
+ new_rulesets.signature_array_buffer,
+ new_rulesets.rulesets_array_buffer
+ ).then(async isvalid => {
+ if(isvalid) {
+ util.log(util.NOTE, update_channel.name + ': Downloaded ruleset signature checks out. Storing rulesets.');
+
+ const rulesets_gz = util.ArrayBufferToString(new_rulesets.rulesets_array_buffer);
+ const rulesets_byte_array = pako.inflate(rulesets_gz);
+ const rulesets = new TextDecoder("utf-8").decode(rulesets_byte_array);
+ const rulesets_json = JSON.parse(rulesets);
+
+ if(rulesets_json.timestamp != rulesets_timestamp){
+ reject(update_channel.name + ': Downloaded ruleset had an incorrect timestamp. This may be an attempted downgrade attack. Aborting.');
+ } else {
+ await store.local.set_promise('rulesets: ' + update_channel.name, window.btoa(rulesets_gz));
+ resolve(true);
+ }
+ } else {
+ reject(update_channel.name + ': Downloaded ruleset signature is invalid. Aborting.');
+ }
+ }).catch(() => {
+ reject(update_channel.name + ': Downloaded ruleset signature could not be verified. Aborting.');
+ });
+ });
+}
+
+// Unzip and apply the rulesets we have stored.
+async function applyStoredRulesets(rulesets_obj){
+ let rulesets_promises = [];
+ for(let update_channel of combined_update_channels){
+ rulesets_promises.push(new Promise(resolve => {
+ const key = 'rulesets: ' + update_channel.name;
+ chrome.storage.local.get(key, root => {
+ if(root[key]){
+ util.log(util.NOTE, update_channel.name + ': Applying stored rulesets.');
+
+ const rulesets_gz = window.atob(root[key]);
+ const rulesets_byte_array = pako.inflate(rulesets_gz);
+ const rulesets = new TextDecoder("utf-8").decode(rulesets_byte_array);
+ const rulesets_json = JSON.parse(rulesets);
+
+ resolve(rulesets_json);
+ } else {
+ resolve();
+ }
+ });
+ }));
+ }
+
+ const rulesets_jsons = await Promise.all(rulesets_promises);
+ if(rulesets_jsons.join("").length > 0){
+ for(let rulesets_json of rulesets_jsons){
+ if(typeof(rulesets_json) != 'undefined'){
+ rulesets_obj.addFromJson(rulesets_json.rulesets);
+ }
+ }
+ } else {
+ rulesets_obj.addFromJson(util.loadExtensionFile('rules/default.rulesets', 'json'));
+ }
+}
+
+// basic workflow for periodic checks
+async function performCheck() {
+ util.log(util.NOTE, 'Checking for new rulesets.');
+
+ const current_timestamp = Date.now() / 1000;
+ store.local.set_promise('last-checked', current_timestamp);
+
+ let num_updates = 0;
+ for(let update_channel of combined_update_channels){
+ let new_rulesets_timestamp = await checkForNewRulesets(update_channel);
+ if(new_rulesets_timestamp){
+ util.log(util.NOTE, update_channel.name + ': A new ruleset bundle has been released. Downloading now.');
+ let new_rulesets = await getNewRulesets(new_rulesets_timestamp, update_channel);
+ try{
+ await verifyAndStoreNewRulesets(new_rulesets, new_rulesets_timestamp, update_channel);
+ store.local.set_promise('rulesets-stored-timestamp: ' + update_channel.name, new_rulesets_timestamp);
+ num_updates++;
+ } catch(err) {
+ util.log(util.WARN, update_channel.name + ': ' + err);
+ }
+ }
+ }
+ if(num_updates > 0){
+ background_callback();
+ }
+};
+
+chrome.storage.onChanged.addListener(async function(changes, areaName) {
+ if (areaName === 'sync' || areaName === 'local') {
+ if ('autoUpdateRulesets' in changes) {
+ if (changes.autoUpdateRulesets.newValue) {
+ await createTimer();
+ } else {
+ destroyTimer();
+ }
+ }
+ }
+
+ if ('update_channels' in changes) {
+ await loadUpdateChannelsKeys();
+ }
+});
+
+let initialCheck,
+ subsequentChecks;
+
+async function createTimer(){
+ const time_to_next_check = await timeToNextCheck();
+
+ initialCheck = setTimeout(() => {
+ performCheck();
+ subsequentChecks = setInterval(performCheck, periodicity * 1000);
+ }, time_to_next_check * 1000);
+}
+
+function destroyTimer(){
+ if (initialCheck) {
+ clearTimeout(initialCheck);
+ }
+ if (subsequentChecks) {
+ clearInterval(subsequentChecks);
+ }
+}
+
+async function initialize(store_param, cb){
+ store = store_param;
+ background_callback = cb;
+
+ await loadUpdateChannelsKeys();
+
+ if (await store.get_promise('autoUpdateRulesets', false)) {
+ await createTimer();
+ }
+}
+
+Object.assign(exports, {
+ applyStoredRulesets,
+ initialize,
+ getRulesetTimestamps
+});
+
+})(typeof exports == 'undefined' ? require.scopes.update = {} : exports);
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/update_channels.js b/data/extensions/https-everywhere@eff.org/background-scripts/update_channels.js
new file mode 100644
index 0000000..8bb21c9
--- /dev/null
+++ b/data/extensions/https-everywhere@eff.org/background-scripts/update_channels.js
@@ -0,0 +1,23 @@
+/* exported update_channels */
+
+'use strict';
+
+(function (exports) {
+
+exports.update_channels = [{
+ name: 'EFF (Full)',
+ jwk: {
+ kty: 'RSA',
+ e: 'AQAB',
+ n: '1cwvFQu3Kw-Pz8bcEFuV5zx0ZheDsc4Tva7Qv6BL90_sDLqCW79Y543nDkPtNVfFH_89pt2kSPp_IcS5XnYiw6zBQeFuILFw5JpvZt14K0s4' +
+ 'e025Q9CXfhYKIBKT9PnqihwAacjMa6rQb7RTu7XxVvqxRb3b0vx2CR40LSlYZ8H_KpeaUwq2oz-fyrI6LFTeYvbO3ZuLKeK5xV1a32xeTVMF' +
+ 'kIj3LxnQalxq-DRHfj7LRRoTnbRDW4uoDc8aVpLFliuO79jUKbobz4slpiWJ4wjKR_O6OK13HbZUiOSxi8Bms-UqBPOyzbMVpmA7lv_zWdaL' +
+ 'u1IVlVXQyLVbbrqI6llRqfHdcJoEl-eC48AofuB-relQtjTEK_hyBf7sPwrbqAarjRjlyEx6Qy5gTXyxM9attfNAeupYR6jm8LKm6TFpfWky' +
+ 'DxUmj_f5pJMBWNTomV74f8iQ2M18_KWMUDCOf80tR0t21Q1iCWdvA3K_KJn05tTLyumlwwlQijMqRkYuao-CX9L3DJIaB3VPYPTSIPUr7oi1' +
+ '6agsuamOyiOtlZiRpEvoNg2ksJMZtwnj5xhBQydkdhMW2ZpHDzcLuZlhJYZL_l3_7wuzRM7vpyA9obP92CpZRFJErGZmFxJC93I4U9-0B0wg' +
+ '-sbyMKGJ5j1BWTnibCklDXtWzXtuiz18EgE'
+ },
+ update_path_prefix: 'https://www.https-rulesets.org/v1/'
+}];
+
+})(typeof exports === 'undefined' ? require.scopes.update_channels = {} : exports);
diff --git a/data/extensions/https-everywhere@eff.org/background-scripts/util.js b/data/extensions/https-everywhere@eff.org/background-scripts/util.js
new file mode 100644
index 0000000..06c87ad
--- /dev/null
+++ b/data/extensions/https-everywhere@eff.org/background-scripts/util.js
@@ -0,0 +1,93 @@
+"use strict";
+
+(function(exports) {
+
+var VERB = 1;
+var DBUG = 2;
+var INFO = 3;
+var NOTE = 4;
+var WARN = 5;
+// FYI: Logging everything is /very/ slow. Chrome will log & buffer
+// these console logs even when the debug tools are closed. :(
+
+// TODO: Add an easy UI to change the log level.
+// (Developers can just type DEFAULT_LOG_LEVEL=VERB in the console)
+var DEFAULT_LOG_LEVEL = NOTE;
+console.log("Hey developer! Want to see more verbose logging?");
+console.log("Type this into the console: let util = require('./util'); util.setDefaultLogLevel(util.VERB);");
+console.log("Accepted levels are VERB, DBUG, INFO, NOTE and WARN, default is NOTE");
+
+function log(level, str) {
+ if (level >= DEFAULT_LOG_LEVEL) {
+ if (level === WARN) {
+ // Show warning with a little yellow icon in Chrome.
+ console.warn(str);
+ } else {
+ console.log(str);
+ }
+ }
+}
+
+function setDefaultLogLevel(level) {
+ DEFAULT_LOG_LEVEL = level;
+}
+
+function getDefaultLogLevel() {
+ return DEFAULT_LOG_LEVEL;
+}
+
+/**
+ * Load a file packaged with the extension
+ *
+ * @param url: a relative URL to local file
+ */
+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;
+ }
+ if (returnType === 'json') {
+ return JSON.parse(xhr.responseText);
+ }
+ return xhr.responseText;
+}
+
+/**
+ * Convert an ArrayBuffer to string
+ *
+ * @param array: an ArrayBuffer to convert
+ */
+function ArrayBufferToString(ab) {
+ let array = new Uint8Array(ab);
+ let string = "";
+
+ for (let byte of array){
+ string += String.fromCharCode(byte);
+ }
+
+ return string;
+}
+
+
+Object.assign(exports, {
+ VERB,
+ DBUG,
+ INFO,
+ NOTE,
+ WARN,
+ log,
+ setDefaultLogLevel,
+ getDefaultLogLevel,
+ loadExtensionFile,
+ ArrayBufferToString
+});
+
+})(typeof exports == 'undefined' ? require.scopes.util = {} : exports);