diff options
Diffstat (limited to 'src/js/webrequest.js')
-rw-r--r-- | src/js/webrequest.js | 1293 |
1 files changed, 1293 insertions, 0 deletions
diff --git a/src/js/webrequest.js b/src/js/webrequest.js new file mode 100644 index 0000000..bb7469b --- /dev/null +++ b/src/js/webrequest.js @@ -0,0 +1,1293 @@ +/* + * + * This file is part of Privacy Badger <https://www.eff.org/privacybadger> + * Copyright (C) 2016 Electronic Frontier Foundation + * + * Derived from Adblock Plus + * Copyright (C) 2006-2013 Eyeo GmbH + * + * Derived from Chameleon <https://github.com/ghostwords/chameleon> + * Copyright (C) 2015 ghostwords + * + * Privacy Badger is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * Privacy Badger is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Privacy Badger. If not, see <http://www.gnu.org/licenses/>. + */ + +/* globals badger:false, log:false */ + +require.scopes.webrequest = (function () { + +/*********************** webrequest scope **/ + +let constants = require("constants"), + getSurrogateURI = require("surrogates").getSurrogateURI, + incognito = require("incognito"), + utils = require("utils"); + +/************ Local Variables *****************/ +let tempAllowlist = {}; + +/***************** Blocking Listener Functions **************/ + +/** + * Event handling of http requests, main logic to collect data what to block + * + * @param {Object} details The event details + * @returns {Object} Can cancel requests + */ +function onBeforeRequest(details) { + let frame_id = details.frameId, + tab_id = details.tabId, + type = details.type, + url = details.url; + + if (type == "main_frame") { + let oldTabData = badger.getFrameData(tab_id), + is_reload = oldTabData && oldTabData.url == url; + forgetTab(tab_id, is_reload); + badger.recordFrame(tab_id, frame_id, url); + initializeAllowedWidgets(tab_id, badger.getFrameData(tab_id).host); + return {}; + } + + if (type == "sub_frame") { + badger.recordFrame(tab_id, frame_id, url); + } + + // Block ping requests sent by navigator.sendBeacon (see, #587) + // tabId for pings are always -1 due to Chrome bugs #522124 and #522129 + // Once these bugs are fixed, PB will treat pings as any other request + if (type == "ping" && tab_id < 0) { + return {cancel: true}; + } + + if (_isTabChromeInternal(tab_id)) { + return {}; + } + + let tab_host = getHostForTab(tab_id); + let request_host = window.extractHostFromURL(url); + + if (!utils.isThirdPartyDomain(request_host, tab_host)) { + return {}; + } + + let action = checkAction(tab_id, request_host, frame_id); + if (!action) { + return {}; + } + + badger.logThirdPartyOriginOnTab(tab_id, request_host, action); + + if (!badger.isPrivacyBadgerEnabled(tab_host)) { + return {}; + } + + if (action != constants.BLOCK && action != constants.USER_BLOCK) { + return {}; + } + + if (type == 'script') { + let surrogate = getSurrogateURI(url, request_host); + if (surrogate) { + return {redirectUrl: surrogate}; + } + } + + // notify the widget replacement content script + chrome.tabs.sendMessage(tab_id, { + replaceWidget: true, + trackerDomain: request_host + }); + + // if this is a heuristically- (not user-) blocked domain + if (action == constants.BLOCK && incognito.learningEnabled(tab_id)) { + // check for DNT policy asynchronously + setTimeout(function () { + badger.checkForDNTPolicy(request_host); + }, 0); + } + + if (type == 'sub_frame') { + setTimeout(function () { + hideBlockedFrame(tab_id, details.parentFrameId, url, request_host); + }, 0); + } + + return {cancel: true}; +} + +/** + * Filters outgoing cookies and referer + * Injects DNT + * + * @param {Object} details Event details + * @returns {Object} modified headers + */ +function onBeforeSendHeaders(details) { + let frame_id = details.frameId, + tab_id = details.tabId, + type = details.type, + url = details.url; + + if (_isTabChromeInternal(tab_id)) { + // DNT policy requests: strip cookies + if (type == "xmlhttprequest" && url.endsWith("/.well-known/dnt-policy.txt")) { + // remove Cookie headers + let newHeaders = []; + for (let i = 0, count = details.requestHeaders.length; i < count; i++) { + let header = details.requestHeaders[i]; + if (header.name.toLowerCase() != "cookie") { + newHeaders.push(header); + } + } + return { + requestHeaders: newHeaders + }; + } + + return {}; + } + + let tab_host = getHostForTab(tab_id); + let request_host = window.extractHostFromURL(url); + + if (!utils.isThirdPartyDomain(request_host, tab_host)) { + if (badger.isPrivacyBadgerEnabled(tab_host)) { + // Still sending Do Not Track even if HTTP and cookie blocking are disabled + if (badger.isDNTSignalEnabled()) { + details.requestHeaders.push({name: "DNT", value: "1"}, {name: "Sec-GPC", value: "1"}); + } + return {requestHeaders: details.requestHeaders}; + } else { + return {}; + } + } + + let action = checkAction(tab_id, request_host, frame_id); + + if (action) { + badger.logThirdPartyOriginOnTab(tab_id, request_host, action); + } + + if (!badger.isPrivacyBadgerEnabled(tab_host)) { + return {}; + } + + // handle cookieblocked requests + if (action == constants.COOKIEBLOCK || action == constants.USER_COOKIEBLOCK) { + let newHeaders; + + // GET requests: remove cookie headers, reduce referrer header to origin + if (details.method == "GET") { + newHeaders = details.requestHeaders.filter(header => { + return (header.name.toLowerCase() != "cookie"); + }).map(header => { + if (header.name.toLowerCase() == "referer") { + header.value = header.value.slice( + 0, + header.value.indexOf('/', header.value.indexOf('://') + 3) + ) + '/'; + } + return header; + }); + + // remove cookie and referrer headers otherwise + } else { + newHeaders = details.requestHeaders.filter(header => { + return (header.name.toLowerCase() != "cookie" && header.name.toLowerCase() != "referer"); + }); + } + + // add DNT header + if (badger.isDNTSignalEnabled()) { + newHeaders.push({name: "DNT", value: "1"}, {name: "Sec-GPC", value: "1"}); + } + + return {requestHeaders: newHeaders}; + } + + // if we are here, we're looking at a third-party request + // that's not yet blocked or cookieblocked + if (badger.isDNTSignalEnabled()) { + details.requestHeaders.push({name: "DNT", value: "1"}, {name: "Sec-GPC", value: "1"}); + } + return {requestHeaders: details.requestHeaders}; +} + +/** + * Filters incoming cookies out of the response header + * + * @param {Object} details The event details + * @returns {Object} The new response headers + */ +function onHeadersReceived(details) { + let tab_id = details.tabId, + url = details.url; + + if (_isTabChromeInternal(tab_id)) { + // DNT policy responses: strip cookies, reject redirects + if (details.type == "xmlhttprequest" && url.endsWith("/.well-known/dnt-policy.txt")) { + // if it's a redirect, cancel it + if (details.statusCode >= 300 && details.statusCode < 400) { + return { + cancel: true + }; + } + + // remove Set-Cookie headers + let headers = details.responseHeaders, + newHeaders = []; + for (let i = 0, count = headers.length; i < count; i++) { + if (headers[i].name.toLowerCase() != "set-cookie") { + newHeaders.push(headers[i]); + } + } + return { + responseHeaders: newHeaders + }; + } + + return {}; + } + + let tab_host = getHostForTab(tab_id); + let response_host = window.extractHostFromURL(url); + + if (!utils.isThirdPartyDomain(response_host, tab_host)) { + return {}; + } + + let action = checkAction(tab_id, response_host, details.frameId); + if (!action) { + return {}; + } + + badger.logThirdPartyOriginOnTab(tab_id, response_host, action); + + if (!badger.isPrivacyBadgerEnabled(tab_host)) { + return {}; + } + + if (action == constants.COOKIEBLOCK || action == constants.USER_COOKIEBLOCK) { + let newHeaders = details.responseHeaders.filter(function(header) { + return (header.name.toLowerCase() != "set-cookie"); + }); + return {responseHeaders: newHeaders}; + } +} + +/*************** Non-blocking listener functions ***************/ + +/** + * Event handler when a tab gets removed + * + * @param {Integer} tabId Id of the tab + */ +function onTabRemoved(tabId) { + forgetTab(tabId); +} + +/** + * Update internal db on tabs when a tab gets replaced + * due to prerendering or instant search. + * + * @param {Integer} addedTabId The new tab id that replaces + * @param {Integer} removedTabId The tab id that gets removed + */ +function onTabReplaced(addedTabId, removedTabId) { + forgetTab(removedTabId); + // Update the badge of the added tab, which was probably used for prerendering. + badger.updateBadge(addedTabId); +} + +/** + * We don't always get a "main_frame" details object in onBeforeRequest, + * so we need a fallback for (re)initializing tabData. + */ +function onNavigate(details) { + const tab_id = details.tabId, + url = details.url; + + // main (top-level) frames only + if (details.frameId !== 0) { + return; + } + + let oldTabData = badger.getFrameData(tab_id), + is_reload = oldTabData && oldTabData.url == url; + + forgetTab(tab_id, is_reload); + + // forget but don't initialize on special browser/extension pages + if (utils.isRestrictedUrl(url)) { + return; + } + + badger.recordFrame(tab_id, 0, url); + + let tab_host = badger.getFrameData(tab_id).host; + + initializeAllowedWidgets(tab_id, tab_host); + + // initialize tab data bookkeeping used by heuristicBlockingAccounting() + // to avoid missing or misattributing learning + // when there is no "main_frame" webRequest callback + // (such as on Service Worker pages) + // + // see the tabOrigins TODO in heuristicblocking.js + // as to why we don't just use tabData + let base = window.getBaseDomain(tab_host); + badger.heuristicBlocking.tabOrigins[tab_id] = base; + badger.heuristicBlocking.tabUrls[tab_id] = url; +} + +/******** Utility Functions **********/ + +/** + * Messages collapser.js content script to hide blocked frames. + */ +function hideBlockedFrame(tab_id, parent_frame_id, frame_url, frame_host) { + // don't hide if hiding is disabled + if (!badger.getSettings().getItem('hideBlockedElements')) { + return; + } + + // don't hide widget frames + if (badger.isWidgetReplacementEnabled()) { + let exceptions = badger.getSettings().getItem('widgetReplacementExceptions'); + for (let widget of badger.widgetList) { + if (exceptions.includes(widget.name)) { + continue; + } + for (let domain of widget.domains) { + if (domain == frame_host) { + return; + } else if (domain[0] == "*") { // leading wildcard domain + if (frame_host.endsWith(domain.slice(1))) { + return; + } + } + } + } + } + + // message content script + chrome.tabs.sendMessage(tab_id, { + hideFrame: true, + url: frame_url + }, { + frameId: parent_frame_id + }, function (response) { + if (response) { + // content script was ready and received our message + return; + } + // content script was not ready + if (chrome.runtime.lastError) { + // ignore + } + // record frame_url and parent_frame_id + // for when content script becomes ready + let tabData = badger.tabData[tab_id]; + if (!tabData.blockedFrameUrls.hasOwnProperty(parent_frame_id)) { + tabData.blockedFrameUrls[parent_frame_id] = []; + } + tabData.blockedFrameUrls[parent_frame_id].push(frame_url); + }); +} + +/** + * Gets the host name for a given tab id + * @param {Integer} tabId chrome tab id + * @return {String} the host name for the tab + */ +function getHostForTab(tabId) { + let mainFrameIdx = 0; + if (!badger.tabData[tabId]) { + return ''; + } + // TODO what does this actually do? + // meant to address https://github.com/EFForg/privacybadger/issues/136 + if (_isTabAnExtension(tabId)) { + // If the tab is an extension get the url of the first frame for its implied URL + // since the url of frame 0 will be the hash of the extension key + mainFrameIdx = Object.keys(badger.tabData[tabId].frames)[1] || 0; + } + let frameData = badger.getFrameData(tabId, mainFrameIdx); + if (!frameData) { + return ''; + } + return frameData.host; +} + +/** + * Record "supercookie" tracking + * + * @param {Integer} tab_id browser tab ID + * @param {String} frame_url URL of the frame with supercookie + */ +function recordSupercookie(tab_id, frame_url) { + const frame_host = window.extractHostFromURL(frame_url), + page_host = badger.getFrameData(tab_id).host; + + if (!utils.isThirdPartyDomain(frame_host, page_host)) { + // Only happens on the start page for google.com + return; + } + + badger.heuristicBlocking.updateTrackerPrevalence( + frame_host, + window.getBaseDomain(frame_host), + window.getBaseDomain(page_host) + ); +} + +/** + * Record canvas fingerprinting + * + * @param {Integer} tabId the tab ID + * @param {Object} msg specific fingerprinting data + */ +function recordFingerprinting(tabId, msg) { + // Abort if we failed to determine the originating script's URL + // TODO find and fix where this happens + if (!msg.scriptUrl) { + return; + } + + // Ignore first-party scripts + let script_host = window.extractHostFromURL(msg.scriptUrl), + document_host = badger.getFrameData(tabId).host; + if (!utils.isThirdPartyDomain(script_host, document_host)) { + return; + } + + let CANVAS_WRITE = { + fillText: true, + strokeText: true + }; + let CANVAS_READ = { + getImageData: true, + toDataURL: true + }; + + if (!badger.tabData[tabId].hasOwnProperty('fpData')) { + badger.tabData[tabId].fpData = {}; + } + + let script_origin = window.getBaseDomain(script_host); + + // Initialize script TLD-level data + if (!badger.tabData[tabId].fpData.hasOwnProperty(script_origin)) { + badger.tabData[tabId].fpData[script_origin] = { + canvas: { + fingerprinting: false, + write: false + } + }; + } + let scriptData = badger.tabData[tabId].fpData[script_origin]; + + if (msg.extra.hasOwnProperty('canvas')) { + if (scriptData.canvas.fingerprinting) { + return; + } + + // If this script already had a canvas write... + if (scriptData.canvas.write) { + // ...and if this is a canvas read... + if (CANVAS_READ.hasOwnProperty(msg.prop)) { + // ...and it got enough data... + if (msg.extra.width > 16 && msg.extra.height > 16) { + // ...we will classify it as fingerprinting + scriptData.canvas.fingerprinting = true; + log(script_host, 'caught fingerprinting on', document_host); + + // Mark this as a strike + badger.heuristicBlocking.updateTrackerPrevalence( + script_host, script_origin, window.getBaseDomain(document_host)); + } + } + // This is a canvas write + } else if (CANVAS_WRITE.hasOwnProperty(msg.prop)) { + scriptData.canvas.write = true; + } + } +} + +/** + * Cleans up tab-specific data. + * + * @param {Integer} tab_id the ID of the tab + * @param {Boolean} is_reload whether the page is simply being reloaded + */ +function forgetTab(tab_id, is_reload) { + delete badger.tabData[tab_id]; + if (!is_reload) { + delete tempAllowlist[tab_id]; + } +} + +/** + * Determines the action to take on a specific FQDN. + * + * @param {Integer} tabId The relevant tab + * @param {String} requestHost The FQDN + * @param {Integer} frameId The id of the frame + * @returns {(String|Boolean)} false or the action to take + */ +function checkAction(tabId, requestHost, frameId) { + // Ignore requests from temporarily unblocked widgets. + // Someone clicked the widget, so let it load. + if (allowedOnTab(tabId, requestHost, frameId)) { + return false; + } + + // Ignore requests from private domains. + if (window.isPrivateDomain(requestHost)) { + return false; + } + + return badger.storage.getBestAction(requestHost); +} + +/** + * Checks if the tab is chrome internal + * + * @param {Integer} tabId Id of the tab to test + * @returns {boolean} Returns true if the tab is chrome internal + * @private + */ +function _isTabChromeInternal(tabId) { + if (tabId < 0) { + return true; + } + + let frameData = badger.getFrameData(tabId); + if (!frameData || !frameData.url.startsWith("http")) { + return true; + } + + return false; +} + +/** + * Checks if the tab is a chrome-extension tab + * + * @param {Integer} tabId Id of the tab to test + * @returns {boolean} Returns true if the tab is from a chrome-extension + * @private + */ +function _isTabAnExtension(tabId) { + let frameData = badger.getFrameData(tabId); + return (frameData && ( + frameData.url.startsWith("chrome-extension://") || + frameData.url.startsWith("moz-extension://") + )); +} + +/** + * Provides the widget replacing content script with list of widgets to replace. + * + * @param {Integer} tab_id the ID of the tab we're replacing widgets in + * + * @returns {Object} dict containing the complete list of widgets + * as well as a mapping to indicate which ones should be replaced + */ +let getWidgetList = (function () { + // cached translations + let translations; + + // inputs to chrome.i18n.getMessage() + const widgetTranslations = [ + { + key: "social_tooltip_pb_has_replaced", + placeholders: ["XXX"] + }, + { + key: "widget_placeholder_pb_has_replaced", + placeholders: ["XXX"] + }, + { key: "allow_once" }, + { key: "allow_on_site" }, + ]; + + return function (tab_id) { + // an object with keys set to widget names that should be replaced + let widgetsToReplace = {}, + widgetList = [], + tabData = badger.tabData[tab_id], + tabOrigins = tabData && tabData.origins && Object.keys(tabData.origins), + exceptions = badger.getSettings().getItem('widgetReplacementExceptions'); + + // optimize translation lookups by doing them just once, + // the first time they are needed + if (!translations) { + translations = widgetTranslations.reduce((memo, data) => { + memo[data.key] = chrome.i18n.getMessage(data.key, data.placeholders); + return memo; + }, {}); + + // TODO duplicated in src/lib/i18n.js + const RTL_LOCALES = ['ar', 'he', 'fa'], + UI_LOCALE = chrome.i18n.getMessage('@@ui_locale'); + translations.rtl = RTL_LOCALES.indexOf(UI_LOCALE) > -1; + } + + for (let widget of badger.widgetList) { + // replace only if the widget is not on the 'do not replace' list + // also don't send widget data used later for dynamic replacement + if (exceptions.includes(widget.name)) { + continue; + } + + widgetList.push(widget); + + // replace only if at least one of the associated domains was blocked + if (!tabOrigins || !tabOrigins.length) { + continue; + } + let replace = widget.domains.some(domain => { + // leading wildcard domain + if (domain[0] == "*") { + domain = domain.slice(1); + // get all domains in tabData.origins that end with this domain + let matches = tabOrigins.filter(origin => { + return origin.endsWith(domain); + }); + // do we have any matches and are they all blocked? + return matches.length && matches.every(origin => { + const action = tabData.origins[origin]; + return ( + action == constants.BLOCK || + action == constants.USER_BLOCK + ); + }); + } + + // regular, non-leading wildcard domain + if (!tabData.origins.hasOwnProperty(domain)) { + return false; + } + const action = tabData.origins[domain]; + return ( + action == constants.BLOCK || + action == constants.USER_BLOCK + ); + + }); + if (replace) { + widgetsToReplace[widget.name] = true; + } + } + + return { + translations, + widgetList, + widgetsToReplace + }; + }; +}()); + +/** + * Checks if given request FQDN is temporarily unblocked on a tab. + * + * The request is allowed if any of the following is true: + * + * - 1a) Request FQDN matches an entry on the exception list for the tab + * - 1b) Request FQDN ends with a wildcard entry from the exception list + * - 2a) Request is from a subframe whose FQDN matches an entry on the list + * - 2b) Same but subframe's FQDN ends with a wildcard entry + * + * @param {Integer} tab_id the ID of the tab to check + * @param {String} request_host the request FQDN to check + * @param {Integer} frame_id the frame ID to check + * + * @returns {Boolean} true if FQDN is on the temporary allow list + */ +function allowedOnTab(tab_id, request_host, frame_id) { + if (!tempAllowlist.hasOwnProperty(tab_id)) { + return false; + } + + let exceptions = tempAllowlist[tab_id]; + + for (let exception of exceptions) { + if (exception == request_host) { + return true; // 1a + // leading wildcard + } else if (exception[0] == "*") { + if (request_host.endsWith(exception.slice(1))) { + return true; // 1b + } + } + } + + if (!frame_id) { + return false; + } + let frameData = badger.getFrameData(tab_id, frame_id); + if (!frameData || !frameData.host) { + return false; + } + + let frame_host = frameData.host; + for (let exception of exceptions) { + if (exception == frame_host) { + return true; // 2a + // leading wildcard + } else if (exception[0] == "*") { + if (frame_host.endsWith(exception.slice(1))) { + return true; // 2b + } + } + } + + return false; +} + +/** + * @returns {Array|Boolean} the list of associated domains or false + */ +function getWidgetDomains(widget_name) { + let widgetData = badger.widgetList.find( + widget => widget.name == widget_name); + + if (!widgetData || + !widgetData.hasOwnProperty("replacementButton") || + !widgetData.replacementButton.unblockDomains) { + return false; + } + + return widgetData.replacementButton.unblockDomains; +} + +/** + * Marks a set of (widget) domains to be (temporarily) allowed on a tab. + * + * @param {Integer} tab_id the ID of the tab + * @param {Array} domains the domains + */ +function allowOnTab(tab_id, domains) { + if (!tempAllowlist.hasOwnProperty(tab_id)) { + tempAllowlist[tab_id] = []; + } + for (let domain of domains) { + if (!tempAllowlist[tab_id].includes(domain)) { + tempAllowlist[tab_id].push(domain); + } + } +} + +/** + * Called upon navigation to prepopulate the temporary allowlist + * with domains for widgets marked as always allowed on a given site. + */ +function initializeAllowedWidgets(tab_id, tab_host) { + let allowedWidgets = badger.getSettings().getItem('widgetSiteAllowlist'); + if (allowedWidgets.hasOwnProperty(tab_host)) { + for (let widget_name of allowedWidgets[tab_host]) { + let widgetDomains = getWidgetDomains(widget_name); + if (widgetDomains) { + allowOnTab(tab_id, widgetDomains); + } + } + } +} + +// NOTE: sender.tab is available for content script (not popup) messages only +function dispatcher(request, sender, sendResponse) { + + // messages from content scripts are to be treated with greater caution: + // https://groups.google.com/a/chromium.org/d/msg/chromium-extensions/0ei-UCHNm34/lDaXwQhzBAAJ + if (!sender.url.startsWith(chrome.runtime.getURL(""))) { + // reject unless it's a known content script message + const KNOWN_CONTENT_SCRIPT_MESSAGES = [ + "allowWidgetOnSite", + "checkDNT", + "checkEnabled", + "checkLocation", + "checkWidgetReplacementEnabled", + "detectFingerprinting", + "fpReport", + "getBlockedFrameUrls", + "getReplacementButton", + "inspectLocalStorage", + "supercookieReport", + "unblockWidget", + ]; + if (!KNOWN_CONTENT_SCRIPT_MESSAGES.includes(request.type)) { + console.error("Rejected unknown message %o from %s", request, sender.url); + return sendResponse(); + } + } + + switch (request.type) { + + case "checkEnabled": { + sendResponse(badger.isPrivacyBadgerEnabled( + window.extractHostFromURL(sender.tab.url) + )); + + break; + } + + case "checkLocation": { + if (!badger.isPrivacyBadgerEnabled(window.extractHostFromURL(sender.tab.url))) { + return sendResponse(); + } + + // Ignore requests from internal Chrome tabs. + if (_isTabChromeInternal(sender.tab.id)) { + return sendResponse(); + } + + let frame_host = window.extractHostFromURL(request.frameUrl), + tab_host = window.extractHostFromURL(sender.tab.url); + + // Ignore requests that aren't from a third party. + if (!frame_host || !utils.isThirdPartyDomain(frame_host, tab_host)) { + return sendResponse(); + } + + let action = checkAction(sender.tab.id, frame_host); + sendResponse(action == constants.COOKIEBLOCK || action == constants.USER_COOKIEBLOCK); + + break; + } + + case "getBlockedFrameUrls": { + if (!badger.isPrivacyBadgerEnabled(window.extractHostFromURL(sender.tab.url))) { + return sendResponse(); + } + let tab_id = sender.tab.id, + frame_id = sender.frameId, + tabData = badger.tabData.hasOwnProperty(tab_id) && badger.tabData[tab_id], + blockedFrameUrls = tabData && + tabData.blockedFrameUrls.hasOwnProperty(frame_id) && + tabData.blockedFrameUrls[frame_id]; + sendResponse(blockedFrameUrls); + break; + } + + case "unblockWidget": { + let widgetDomains = getWidgetDomains(request.widgetName); + if (!widgetDomains) { + return sendResponse(); + } + allowOnTab(sender.tab.id, widgetDomains); + sendResponse(); + break; + } + + case "allowWidgetOnSite": { + // record that we always want to activate this widget on this site + let tab_host = window.extractHostFromURL(sender.tab.url), + allowedWidgets = badger.getSettings().getItem('widgetSiteAllowlist'); + if (!allowedWidgets.hasOwnProperty(tab_host)) { + allowedWidgets[tab_host] = []; + } + if (!allowedWidgets[tab_host].includes(request.widgetName)) { + allowedWidgets[tab_host].push(request.widgetName); + badger.getSettings().setItem('widgetSiteAllowlist', allowedWidgets); + } + sendResponse(); + break; + } + + case "getReplacementButton": { + let widgetData = badger.widgetList.find( + widget => widget.name == request.widgetName); + if (!widgetData || + !widgetData.hasOwnProperty("replacementButton") || + !widgetData.replacementButton.imagePath) { + return sendResponse(); + } + + let button_path = chrome.runtime.getURL( + "skin/socialwidgets/" + widgetData.replacementButton.imagePath); + + let image_type = button_path.slice(button_path.lastIndexOf('.') + 1); + + let xhrOptions = {}; + if (image_type != "svg") { + xhrOptions.responseType = "arraybuffer"; + } + + // fetch replacement button image data + utils.xhrRequest(button_path, function (err, response) { + // one data URI for SVGs + if (image_type == "svg") { + return sendResponse('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(response)); + } + + // another data URI for all other image formats + sendResponse( + 'data:image/' + image_type + ';base64,' + + utils.arrayBufferToBase64(response) + ); + }, "GET", xhrOptions); + + // indicate this is an async response to chrome.runtime.onMessage + return true; + } + + case "fpReport": { + if (Array.isArray(request.data)) { + request.data.forEach(function (msg) { + recordFingerprinting(sender.tab.id, msg); + }); + } else { + recordFingerprinting(sender.tab.id, request.data); + } + + break; + } + + case "supercookieReport": { + if (request.frameUrl && badger.hasSupercookie(request.data)) { + recordSupercookie(sender.tab.id, request.frameUrl); + } + break; + } + + case "inspectLocalStorage": { + let tab_host = window.extractHostFromURL(sender.tab.url), + frame_host = window.extractHostFromURL(request.frameUrl); + + sendResponse(frame_host && + badger.isLearningEnabled(sender.tab.id) && + badger.isPrivacyBadgerEnabled(tab_host) && + utils.isThirdPartyDomain(frame_host, tab_host)); + + break; + } + + case "detectFingerprinting": { + let tab_host = window.extractHostFromURL(sender.tab.url); + + sendResponse( + badger.isLearningEnabled(sender.tab.id) && + badger.isPrivacyBadgerEnabled(tab_host)); + + break; + } + + case "checkWidgetReplacementEnabled": { + let response = false, + tab_host = window.extractHostFromURL(sender.tab.url); + + if (badger.isPrivacyBadgerEnabled(tab_host) && + badger.isWidgetReplacementEnabled()) { + response = getWidgetList(sender.tab.id); + } + + sendResponse(response); + + break; + } + + case "getPopupData": { + let tab_id = request.tabId; + + if (!badger.tabData.hasOwnProperty(tab_id)) { + sendResponse({ + criticalError: badger.criticalError, + noTabData: true, + seenComic: true, + }); + break; + } + + let tab_host = window.extractHostFromURL(request.tabUrl), + origins = badger.tabData[tab_id].origins, + cookieblocked = {}; + + for (let origin in origins) { + // see if origin would be cookieblocked if not for user override + if (badger.storage.wouldGetCookieblocked(origin)) { + cookieblocked[origin] = true; + } + } + + sendResponse({ + cookieblocked, + criticalError: badger.criticalError, + enabled: badger.isPrivacyBadgerEnabled(tab_host), + errorText: badger.tabData[tab_id].errorText, + learnLocally: badger.getSettings().getItem("learnLocally"), + noTabData: false, + origins, + seenComic: badger.getSettings().getItem("seenComic"), + showLearningPrompt: badger.getPrivateSettings().getItem("showLearningPrompt"), + showNonTrackingDomains: badger.getSettings().getItem("showNonTrackingDomains"), + tabHost: tab_host, + tabId: tab_id, + tabUrl: request.tabUrl, + trackerCount: badger.getTrackerCount(tab_id) + }); + + break; + } + + case "getOptionsData": { + let origins = badger.storage.getTrackingDomains(); + + let cookieblocked = {}; + for (let origin in origins) { + // see if origin would be cookieblocked if not for user override + if (badger.storage.wouldGetCookieblocked(origin)) { + cookieblocked[origin] = true; + } + } + + sendResponse({ + cookieblocked, + isWidgetReplacementEnabled: badger.isWidgetReplacementEnabled(), + origins, + settings: badger.getSettings().getItemClones(), + webRTCAvailable: badger.webRTCAvailable, + widgets: badger.widgetList.map(widget => widget.name), + }); + + break; + } + + case "resetData": { + badger.storage.clearTrackerData(); + badger.loadSeedData(err => { + if (err) { + console.error(err); + } + badger.blockWidgetDomains(); + sendResponse(); + }); + // indicate this is an async response to chrome.runtime.onMessage + return true; + } + + case "removeAllData": { + badger.storage.clearTrackerData(); + sendResponse(); + break; + } + + case "seenComic": { + badger.getSettings().setItem("seenComic", true); + sendResponse(); + break; + } + + case "seenLearningPrompt": { + badger.getPrivateSettings().setItem("showLearningPrompt", false); + sendResponse(); + break; + } + + case "activateOnSite": { + badger.enablePrivacyBadgerForOrigin(request.tabHost); + badger.updateIcon(request.tabId, request.tabUrl); + sendResponse(); + break; + } + + case "deactivateOnSite": { + badger.disablePrivacyBadgerForOrigin(request.tabHost); + badger.updateIcon(request.tabId, request.tabUrl); + sendResponse(); + break; + } + + case "revertDomainControl": { + badger.storage.revertUserAction(request.origin); + sendResponse({ + origins: badger.storage.getTrackingDomains() + }); + break; + } + + case "downloadCloud": { + chrome.storage.sync.get("disabledSites", function (store) { + if (chrome.runtime.lastError) { + sendResponse({success: false, message: chrome.runtime.lastError.message}); + } else if (store.hasOwnProperty("disabledSites")) { + let disabledSites = _.union( + badger.getDisabledSites(), + store.disabledSites + ); + badger.getSettings().setItem("disabledSites", disabledSites); + sendResponse({ + success: true, + disabledSites + }); + } else { + sendResponse({ + success: false, + message: chrome.i18n.getMessage("download_cloud_no_data") + }); + } + }); + + // indicate this is an async response to chrome.runtime.onMessage + return true; + } + + case "uploadCloud": { + let obj = {}; + obj.disabledSites = badger.getDisabledSites(); + chrome.storage.sync.set(obj, function () { + if (chrome.runtime.lastError) { + sendResponse({success: false, message: chrome.runtime.lastError.message}); + } else { + sendResponse({success: true}); + } + }); + // indicate this is an async response to chrome.runtime.onMessage + return true; + } + + case "savePopupToggle": { + let domain = request.origin, + action = request.action; + + badger.saveAction(action, domain); + + // update cached tab data so that a reopened popup displays correct state + badger.tabData[request.tabId].origins[domain] = "user_" + action; + + break; + } + + case "saveOptionsToggle": { + // called when the user manually sets a slider on the options page + badger.saveAction(request.action, request.origin); + sendResponse({ + origins: badger.storage.getTrackingDomains() + }); + break; + } + + case "mergeUserData": { + // called when a user uploads data exported from another Badger instance + badger.mergeUserData(request.data); + badger.blockWidgetDomains(); + sendResponse({ + disabledSites: badger.getDisabledSites(), + origins: badger.storage.getTrackingDomains(), + }); + break; + } + + case "updateSettings": { + const settings = badger.getSettings(); + for (let key in request.data) { + if (badger.defaultSettings.hasOwnProperty(key)) { + settings.setItem(key, request.data[key]); + } else { + console.error("Unknown Badger setting:", key); + } + } + sendResponse(); + break; + } + + case "updateBadge": { + let tab_id = request.tab_id; + badger.updateBadge(tab_id); + sendResponse(); + break; + } + + case "disablePrivacyBadgerForOrigin": { + badger.disablePrivacyBadgerForOrigin(request.domain); + sendResponse({ + disabledSites: badger.getDisabledSites() + }); + break; + } + + case "enablePrivacyBadgerForOriginList": { + request.domains.forEach(function (domain) { + badger.enablePrivacyBadgerForOrigin(domain); + }); + sendResponse({ + disabledSites: badger.getDisabledSites() + }); + break; + } + + case "removeOrigin": { + badger.storage.getStore("snitch_map").deleteItem(request.origin); + badger.storage.getStore("action_map").deleteItem(request.origin); + sendResponse({ + origins: badger.storage.getTrackingDomains() + }); + break; + } + + case "saveErrorText": { + let activeTab = badger.tabData[request.tabId]; + activeTab.errorText = request.errorText; + break; + } + + case "removeErrorText": { + let activeTab = badger.tabData[request.tabId]; + delete activeTab.errorText; + break; + } + + case "checkDNT": { + // called from contentscripts/dnt.js to check if we should enable it + sendResponse( + badger.isDNTSignalEnabled() + && badger.isPrivacyBadgerEnabled( + window.extractHostFromURL(sender.tab.url) + ) + ); + break; + } + + } +} + +/*************** Event Listeners *********************/ +function startListeners() { + chrome.webNavigation.onBeforeNavigate.addListener(onNavigate); + + chrome.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["http://*/*", "https://*/*"]}, ["blocking"]); + + let extraInfoSpec = ['requestHeaders', 'blocking']; + if (chrome.webRequest.OnBeforeSendHeadersOptions.hasOwnProperty('EXTRA_HEADERS')) { + extraInfoSpec.push('extraHeaders'); + } + chrome.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ["http://*/*", "https://*/*"]}, extraInfoSpec); + + extraInfoSpec = ['responseHeaders', 'blocking']; + if (chrome.webRequest.OnHeadersReceivedOptions.hasOwnProperty('EXTRA_HEADERS')) { + extraInfoSpec.push('extraHeaders'); + } + chrome.webRequest.onHeadersReceived.addListener(onHeadersReceived, {urls: ["<all_urls>"]}, extraInfoSpec); + + chrome.tabs.onRemoved.addListener(onTabRemoved); + chrome.tabs.onReplaced.addListener(onTabReplaced); + chrome.runtime.onMessage.addListener(dispatcher); +} + +/************************************** exports */ +let exports = { + startListeners +}; +return exports; +/************************************** exports */ +})(); |