From f00c3b674e1873b5d6c718661adfb98a46277533 Mon Sep 17 00:00:00 2001 From: kkapsner Date: Tue, 4 Sep 2018 23:29:58 +0200 Subject: [PATCH] First DOMRect draft For #236 --- .vscode/settings.json | 2 + _locales/en/messages.json | 29 +++ lib/intercept.js | 2 +- lib/modifiedAPI.js | 3 + lib/modifiedDOMRectAPI.js | 409 +++++++++++++++++++++++++++++++++++++ lib/settingDefinitions.js | 15 ++ manifest.json | 1 + options/settingsDisplay.js | 7 + 8 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 lib/modifiedDOMRectAPI.js diff --git a/.vscode/settings.json b/.vscode/settings.json index ed9cc7b..1819391 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,8 @@ "Nachfrageverweigerungsmodus", "Oakenpants", "PDFs", + "Rect", + "Rects", "Spoofer", "Thorin", "Vortäuschaktion", diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 383f776..fbbb1b0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -105,6 +105,10 @@ "message": "Window API", "description": "" }, + "section_DOMRect-api":{ + "message": "DOMRect API", + "description": "" + }, "displayAdvancedSettings_title": { "message": "Expert mode", @@ -213,6 +217,18 @@ "message": "Do you want to allow window readout?", "description": "" }, + "askForDOMRectPermission": { + "message": "Do you want to allow the DOMRect API?", + "description": "" + }, + "askForDOMRectInputPermission": { + "message": "Do you want to allow DOMRect-API input?", + "description": "" + }, + "askForDOMRectReadoutPermission": { + "message": "Do you want to allow DOMRect readout?", + "description": "" + }, "askOnlyOnce_title": { "message": "Ask only once", "description": "" @@ -544,6 +560,10 @@ "message": "Faked window readout on {url}", "description": "" }, + "fakedDOMRectReadout": { + "message": "Faked DOMRect readout on {url}", + "description": "" + }, "fakedInput": { "message": "Faked at input on {url}", "description": "" @@ -869,6 +889,15 @@ "description": "" }, + "protectDOMRect_title": { + "message": "Protect DOMRect API", + "description": "" + }, + "protectDOMRect_description": { + "message": "This protects against the \"getClientRects()\" fingerprinting and several similar methods.", + "description": "" + }, + "theme_title": { "message": "Theme", "description": "" diff --git a/lib/intercept.js b/lib/intercept.js index 7eea9f5..e35437e 100644 --- a/lib/intercept.js +++ b/lib/intercept.js @@ -325,7 +325,7 @@ const getter = changedGetter.getterGenerator(checker); descriptor.get = exportFunction(getter, window); - if (changedGetter.setterGenerator){ + if (descriptor.hasOwnProperty("set") && changedGetter.setterGenerator){ const setter = changedGetter.setterGenerator(window, descriptor.set); descriptor.set = exportFunction(setter, window); } diff --git a/lib/modifiedAPI.js b/lib/modifiedAPI.js index 2d3accb..02d721c 100644 --- a/lib/modifiedAPI.js +++ b/lib/modifiedAPI.js @@ -19,6 +19,7 @@ const getWrapped = require("sdk/getWrapped"); const {hasType, checkerWrapper} = require("./modifiedAPIFunctions"); const modifiedAudioAPI = require("./modifiedAudioAPI"); + const modifiedDOMRectAPI = require("./modifiedDOMRectAPI"); var randomSupply = null; @@ -155,6 +156,7 @@ scope.setRandomSupply = function(supply){ randomSupply = supply; modifiedAudioAPI.setRandomSupply(supply); + modifiedDOMRectAPI.setRandomSupply(supply); }; var canvasContextType = new WeakMap(); // changed functions and their fakes @@ -488,4 +490,5 @@ appendModified(modifiedAudioAPI); appendModified(require("./modifiedHistoryAPI")); appendModified(require("./modifiedWindowAPI")); + appendModified(modifiedDOMRectAPI); }()); \ No newline at end of file diff --git a/lib/modifiedDOMRectAPI.js b/lib/modifiedDOMRectAPI.js new file mode 100644 index 0000000..ca8a3f6 --- /dev/null +++ b/lib/modifiedDOMRectAPI.js @@ -0,0 +1,409 @@ +/* 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(){ + "use strict"; + + var scope; + if ((typeof exports) !== "undefined"){ + scope = exports; + } + else { + window.scope.modifiedDOMRectAPI = {}; + scope = window.scope.modifiedDOMRectAPI; + } + + const {hasType, checkerWrapper} = require("./modifiedAPIFunctions"); + const {md5String: hash} = require("./hash"); + const getWrapped = require("sdk/getWrapped"); + + + var randomSupply = null; + scope.setRandomSupply = function(supply){ + randomSupply = supply; + }; + + const cache = new Map(); + function getHash(domRect){ + return hash(new Float64Array([domRect.x, domRect.y, domRect.width, domRect.height])); + } + + const registeredRects = new WeakMap(); + function registerDOMRect(domRect, notify){ + registeredRects.set(getWrapped(domRect), { + notify: function(){ + let done = false; + return function(message){ + if (!done){ + done = true; + notify(message); + } + }; + }() + }); + } + function getDOMRectRegistration(domRect){ + return registeredRects.get(getWrapped(domRect)); + } + function getFakeDomRect(window, domRect, notify){ + + var rng = randomSupply.getRng(4, window); + function getFakeValue(value, i){ + if (value % 1 === 0){ + return value; + } + else { + return value + 0.01 * (rng(i) / 0xffffffff - 0.5); + } + } + const hash = getHash(domRect); + let cached = cache.get(hash); + if (!cached){ + notify("fakedDOMRectReadout"); + cached = new domRect.constructor( + getFakeValue(domRect.x, 0), + getFakeValue(domRect.y, 1), + getFakeValue(domRect.width, 2), + getFakeValue(domRect.height, 3) + ); + cache.set(getHash(cached), cached); + } + return cached; + } + + scope.changedFunctions = { + getClientRects: { + type: "domRect", + getStatus: getStatus, + object: ["Range", "Element"], + fakeGenerator: function(checker){ + return function getClientRects(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + var {prefs, notify, window, original} = check; + var ret = original.apply(this, window.Array.from(args)); + for (let i = 0; i < ret.length; i += 1){ + registerDOMRect(ret[i], notify); + } + return ret; + }); + }; + } + }, + getBoundingClientRect: { + type: "domRect", + getStatus: getStatus, + object: ["Range", "Element"], + fakeGenerator: function(checker){ + return function getBoundingClientRect(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + var {prefs, notify, window, original} = check; + var ret = original.apply(this, window.Array.from(args)); + registerDOMRect(ret, notify); + return ret; + }); + }; + + } + }, + getBounds: { + type: "domRect", + getStatus: getStatus, + object: ["DOMQuad"], + fakeGenerator: function(checker){ + return function getBounds(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + var {prefs, notify, window, original} = check; + var ret = original.apply(this, window.Array.from(args)); + registerDOMRect(ret, notify); + return ret; + }); + }; + } + }, + getBBox: { + type: "domRect", + getStatus: getStatus, + object: ["SVGGraphicsElement"], + fakeGenerator: function(checker){ + return function getBBox(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + var {prefs, notify, window, original} = check; + var ret = original.apply(this, window.Array.from(args)); + registerDOMRect(ret, notify); + return ret; + }); + }; + } + }, + getExtentOfChar: { + type: "domRect", + getStatus: getStatus, + object: ["SVGTextContentElement"], + fakeGenerator: function(checker){ + return function getBBox(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + var {prefs, notify, window, original} = check; + var ret = original.apply(this, window.Array.from(args)); + registerDOMRect(ret, notify); + return ret; + }); + }; + } + }, + }; + + function createCheckerCallback(property){ + return function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + if (prefs("protectDOMRect", window.location)){ + const registration = getDOMRectRegistration(this); + if (registration){ + return getFakeDomRect(window, this, registration.notify)[property]; + } + } + + return originalValue; + }; + } + function setProperty(domRect, window, original, newValue, property){ // eslint-disable-line max-params + const registration = getDOMRectRegistration(domRect); + if (registration){ + const fakeDomRect = getFakeDomRect(window, domRect, registration.notify); + registeredRects.delete(getWrapped(domRect)); + ["x", "y", "width", "height"].forEach(function(prop){ + if (prop === property){ + domRect[prop] = newValue; + } + else { + domRect[prop] = fakeDomRect[prop]; + } + }); + } + else { + original.apply(domRect, window.Array.from(arguments)); + } + } + + scope.changedGetters = [ + { + objectGetters: [ + function(window){return window.DOMRect.prototype;}, + function(window){return window.DOMRectReadOnly.prototype;} + ], + name: "x", + getterGenerator: function(checker){ + const temp = { + get x(){ + return checkerWrapper(checker, this, arguments, createCheckerCallback("x")); + } + }; + return Object.getOwnPropertyDescriptor(temp, "x").get; + }, + setterGenerator: function(window, original){ + const temp = { + set x(x){ + setProperty(this, window, original, x, "x"); + } + }; + return Object.getOwnPropertyDescriptor(temp, "x").set; + } + }, + { + objectGetters: [ + function(window){return window.DOMRect.prototype;}, + function(window){return window.DOMRectReadOnly.prototype;} + ], + name: "y", + getterGenerator: function(checker){ + const temp = { + get y(){ + return checkerWrapper(checker, this, arguments, createCheckerCallback("y")); + } + }; + return Object.getOwnPropertyDescriptor(temp, "y").get; + }, + setterGenerator: function(window, original){ + const temp = { + set y(y){ + setProperty(this, window, original, y, "y"); + } + }; + return Object.getOwnPropertyDescriptor(temp, "y").set; + } + }, + { + objectGetters: [ + function(window){return window.DOMRect.prototype;}, + function(window){return window.DOMRectReadOnly.prototype;} + ], + name: "width", + getterGenerator: function(checker){ + const temp = { + get width(){ + return checkerWrapper(checker, this, arguments, createCheckerCallback("width")); + } + }; + return Object.getOwnPropertyDescriptor(temp, "width").get; + }, + setterGenerator: function(window, original){ + const temp = { + set width(width){ + setProperty(this, window, original, width, "width"); + } + }; + return Object.getOwnPropertyDescriptor(temp, "width").set; + } + }, + { + objectGetters: [ + function(window){return window.DOMRect.prototype;}, + function(window){return window.DOMRectReadOnly.prototype;} + ], + name: "height", + getterGenerator: function(checker){ + const temp = { + get height(){ + return checkerWrapper(checker, this, arguments, createCheckerCallback("height")); + } + }; + return Object.getOwnPropertyDescriptor(temp, "height").get; + }, + setterGenerator: function(window, original){ + const temp = { + set height(height){ + setProperty(this, window, original, height, "height"); + } + }; + return Object.getOwnPropertyDescriptor(temp, "height").set; + } + }, + { + objectGetters: [ + function(window){return window.DOMRectReadOnly.prototype;} + ], + name: "left", + getterGenerator: function(checker){ + const callback = createCheckerCallback("left"); + const temp = { + get left(){ + return checkerWrapper(checker, this, arguments, callback); + } + }; + return Object.getOwnPropertyDescriptor(temp, "left").get; + } + }, + { + objectGetters: [ + function(window){return window.DOMRectReadOnly.prototype;} + ], + name: "right", + getterGenerator: function(checker){ + const temp = { + get right(){ + return checkerWrapper(checker, this, arguments, createCheckerCallback("right")); + } + }; + return Object.getOwnPropertyDescriptor(temp, "right").get; + } + }, + { + objectGetters: [ + function(window){return window.DOMRectReadOnly.prototype;} + ], + name: "top", + getterGenerator: function(checker){ + const temp = { + get top(){ + return checkerWrapper(checker, this, arguments, createCheckerCallback("top")); + } + }; + return Object.getOwnPropertyDescriptor(temp, "top").get; + } + }, + { + objectGetters: [ + function(window){return window.DOMRectReadOnly.prototype;} + ], + name: "bottom", + getterGenerator: function(checker){ + const temp = { + get bottom(){ + return checkerWrapper(checker, this, arguments, createCheckerCallback("bottom")); + } + }; + return Object.getOwnPropertyDescriptor(temp, "bottom").get; + } + }, + { + objectGetters: [ + function(window){return window.IntersectionObserverEntry.prototype;} + ], + name: "intersectionRect", + getterGenerator: function(checker){ + const temp = { + get intersectionRect(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + registerDOMRect(originalValue, notify); + return originalValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "intersectionRect").get; + } + }, + { + objectGetters: [ + function(window){return window.IntersectionObserverEntry.prototype;} + ], + name: "boundingClientRect", + getterGenerator: function(checker){ + const temp = { + get boundingClientRect(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + registerDOMRect(originalValue, notify); + return originalValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "boundingClientRect").get; + } + }, + { + objectGetters: [ + function(window){return window.IntersectionObserverEntry.prototype;} + ], + name: "rootBounds", + getterGenerator: function(checker){ + const temp = { + get rootBounds(){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.apply(this, window.Array.from(args)); + registerDOMRect(originalValue, notify); + return originalValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, "rootBounds").get; + } + } + ]; + + + function getStatus(obj, status){ + status = Object.create(status); + status.active = hasType(status, "readout"); + return status; + } + + scope.changedGetters.forEach(function(changedGetter){ + changedGetter.type = "readout"; + changedGetter.getStatus = getStatus; + changedGetter.api = "domRect"; + }); +}()); \ No newline at end of file diff --git a/lib/settingDefinitions.js b/lib/settingDefinitions.js index 7533d9b..ef530f3 100644 --- a/lib/settingDefinitions.js +++ b/lib/settingDefinitions.js @@ -95,6 +95,15 @@ {name: "Window-API", level: 1}, "opener", "name", + {name: "DOMRect-API", level: 1}, + "getClientRects", + "getBoundingClientRect", + "getBounds", + "getBBox", + "getExtentOfChar", + "intersectionRect", + "boundingClientRect", + "rootBounds", ], defaultKeyValue: false }, @@ -195,6 +204,7 @@ "audio", "history", "window", + "DOMRect", ], defaultKeyValue: false }, @@ -252,6 +262,11 @@ defaultValue: false, urlSpecific: true }, + { + name: "protectDOMRect", + defaultValue: true, + urlSpecific: true + }, { name: "blockDataURLs", defaultValue: true diff --git a/manifest.json b/manifest.json index 145aa51..e62c476 100644 --- a/manifest.json +++ b/manifest.json @@ -39,6 +39,7 @@ "lib/modifiedAudioAPI.js", "lib/modifiedHistoryAPI.js", "lib/modifiedWindowAPI.js", + "lib/modifiedDOMRectAPI.js", "lib/modifiedAPI.js", "lib/randomSupplies.js", "lib/intercept.js", diff --git a/options/settingsDisplay.js b/options/settingsDisplay.js index 4be9493..589bb18 100644 --- a/options/settingsDisplay.js +++ b/options/settingsDisplay.js @@ -404,6 +404,13 @@ "displayAdvancedSettings": [true] } }, + "DOMRect-API", + { + "name": "protectDOMRect", + "displayDependencies": { + "displayAdvancedSettings": [true] + } + }, "misc", { "name": "theme"