summaryrefslogtreecommitdiffstats
path: root/src/js/webrequest.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 19:47:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 19:47:39 +0000
commit8d13bdc6cac0e20c43c6f909fc0208774b9c5c84 (patch)
tree5fd46925c6b4a881c9208772ed8e5cc0588bc164 /src/js/webrequest.js
parentInitial commit. (diff)
downloadprivacybadger-8d13bdc6cac0e20c43c6f909fc0208774b9c5c84.tar.xz
privacybadger-8d13bdc6cac0e20c43c6f909fc0208774b9c5c84.zip
Adding upstream version 2020.10.7.upstream/2020.10.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/js/webrequest.js')
-rw-r--r--src/js/webrequest.js1293
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 */
+})();