mirror of
https://github.com/kkapsner/CanvasBlocker
synced 2024-12-23 05:10:27 +01:00
467 lines
14 KiB
JavaScript
467 lines
14 KiB
JavaScript
/* 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";
|
|
|
|
let scope;
|
|
if ((typeof exports) !== "undefined"){
|
|
scope = exports;
|
|
}
|
|
else {
|
|
scope = require.register("./intercept", {});
|
|
}
|
|
|
|
const {changedFunctions, changedGetters, setRandomSupply} = require("./modifiedAPI");
|
|
const randomSupplies = require("./randomSupplies");
|
|
const logging = require("./logging");
|
|
const settings = require("./settings");
|
|
const extension = require("./extension");
|
|
|
|
setRandomSupply(randomSupplies.nonPersistent);
|
|
const apiNames = Object.keys(changedFunctions);
|
|
let undef;
|
|
function setRandomSupplyByType(type){
|
|
switch (type){
|
|
case "persistent":
|
|
setRandomSupply(randomSupplies.persistent);
|
|
break;
|
|
case "constant":
|
|
setRandomSupply(randomSupplies.constant);
|
|
break;
|
|
case "white":
|
|
setRandomSupply(randomSupplies.white);
|
|
break;
|
|
default:
|
|
setRandomSupply(randomSupplies.nonPersistent);
|
|
}
|
|
}
|
|
settings.on("rng", function(){
|
|
setRandomSupplyByType(settings.rng);
|
|
});
|
|
if (!settings.isStillDefault){
|
|
setRandomSupplyByType(settings.rng);
|
|
}
|
|
|
|
function getURL(windowToProcess){
|
|
let href;
|
|
try {
|
|
href = windowToProcess.location.href;
|
|
}
|
|
catch (error){
|
|
// unable to read location due to SOP
|
|
// since we are not able to do anything in that case we can allow everything
|
|
return "about:SOP";
|
|
}
|
|
if (!href || href === "about:blank"){
|
|
if (windowToProcess !== windowToProcess.parent){
|
|
return getURL(windowToProcess.parent);
|
|
}
|
|
else if (windowToProcess.opener){
|
|
return getURL(windowToProcess.opener);
|
|
}
|
|
}
|
|
return href;
|
|
}
|
|
const getAllFunctionObjects = function(windowToProcess, changedFunction){
|
|
return (
|
|
Array.isArray(changedFunction.object)?
|
|
changedFunction.object:
|
|
[changedFunction.object]
|
|
).map(function(name){
|
|
if (name){
|
|
const constructor = extension.getWrapped(windowToProcess)[name];
|
|
if (constructor){
|
|
return constructor.prototype;
|
|
}
|
|
}
|
|
return false;
|
|
}).concat(
|
|
changedFunction.objectGetters?
|
|
changedFunction.objectGetters.map(function(objectGetter){
|
|
return objectGetter(extension.getWrapped(windowToProcess));
|
|
}):
|
|
[]
|
|
);
|
|
};
|
|
const forEachFunction = function(windowToProcess, callback){
|
|
apiNames.forEach(function(name){
|
|
const changedFunction = changedFunctions[name];
|
|
if (changedFunction.name){
|
|
name = changedFunction.name;
|
|
}
|
|
getAllFunctionObjects(windowToProcess, changedFunction).forEach(function(object){
|
|
if (object){
|
|
callback({name, object: object, changedFunction});
|
|
}
|
|
});
|
|
});
|
|
};
|
|
const forEachGetter = function(windowToProcess, callback){
|
|
changedGetters.forEach(function(changedGetter){
|
|
const name = changedGetter.name;
|
|
changedGetter.objectGetters.forEach(function(changedGetter){
|
|
const object = changedGetter(extension.getWrapped(windowToProcess));
|
|
if (object){
|
|
callback({name, object, objectGetter: changedGetter});
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const forEach = function(windowToProcess, callback){
|
|
forEachFunction(windowToProcess, callback);
|
|
forEachGetter(windowToProcess, callback);
|
|
};
|
|
|
|
const doRealIntercept = function(windowToProcess, apis, state){
|
|
if (!state.intercepted){
|
|
scope.intercept({subject: windowToProcess}, apis);
|
|
state.intercepted = true;
|
|
}
|
|
};
|
|
const doPreIntercept = function(windowToProcess, apis, state){
|
|
const forceLoad = true;
|
|
const originalPropertyDescriptors = {};
|
|
const undoPreIntercept = function(){
|
|
if (state.preIntercepted){
|
|
state.preIntercepted = false;
|
|
forEach(windowToProcess, function({name, object}){
|
|
const originalPropertyDescriptor = originalPropertyDescriptors[name].get(object);
|
|
if (originalPropertyDescriptor){
|
|
Object.defineProperty(
|
|
object,
|
|
name,
|
|
originalPropertyDescriptor
|
|
);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
if (!state.preIntercepted){
|
|
forEach(windowToProcess, function({name, object}){
|
|
const map = originalPropertyDescriptors[name] || new WeakMap();
|
|
originalPropertyDescriptors[name] = map;
|
|
|
|
const originalPropertyDescriptor = Object.getOwnPropertyDescriptor(object, name);
|
|
if (!originalPropertyDescriptor){
|
|
return;
|
|
}
|
|
|
|
map.set(object, originalPropertyDescriptor);
|
|
const temp = class {
|
|
[`get ${name}`](){
|
|
if (forceLoad){
|
|
logging.warning("force load the settings. Calling stack:", (new Error()).stack);
|
|
undoPreIntercept();
|
|
settings.forceLoad();
|
|
doRealIntercept(windowToProcess, apis, state);
|
|
const descriptor = Object.getOwnPropertyDescriptor(object, name);
|
|
return descriptor.value || descriptor.get.call(this);
|
|
}
|
|
else {
|
|
logging.notice("API blocked (%s)", name);
|
|
const url = getURL(windowToProcess);
|
|
if (!url){
|
|
return undef;
|
|
}
|
|
const error = new Error();
|
|
apis.notify({
|
|
url,
|
|
errorStack: error.stack,
|
|
messageId: "preBlock",
|
|
timestamp: new Date(),
|
|
functionName: name,
|
|
dataURL: false
|
|
});
|
|
return undef;
|
|
}
|
|
}
|
|
[`set ${name}`](newValue){}
|
|
}.prototype;
|
|
Object.defineProperty(
|
|
object,
|
|
name,
|
|
{
|
|
enumerable: true,
|
|
configurable: true,
|
|
get: extension.exportFunctionWithName(temp[`get ${name}`], windowToProcess, `get ${name}`),
|
|
set: extension.exportFunctionWithName(temp[`set ${name}`], windowToProcess, `set ${name}`)
|
|
}
|
|
);
|
|
});
|
|
state.preIntercepted = true;
|
|
}
|
|
return undoPreIntercept;
|
|
};
|
|
|
|
scope.preIntercept = function preIntercept({subject: windowToProcess}, apis){
|
|
if (!settings.isStillDefault){
|
|
logging.message("settings already loaded -> no need to pre intercept");
|
|
scope.intercept({subject: windowToProcess}, apis);
|
|
}
|
|
else {
|
|
logging.message("settings not loaded -> need to pre intercept");
|
|
|
|
const state = {
|
|
preIntercepted: false,
|
|
intercepted: false
|
|
};
|
|
|
|
const undoPreIntercept = doPreIntercept(windowToProcess, apis, state);
|
|
settings.onloaded(function(){
|
|
undoPreIntercept();
|
|
doRealIntercept(windowToProcess, apis, state);
|
|
});
|
|
}
|
|
};
|
|
|
|
function getDataURL(object, prefs){
|
|
return (
|
|
object &&
|
|
prefs("storeImageForInspection") &&
|
|
prefs("showNotifications")?
|
|
(
|
|
object instanceof HTMLCanvasElement?
|
|
object.toDataURL():
|
|
(
|
|
object.canvas instanceof HTMLCanvasElement?
|
|
object.canvas.toDataURL():
|
|
false
|
|
)
|
|
):
|
|
false
|
|
);
|
|
}
|
|
|
|
let extensionID = extension.extensionID;
|
|
|
|
function generateChecker({
|
|
name, changedFunction, siteStatus, original,
|
|
window: windowToProcess, prefs, notify, checkStack, ask
|
|
}){
|
|
return function checker(callingDepth = 3){
|
|
const errorStack = (new Error()).stack;
|
|
|
|
try {
|
|
// return original if the extension itself requested the function
|
|
if (
|
|
errorStack
|
|
.split("\n", callingDepth + 2)[callingDepth + 1]
|
|
.split("@", callingDepth + 1)[1]
|
|
.startsWith(extensionID)
|
|
){
|
|
return {allow: true, original, window: windowToProcess};
|
|
}
|
|
}
|
|
catch (error) {
|
|
// stack had an unknown form
|
|
}
|
|
if (checkStack(errorStack)){
|
|
return {allow: true, original, window: windowToProcess};
|
|
}
|
|
const funcStatus = changedFunction.getStatus(this, siteStatus, prefs);
|
|
|
|
const This = this;
|
|
function notifyCallback(messageId){
|
|
notify({
|
|
url: getURL(windowToProcess),
|
|
errorStack,
|
|
messageId,
|
|
timestamp: new Date(),
|
|
functionName: name,
|
|
api: changedFunction.api,
|
|
dataURL: getDataURL(This, prefs)
|
|
});
|
|
}
|
|
const protectedAPIFeatures = prefs("protectedAPIFeatures");
|
|
if (
|
|
funcStatus.active &&
|
|
(
|
|
!protectedAPIFeatures.hasOwnProperty(name + " @ " + changedFunction.api) ||
|
|
protectedAPIFeatures[name + " @ " + changedFunction.api]
|
|
)
|
|
){
|
|
if (funcStatus.mode === "ask"){
|
|
funcStatus.mode = ask({
|
|
window: windowToProcess,
|
|
type: changedFunction.type,
|
|
api: changedFunction.api,
|
|
canvas: this instanceof HTMLCanvasElement?
|
|
this:
|
|
(
|
|
this &&
|
|
(this.canvas instanceof HTMLCanvasElement)?
|
|
this.canvas:
|
|
false
|
|
),
|
|
errorStack
|
|
});
|
|
}
|
|
switch (funcStatus.mode){
|
|
case "allow":
|
|
return {allow: true, original, window: windowToProcess};
|
|
case "fake":
|
|
return {
|
|
allow: "fake",
|
|
prefs,
|
|
notify: notifyCallback,
|
|
window: windowToProcess,
|
|
original
|
|
};
|
|
//case "block":
|
|
default:
|
|
return {allow: false, notify: notifyCallback};
|
|
}
|
|
}
|
|
else {
|
|
return {allow: true, original, window: windowToProcess};
|
|
}
|
|
};
|
|
}
|
|
|
|
function interceptFunctions(windowToProcess, siteStatus, {checkStack, ask, notify, prefs}){
|
|
apiNames.forEach(function(name){
|
|
const changedFunction = changedFunctions[name];
|
|
if (changedFunction.name){
|
|
name = changedFunction.name;
|
|
}
|
|
const functionStatus = changedFunction.getStatus(undefined, siteStatus, prefs);
|
|
logging.verbose("status for", name, ":", functionStatus);
|
|
if (!functionStatus.active) return;
|
|
|
|
getAllFunctionObjects(windowToProcess, changedFunction).forEach(function(object){
|
|
if (!object) return;
|
|
|
|
const original = object[name];
|
|
const checker = generateChecker({
|
|
name, changedFunction, siteStatus, original,
|
|
window: windowToProcess, prefs, checkStack, ask, notify
|
|
});
|
|
const descriptor = Object.getOwnPropertyDescriptor(object, name);
|
|
if (!descriptor) return;
|
|
const type = descriptor.hasOwnProperty("value")? "value": "get";
|
|
let changed;
|
|
if (type ==="value"){
|
|
if (changedFunction.fakeGenerator){
|
|
if ((changedFunction.exportOptions || {}).allowCallbacks){
|
|
changed = extension.exportFunctionWithName(
|
|
changedFunction.fakeGenerator(checker, original, windowToProcess),
|
|
windowToProcess,
|
|
original.name
|
|
);
|
|
}
|
|
else {
|
|
const generated = changedFunction.fakeGenerator(checker, original, windowToProcess);
|
|
changed = extension.createProxyFunction(windowToProcess, original, generated);
|
|
}
|
|
}
|
|
else {
|
|
changed = null;
|
|
}
|
|
}
|
|
else {
|
|
changed = extension.createProxyFunction(windowToProcess, original, extension.exportFunctionWithName(
|
|
changedFunction.fakeGenerator(checker),
|
|
windowToProcess,
|
|
original.name
|
|
));
|
|
}
|
|
extension.changeProperty(windowToProcess, changedFunction.api, {
|
|
object, name, type, changed
|
|
});
|
|
});
|
|
});
|
|
}
|
|
function interceptGetters(windowToProcess, siteStatus, {checkStack, ask, notify, prefs}){
|
|
changedGetters.forEach(function(changedGetter){
|
|
const name = changedGetter.name;
|
|
const functionStatus = changedGetter.getStatus(undefined, siteStatus, prefs);
|
|
logging.verbose("status for", changedGetter, ":", functionStatus);
|
|
if (!functionStatus.active) return;
|
|
|
|
changedGetter.objectGetters.forEach(function(objectGetter){
|
|
const object = objectGetter(extension.getWrapped(windowToProcess));
|
|
if (!object) return;
|
|
|
|
const descriptor = Object.getOwnPropertyDescriptor(object, name);
|
|
if (!descriptor) return;
|
|
|
|
if (descriptor.hasOwnProperty("get")){
|
|
const original = descriptor.get;
|
|
const checker = generateChecker({
|
|
name, changedFunction: changedGetter, siteStatus, original,
|
|
window: windowToProcess, prefs, checkStack, ask, notify
|
|
});
|
|
const getter = changedGetter.getterGenerator(checker, original, windowToProcess);
|
|
extension.changeProperty(windowToProcess, changedGetter.api,
|
|
{
|
|
object, name, type: "get",
|
|
changed: extension.createProxyFunction(windowToProcess, original, getter)
|
|
}
|
|
);
|
|
|
|
if (descriptor.hasOwnProperty("set") && descriptor.set && changedGetter.setterGenerator){
|
|
const original = descriptor.set;
|
|
const setter = changedGetter.setterGenerator(
|
|
windowToProcess,
|
|
original,
|
|
prefs
|
|
);
|
|
extension.changeProperty(windowToProcess, changedGetter.api,
|
|
{
|
|
object, name, type: "set",
|
|
changed: extension.createProxyFunction(windowToProcess, original, setter)
|
|
}
|
|
);
|
|
}
|
|
|
|
}
|
|
else if (
|
|
changedGetter.valueGenerator &&
|
|
descriptor.hasOwnProperty("value")
|
|
){
|
|
const protectedAPIFeatures = prefs("protectedAPIFeatures");
|
|
if (
|
|
protectedAPIFeatures.hasOwnProperty(name + " @ " + changedGetter.api) &&
|
|
!protectedAPIFeatures[name + " @ " + changedGetter.api]
|
|
){
|
|
return;
|
|
}
|
|
switch (functionStatus.mode){
|
|
case "ask": case "block": case "fake":
|
|
extension.changeProperty(windowToProcess, changedGetter.api, {
|
|
object, name, type: "value",
|
|
changed: changedGetter.valueGenerator({
|
|
mode: functionStatus.mode,
|
|
original: descriptor.value,
|
|
notify: function notifyCallback(messageId){
|
|
notify({
|
|
url: getURL(windowToProcess),
|
|
errorStack: (new Error()).stack,
|
|
messageId,
|
|
timestamp: new Date(),
|
|
functionName: name,
|
|
api: changedGetter.api
|
|
});
|
|
}
|
|
})
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
logging.error("Try to fake non getter property:", changedGetter);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
scope.intercept = function intercept({subject: windowToProcess}, apis){
|
|
const siteStatus = apis.check({url: getURL(windowToProcess)});
|
|
logging.verbose("status for page", windowToProcess, siteStatus);
|
|
if (siteStatus.mode !== "allow"){
|
|
interceptFunctions(windowToProcess, siteStatus, apis);
|
|
interceptGetters(windowToProcess, siteStatus, apis);
|
|
}
|
|
};
|
|
}()); |