diff options
author | Ruben Rodriguez <ruben@gnu.org> | 2018-09-13 20:39:48 -0400 |
---|---|---|
committer | Ruben Rodriguez <ruben@gnu.org> | 2018-09-13 21:02:13 -0400 |
commit | d26b319fd6f98517cc3421f10bf18698b953e4d2 (patch) | |
tree | bc70c4e472a2eaf514d411dba5067d530e5bbea9 /data/extensions/https-everywhere@eff.org/background-scripts | |
parent | c3b304c51a3386ea09527a479a883253ea35243a (diff) |
Updated extensions list for v60
Diffstat (limited to 'data/extensions/https-everywhere@eff.org/background-scripts')
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); |