diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 03a5eef..99b3771 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1192,6 +1192,10 @@ "message": "CAUTION: the actual browser in use cannot be faked entirely as there is a multitude of ways to detect it. E.g. feature tests and browser specific rendering of HTML elements will always leak.", "description": "" }, + "navigatorSettings_contextualIdentities": { + "message": "Settings for the container {select} are shown.", + "description": "" + }, "navigatorSettings_presetSection.os": { "message": "Operating system presets", "description": "" diff --git a/lib/frame.js b/lib/frame.js index eb6157b..57ec4af 100644 --- a/lib/frame.js +++ b/lib/frame.js @@ -87,6 +87,8 @@ logging.notice("my tab cookie store id is", data.cookieStoreId); const {persistent: persistentRnd} = require("./randomSupplies"); persistentRnd.setCookieStoreId(data.cookieStoreId); + const modifiedNavigatorAPI = require("./modifiedNavigatorAPI"); + modifiedNavigatorAPI.setCookieStoreId(data.cookieStoreId); } const persistentRndName = "persistent" + (extension.inIncognitoContext? "Incognito": "") + "Rnd"; if (data.hasOwnProperty(persistentRndName)){ diff --git a/lib/modifiedNavigatorAPI.js b/lib/modifiedNavigatorAPI.js index c395b51..fb9efa7 100644 --- a/lib/modifiedNavigatorAPI.js +++ b/lib/modifiedNavigatorAPI.js @@ -13,8 +13,34 @@ } const {checkerWrapper, setGetterProperties, getStatusByFlag} = require("./modifiedAPIFunctions"); + const logging = require("./logging"); const navigator = require("./navigator"); + let cookieStoreId = false; + scope.setCookieStoreId = function(newCookieStoreId){ + if (typeof newCookieStoreId === "string"){ + cookieStoreId = ( + newCookieStoreId !== "" && + newCookieStoreId !== "firefox-default" + )? newCookieStoreId: ""; + } + }; + function getCookieStoreId(){ + while (cookieStoreId === false){ + logging.message("Starting synchronous request to wait for cookie store id."); + try { + let xhr = new XMLHttpRequest(); + xhr.open("GET", "https://[::]", false); + xhr.send(); + xhr = null; + } + catch (error){ + logging.verbose("Error in XHR:", error); + } + } + return cookieStoreId; + } + scope.changedGetters = navigator.allProperties.map(function(property){ return { objectGetters: [function(window){return window.Navigator && window.Navigator.prototype;}], @@ -25,7 +51,7 @@ return checkerWrapper(checker, this, arguments, function(args, check){ const {notify, original} = check; const originalValue = original.call(this, ...args); - const returnValue = navigator.getNavigatorValue(property); + const returnValue = navigator.getNavigatorValue(property, getCookieStoreId); if (originalValue !== returnValue){ notify("fakedNavigatorReadout"); } diff --git a/lib/navigator.js b/lib/navigator.js index d95fb43..616b87d 100644 --- a/lib/navigator.js +++ b/lib/navigator.js @@ -36,38 +36,63 @@ changedValues = newValue; }); - - function getValue(name, stack = []){ - if (stack.indexOf(name) !== -1){ - return "[ERROR: loop in property definition]"; - } - stack.push(name); - - switch (name){ - case "original value": - return original[stack[stack.length - 2]]; - case "random": - return String.fromCharCode(Math.floor(65 + 85 * Math.random())); - default: - if (changedValues.hasOwnProperty(name)){ - return parseString(changedValues[name], stack.slice()); + const getValue = function(){ + function getChangedValues(getCookieStoreId){ + if (changedValues.contextualIdentities){ + const cookieStoreId = getCookieStoreId(); + if ( + cookieStoreId !== "" && + cookieStoreId !== "firefox-default" && + changedValues.contextualIdentities[cookieStoreId] + ){ + return changedValues.contextualIdentities[cookieStoreId]; } else { - return original[name]; + return changedValues; } + } + else { + return changedValues; + } } - } - function parseString(string, stack){ - if (string === "{undefined}"){ - return undefined; - } - return string.replace(/{([a-z[\]_. -]*)}/ig, function(m, name){ - return getValue(name, stack.slice()); - }); - } + + return function getValue(name, getCookieStoreId){ + const changedValues = getChangedValues(getCookieStoreId); + + function getValueInternal(name, stack = []){ + if (stack.indexOf(name) !== -1){ + return "[ERROR: loop in property definition]"; + } + stack.push(name); + + switch (name){ + case "original value": + return original[stack[stack.length - 2]]; + case "random": + return String.fromCharCode(Math.floor(65 + 85 * Math.random())); + default: + if (changedValues.hasOwnProperty(name)){ + return parseString(changedValues[name], stack.slice()); + } + else { + return original[name]; + } + } + } + function parseString(string, stack){ + if (string === "{undefined}"){ + return undefined; + } + return string.replace(/{([a-z[\]_. -]*)}/ig, function(m, name){ + return getValueInternal(name, stack.slice()); + }); + } + return getValueInternal(name); + }; + }(); - scope.getNavigatorValue = function getNavigatorValue(name){ - return getValue(name); + scope.getNavigatorValue = function getNavigatorValue(name, getCookieStoreId){ + return getValue(name, getCookieStoreId); }; function changeHTTPHeader(details){ @@ -82,7 +107,9 @@ ){ for (let header of details.requestHeaders){ if (header.name.toLowerCase() === "user-agent"){ - header.value = getValue("userAgent"); + header.value = getValue("userAgent", function(){ + return details.cookieStoreId; + }); } } } @@ -118,5 +145,24 @@ scope.unregisterHeaderChange(); } }); + + if (browser.contextualIdentities && browser.contextualIdentities.onRemoved){ + logging.message("register contextual navigator identities removal"); + browser.contextualIdentities.onRemoved.addListener(function(details){ + logging.message("Contextual navigator identity", details.contextualIdentity.cookieStoreId, "removed."); + if (changedValues.contextualIdentities){ + delete changedValues.contextualIdentities[details.contextualIdentity.cookieStoreId]; + if (Object.keys(changedValues.contextualIdentities).length === 0){ + delete changedValues.contextualIdentities; + } + settings.navigatorDetails = changedValues; + } + }); + } + else { + logging.error( + "Old Firefox does not support browser.contextualIdentities.onRemoved" + ); + } }; }()); \ No newline at end of file diff --git a/options/navigator.css b/options/navigator.css index 6c27968..58697c8 100644 --- a/options/navigator.css +++ b/options/navigator.css @@ -1,3 +1,8 @@ +.contextualIdentities { + display: block; + margin: 0.5em 0; +} + .presetSection ul { list-style-type: none; padding: 0; diff --git a/options/navigator.js b/options/navigator.js index 01dc59a..e4bc814 100644 --- a/options/navigator.js +++ b/options/navigator.js @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -(function(){ +(async function(){ "use strict"; const extension = require("../lib/extension"); @@ -26,6 +26,144 @@ disclaimer.textContent = extension.getTranslation("navigatorSettings_disclaimer"); document.body.appendChild(disclaimer); + const navigatorDetails = await async function(){ + let cookieStoreId = ""; + if (browser.contextualIdentities){ + const contextualIdentities = await browser.contextualIdentities.query({}); + if (contextualIdentities.length){ + const containerLabel = document.createElement("label"); + containerLabel.className = "contextualIdentities"; + containerLabel.appendChild(extension.parseTranslation( + extension.getTranslation("navigatorSettings_contextualIdentities"), + { + select: function(){ + const contextualIdentitiesSelect = document.createElement("select"); + contextualIdentitiesSelect.appendChild(new Option("", "")); + contextualIdentities.forEach(function(contextualIdentity){ + contextualIdentitiesSelect.appendChild(new Option( + contextualIdentity.name, + contextualIdentity.cookieStoreId + )); + }); + window.addEventListener("load", function(){ + cookieStoreId = contextualIdentitiesSelect.value; + emitUpdate(); + }); + contextualIdentitiesSelect.addEventListener("change", function(){ + cookieStoreId = this.value; + emitUpdate(); + }); + return contextualIdentitiesSelect; + } + } + )); + document.body.appendChild(containerLabel); + } + } + + const callbacks = []; + let baseStorage; + let loaded = false; + + function getKeys(object){ + return Object.keys(object).filter(function(key){ + return key !== "contextualIdentities"; + }); + } + + function storageEqual(storage1, storage2){ + const keys1 = getKeys(storage1); + const keys2 = getKeys(storage2); + return keys1.length === keys2.length && keys1.every(function(key){ + return storage1[key] === storage2[key]; + }); + } + + function copyData(from, to = {}){ + getKeys(from).forEach(function(key){ + to[key] = from[key]; + }); + return to; + } + + function getStorage(){ + if (cookieStoreId === ""){ + return baseStorage; + } + else { + if (!baseStorage.contextualIdentities){ + baseStorage.contextualIdentities = {}; + } + if (!baseStorage.contextualIdentities[cookieStoreId]){ + baseStorage.contextualIdentities[cookieStoreId] = copyData(baseStorage); + } + return baseStorage.contextualIdentities[cookieStoreId]; + } + } + + settings.on("navigatorDetails", function({newValue}){ + baseStorage = newValue; + emitUpdate(); + }); + settings.onloaded(function(){ + loaded = true; + baseStorage = settings.navigatorDetails; + emitUpdate(); + }); + function emitUpdate(){ + if (!loaded){ + return; + } + const storage = getStorage(); + callbacks.forEach(async function(callback){ + callback(storage); + }); + } + const api = { + get: getStorage, + getComputedValue: function getComputedValue(property){ + return navigator.getNavigatorValue(property, function(){ + return cookieStoreId; + }); + }, + onUpdate: function onUpdate(callback){ + callbacks.push(callback); + if (loaded){ + callback(getStorage()); + } + }, + save: function save(){ + if (!loaded){ + return; + } + if (baseStorage.contextualIdentities){ + if (Object.keys(baseStorage.contextualIdentities).reduce(function(lastValue, contextualIdentity){ + if (storageEqual(baseStorage.contextualIdentities[contextualIdentity], baseStorage)){ + delete baseStorage.contextualIdentities[contextualIdentity]; + return lastValue; + } + return false; + }, true)){ + delete baseStorage.contextualIdentities; + } + } + settings.navigatorDetails = baseStorage; + }, + reset: function reset(){ + if (cookieStoreId === ""){ + baseStorage = baseStorage.contextualIdentities? { + contextualIdentities: baseStorage.contextualIdentities + }: {}; + } + else { + baseStorage.contextualIdentities[cookieStoreId] = {}; + } + api.save(); + } + }; + return api; + }(); + function presetSection(title, presets){ const container = document.createElement("div"); container.className = "presetSection"; @@ -64,15 +202,10 @@ li.classList.remove("active"); } } - settings.on("navigatorDetails", function({newValue}){ - checkActive(newValue); - }); - settings.onloaded(function(){ - checkActive(settings.navigatorDetails); - }); + navigatorDetails.onUpdate(checkActive); button.addEventListener("click", function(){ - const data = settings.navigatorDetails; + const data = navigatorDetails.get(); Object.keys(presetProperties).forEach(function(property){ if (presetProperties[property] === undefined){ delete data[property]; @@ -85,7 +218,7 @@ data[property] = value; } }); - settings.navigatorDetails = data; + navigatorDetails.save(); }); }); @@ -249,11 +382,11 @@ input.value = currentProperties.hasOwnProperty(property)? currentProperties[property]: "{original value}"; input.addEventListener("change", function(){ currentProperties[property] = this.value; - settings.navigatorDetails = currentProperties; + navigatorDetails.save(); }); const computedValue = document.createElement("td"); - computedValue.textContent = navigator.getNavigatorValue(property); + computedValue.textContent = navigatorDetails.getComputedValue(property); row.appendChild(computedValue); section.appendChild(row); @@ -265,7 +398,8 @@ valueSection.appendChild(section); Object.keys(currentProperties).filter(function(property){ - return navigator.allProperties.indexOf(property) === -1; + return property !== "contextualIdentities" && + navigator.allProperties.indexOf(property) === -1; }).sort().forEach(createPropertyRow.bind(undefined, section)); section = document.createElement("tbody"); @@ -274,19 +408,11 @@ navigator.allProperties.forEach(createPropertyRow.bind(undefined, section)); } - - settings.on("navigatorDetails", function({newValue}){ - updateValueSection(newValue); - }); - settings.onloaded(function(){ - updateValueSection(settings.navigatorDetails); - }); + navigatorDetails.onUpdate(updateValueSection); const resetButton = document.createElement("button"); resetButton.className = "button"; resetButton.textContent = extension.getTranslation("navigatorSettings_reset"); - resetButton.addEventListener("click", function(){ - settings.navigatorDetails = {}; - }); + resetButton.addEventListener("click", navigatorDetails.reset); document.body.appendChild(resetButton); }()); \ No newline at end of file diff --git a/releaseNotes.txt b/releaseNotes.txt index 05b4696..c73af51 100644 --- a/releaseNotes.txt +++ b/releaseNotes.txt @@ -4,6 +4,7 @@ Version 1.1: new features: - added notice for privacy.resistFingerprinting + - added container specific navigator settings fixes: - error when exporting function with name "top" diff --git a/versions/updates.json b/versions/updates.json index 95f45bf..6413e27 100644 --- a/versions/updates.json +++ b/versions/updates.json @@ -113,6 +113,10 @@ { "version": "1.1Alpha20192301", "update_link": "https://canvasblocker.kkapsner.de/versions/canvasblocker_beta-1.1Alpha20192301-an+fx.xpi" + }, + { + "version": "1.1Alpha20192501", + "update_link": "https://canvasblocker.kkapsner.de/versions/canvasblocker_beta-1.1Alpha20192501-an+fx.xpi" } ] }