1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-07-08 12:57:57 +02:00

Ensure scriptlet logging information make it to destination

Avoid race conditions between isolated world-side broadcast channel
and main-side broadcast channel, so as to not lose logging
information if the isolated world-side is not yet ready to
receive through its broadcast channel.

Additionally, added new scriptlet: `trusted-replace-argument`.

[...]##+js(trusted-replace-argument, fn, argpos, argval [,condition, pattern])

Where:

- `fn` is the function we want to proxy through an `apply` handler.
  This can also be a class, in which case the scriptlet will proxy
  through `construct` handler. At the moment, `fn` must exist at the
  time the scriptlet executes.

- `argpos` is the 0-based position of the argument we want to change

- `argval` is the value we want to have for the argument -- the value
  is interpreted the same way the value for `set-constant` is
  interpreted.

- `condition, pattern` is a vararg which tells the scriptlet to act
  only if `pattern` is found in the argument to overwrite.

Example of usage:

    alliptvlinks.com##+js(trusted-replace-argument, MutationObserver, 0, noopFunc)
This commit is contained in:
Raymond Hill 2024-01-26 12:18:30 -05:00
parent 55e4cee6e8
commit 34da372d7a
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
4 changed files with 217 additions and 115 deletions

View File

@ -156,25 +156,39 @@ function safeSelf() {
return this.Object_fromEntries(entries); return this.Object_fromEntries(entries);
}, },
}; };
if ( scriptletGlobals.bcSecret !== undefined ) {
const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret);
safe.logLevel = scriptletGlobals.logLevel || 1;
safe.sendToLogger = (type, ...args) => {
if ( args.length === 0 ) { return; }
const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`;
bc.postMessage({ what: 'messageToLogger', type, text });
};
bc.onmessage = ev => {
const msg = ev.data;
if ( msg instanceof Object === false ) { return; }
switch ( msg.what ) {
case 'setScriptletLogLevel':
safe.logLevel = msg.level;
break;
}
};
}
scriptletGlobals.safeSelf = safe; scriptletGlobals.safeSelf = safe;
if ( scriptletGlobals.bcSecret === undefined ) { return safe; }
// This is executed only when the logger is opened
const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret);
let bcBuffer = [];
safe.logLevel = scriptletGlobals.logLevel || 1;
safe.sendToLogger = (type, ...args) => {
if ( args.length === 0 ) { return; }
const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`;
if ( bcBuffer === undefined ) {
return bc.postMessage({ what: 'messageToLogger', type, text });
}
bcBuffer.push({ type, text });
};
bc.onmessage = ev => {
const msg = ev.data;
switch ( msg ) {
case 'iamready!':
if ( bcBuffer === undefined ) { break; }
bcBuffer.forEach(({ type, text }) =>
bc.postMessage({ what: 'messageToLogger', type, text })
);
bcBuffer = undefined;
break;
case 'setScriptletLogLevelToOne':
safe.logLevel = 1;
break;
case 'setScriptletLogLevelToTwo':
safe.logLevel = 2;
break;
}
};
bc.postMessage('areyouready?');
return safe; return safe;
} }
@ -454,34 +468,90 @@ function abortCurrentScriptCore(
/******************************************************************************/ /******************************************************************************/
builtinScriptlets.push({ builtinScriptlets.push({
name: 'set-constant-core.fn', name: 'validate-constant.fn',
fn: setConstantCore, fn: validateConstantFn,
dependencies: [ dependencies: [
'run-at.fn',
'safe-self.fn', 'safe-self.fn',
], ],
}); });
function validateConstantFn(trusted, raw) {
const safe = safeSelf();
const extraArgs = safe.getExtraArgs(Array.from(arguments), 2);
let value;
if ( raw === 'undefined' ) {
value = undefined;
} else if ( raw === 'false' ) {
value = false;
} else if ( raw === 'true' ) {
value = true;
} else if ( raw === 'null' ) {
value = null;
} else if ( raw === "''" || raw === '' ) {
value = '';
} else if ( raw === '[]' || raw === 'emptyArr' ) {
value = [];
} else if ( raw === '{}' || raw === 'emptyObj' ) {
value = {};
} else if ( raw === 'noopFunc' ) {
value = function(){};
} else if ( raw === 'trueFunc' ) {
value = function(){ return true; };
} else if ( raw === 'falseFunc' ) {
value = function(){ return false; };
} else if ( /^-?\d+$/.test(raw) ) {
value = parseInt(raw);
if ( isNaN(raw) ) { return; }
if ( Math.abs(raw) > 0x7FFF ) { return; }
} else if ( trusted ) {
if ( raw.startsWith('{') && raw.endsWith('}') ) {
try { value = safe.JSON_parse(raw).value; } catch(ex) { return; }
}
} else {
return;
}
if ( extraArgs.as !== undefined ) {
if ( extraArgs.as === 'function' ) {
return ( ) => value;
} else if ( extraArgs.as === 'callback' ) {
return ( ) => (( ) => value);
} else if ( extraArgs.as === 'resolved' ) {
return Promise.resolve(value);
} else if ( extraArgs.as === 'rejected' ) {
return Promise.reject(value);
}
}
return value;
}
function setConstantCore( /******************************************************************************/
builtinScriptlets.push({
name: 'set-constant.fn',
fn: setConstantFn,
dependencies: [
'run-at.fn',
'safe-self.fn',
'validate-constant.fn',
],
});
function setConstantFn(
trusted = false, trusted = false,
chain = '', chain = '',
cValue = '' rawValue = ''
) { ) {
if ( chain === '' ) { return; } if ( chain === '' ) { return; }
const safe = safeSelf(); const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('set-constant', chain, cValue); const logPrefix = safe.makeLogPrefix('set-constant', chain, rawValue);
const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
function setConstant(chain, cValue) { function setConstant(chain, rawValue) {
const trappedProp = (( ) => { const trappedProp = (( ) => {
const pos = chain.lastIndexOf('.'); const pos = chain.lastIndexOf('.');
if ( pos === -1 ) { return chain; } if ( pos === -1 ) { return chain; }
return chain.slice(pos+1); return chain.slice(pos+1);
})(); })();
if ( trappedProp === '' ) { return; }
const thisScript = document.currentScript;
const cloakFunc = fn => { const cloakFunc = fn => {
safe.Object_defineProperty(fn, 'name', { value: trappedProp }); safe.Object_defineProperty(fn, 'name', { value: trappedProp });
const proxy = new Proxy(fn, { return new Proxy(fn, {
defineProperty(target, prop) { defineProperty(target, prop) {
if ( prop !== 'toString' ) { if ( prop !== 'toString' ) {
return Reflect.defineProperty(...arguments); return Reflect.defineProperty(...arguments);
@ -503,50 +573,12 @@ function setConstantCore(
return Reflect.get(...arguments); return Reflect.get(...arguments);
}, },
}); });
return proxy;
}; };
if ( cValue === 'undefined' ) { if ( trappedProp === '' ) { return; }
cValue = undefined; const thisScript = document.currentScript;
} else if ( cValue === 'false' ) { let normalValue = validateConstantFn(trusted, rawValue);
cValue = false; if ( rawValue === 'noopFunc' || rawValue === 'trueFunc' || rawValue === 'falseFunc' ) {
} else if ( cValue === 'true' ) { normalValue = cloakFunc(normalValue);
cValue = true;
} else if ( cValue === 'null' ) {
cValue = null;
} else if ( cValue === "''" || cValue === '' ) {
cValue = '';
} else if ( cValue === '[]' || cValue === 'emptyArr' ) {
cValue = [];
} else if ( cValue === '{}' || cValue === 'emptyObj' ) {
cValue = {};
} else if ( cValue === 'noopFunc' ) {
cValue = cloakFunc(function(){});
} else if ( cValue === 'trueFunc' ) {
cValue = cloakFunc(function(){ return true; });
} else if ( cValue === 'falseFunc' ) {
cValue = cloakFunc(function(){ return false; });
} else if ( /^-?\d+$/.test(cValue) ) {
cValue = parseInt(cValue);
if ( isNaN(cValue) ) { return; }
if ( Math.abs(cValue) > 0x7FFF ) { return; }
} else if ( trusted ) {
if ( cValue.startsWith('{') && cValue.endsWith('}') ) {
try { cValue = safe.JSON_parse(cValue).value; } catch(ex) { return; }
}
} else {
return;
}
if ( extraArgs.as !== undefined ) {
const value = cValue;
if ( extraArgs.as === 'function' ) {
cValue = ( ) => value;
} else if ( extraArgs.as === 'callback' ) {
cValue = ( ) => (( ) => value);
} else if ( extraArgs.as === 'resolved' ) {
cValue = Promise.resolve(value);
} else if ( extraArgs.as === 'rejected' ) {
cValue = Promise.reject(value);
}
} }
let aborted = false; let aborted = false;
const mustAbort = function(v) { const mustAbort = function(v) {
@ -554,8 +586,8 @@ function setConstantCore(
if ( aborted ) { return true; } if ( aborted ) { return true; }
aborted = aborted =
(v !== undefined && v !== null) && (v !== undefined && v !== null) &&
(cValue !== undefined && cValue !== null) && (normalValue !== undefined && normalValue !== null) &&
(typeof v !== typeof cValue); (typeof v !== typeof normalValue);
if ( aborted ) { if ( aborted ) {
safe.uboLog(logPrefix, `Aborted because value set to ${v}`); safe.uboLog(logPrefix, `Aborted because value set to ${v}`);
} }
@ -564,11 +596,11 @@ function setConstantCore(
// https://github.com/uBlockOrigin/uBlock-issues/issues/156 // https://github.com/uBlockOrigin/uBlock-issues/issues/156
// Support multiple trappers for the same property. // Support multiple trappers for the same property.
const trapProp = function(owner, prop, configurable, handler) { const trapProp = function(owner, prop, configurable, handler) {
if ( handler.init(configurable ? owner[prop] : cValue) === false ) { return; } if ( handler.init(configurable ? owner[prop] : normalValue) === false ) { return; }
const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop); const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop);
let prevGetter, prevSetter; let prevGetter, prevSetter;
if ( odesc instanceof safe.Object ) { if ( odesc instanceof safe.Object ) {
owner[prop] = cValue; owner[prop] = normalValue;
if ( odesc.get instanceof Function ) { if ( odesc.get instanceof Function ) {
prevGetter = odesc.get; prevGetter = odesc.get;
} }
@ -583,7 +615,7 @@ function setConstantCore(
if ( prevGetter !== undefined ) { if ( prevGetter !== undefined ) {
prevGetter(); prevGetter();
} }
return handler.getter(); // cValue return handler.getter();
}, },
set(a) { set(a) {
if ( prevSetter !== undefined ) { if ( prevSetter !== undefined ) {
@ -592,7 +624,9 @@ function setConstantCore(
handler.setter(a); handler.setter(a);
} }
}); });
safe.uboLog(logPrefix, 'Trap installed');
} catch(ex) { } catch(ex) {
safe.uboErr(logPrefix, ex);
} }
}; };
const trapChain = function(owner, chain) { const trapChain = function(owner, chain) {
@ -610,11 +644,11 @@ function setConstantCore(
return this.v; return this.v;
} }
safe.uboLog(logPrefix, 'Property read'); safe.uboLog(logPrefix, 'Property read');
return cValue; return normalValue;
}, },
setter: function(a) { setter: function(a) {
if ( mustAbort(a) === false ) { return; } if ( mustAbort(a) === false ) { return; }
cValue = a; normalValue = a;
} }
}); });
return; return;
@ -646,7 +680,7 @@ function setConstantCore(
trapChain(window, chain); trapChain(window, chain);
} }
runAt(( ) => { runAt(( ) => {
setConstant(chain, cValue); setConstant(chain, rawValue);
}, extraArgs.runAt); }, extraArgs.runAt);
} }
@ -1307,6 +1341,37 @@ function replaceFetchResponseFn(
}); });
} }
/******************************************************************************/
builtinScriptlets.push({
name: 'proxy-apply.fn',
fn: proxyApplyFn,
dependencies: [
'safe-self.fn',
],
});
function proxyApplyFn(
target = '',
handler = ''
) {
let context = globalThis;
let prop = target;
for (;;) {
const pos = prop.indexOf('.');
if ( pos === -1 ) { break; }
context = context[prop.slice(0, pos)];
if ( context instanceof Object === false ) { return; }
prop = prop.slice(pos+1);
}
const fn = context[prop];
if ( typeof fn !== 'function' ) { return; }
if ( fn.prototype && fn.prototype.constructor === fn ) {
context[prop] = new Proxy(fn, { construct: handler });
return (...args) => { return Reflect.construct(...args); };
}
context[prop] = new Proxy(fn, { apply: handler });
return (...args) => { return Reflect.apply(...args); };
}
/******************************************************************************* /*******************************************************************************
@ -2208,13 +2273,13 @@ builtinScriptlets.push({
], ],
fn: setConstant, fn: setConstant,
dependencies: [ dependencies: [
'set-constant-core.fn' 'set-constant.fn'
], ],
}); });
function setConstant( function setConstant(
...args ...args
) { ) {
setConstantCore(false, ...args); setConstantFn(false, ...args);
} }
/******************************************************************************/ /******************************************************************************/
@ -2517,7 +2582,7 @@ function noXhrIf(
details.xhr.dispatchEvent(new Event('readystatechange')); details.xhr.dispatchEvent(new Event('readystatechange'));
details.xhr.dispatchEvent(new Event('load')); details.xhr.dispatchEvent(new Event('load'));
details.xhr.dispatchEvent(new Event('loadend')); details.xhr.dispatchEvent(new Event('loadend'));
safe.uboLog(logPrefix, `Prevented with:\n${details.xhr.response}`); safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`);
}); });
} }
getResponseHeader(headerName) { getResponseHeader(headerName) {
@ -3944,13 +4009,13 @@ builtinScriptlets.push({
], ],
fn: trustedSetConstant, fn: trustedSetConstant,
dependencies: [ dependencies: [
'set-constant-core.fn' 'set-constant.fn'
], ],
}); });
function trustedSetConstant( function trustedSetConstant(
...args ...args
) { ) {
setConstantCore(true, ...args); setConstantFn(true, ...args);
} }
/******************************************************************************* /*******************************************************************************
@ -4437,40 +4502,67 @@ builtinScriptlets.push({
fn: trustedPruneOutboundObject, fn: trustedPruneOutboundObject,
dependencies: [ dependencies: [
'object-prune.fn', 'object-prune.fn',
'proxy-apply.fn',
'safe-self.fn', 'safe-self.fn',
], ],
}); });
function trustedPruneOutboundObject( function trustedPruneOutboundObject(
entryPoint = '', propChain = '',
rawPrunePaths = '', rawPrunePaths = '',
rawNeedlePaths = '' rawNeedlePaths = ''
) { ) {
if ( entryPoint === '' ) { return; } if ( propChain === '' ) { return; }
let context = globalThis;
let prop = entryPoint;
for (;;) {
const pos = prop.indexOf('.');
if ( pos === -1 ) { break; }
context = context[prop.slice(0, pos)];
if ( context instanceof Object === false ) { return; }
prop = prop.slice(pos+1);
}
if ( typeof context[prop] !== 'function' ) { return; }
const safe = safeSelf(); const safe = safeSelf();
const extraArgs = safe.getExtraArgs(Array.from(arguments), 3); const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
context[prop] = new Proxy(context[prop], { const reflector = proxyApplyFn(propChain, function(...args) {
apply: function(target, thisArg, args) { const objBefore = reflector(...args);
const objBefore = Reflect.apply(target, thisArg, args); if ( objBefore instanceof Object === false ) { return objBefore; }
if ( objBefore instanceof Object === false ) { return objBefore; } const objAfter = objectPruneFn(
const objAfter = objectPruneFn( objBefore,
objBefore, rawPrunePaths,
rawPrunePaths, rawNeedlePaths,
rawNeedlePaths, { matchAll: true },
{ matchAll: true }, extraArgs
extraArgs );
); return objAfter || objBefore;
return objAfter || objBefore; });
}, }
/******************************************************************************/
builtinScriptlets.push({
name: 'trusted-replace-argument.js',
requiresTrust: true,
fn: trustedReplaceArgument,
dependencies: [
'proxy-apply.fn',
'safe-self.fn',
'validate-constant.fn',
],
});
function trustedReplaceArgument(
propChain = '',
argpos = '',
argraw = ''
) {
if ( propChain === '' ) { return; }
if ( argpos === '' ) { return; }
if ( argraw === '' ) { return; }
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('trusted-replace-argument', propChain, argpos, argraw);
const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
const normalValue = validateConstantFn(true, argraw);
const reCondition = extraArgs.condition
? safe.patternToRegex(extraArgs.condition)
: /^/;
const reflector = proxyApplyFn(propChain, function(...args) {
const arglist = args.length >= 2 && args[1];
if ( Array.isArray(arglist) === false ) { return reflector(...args); }
const argBefore = arglist[argpos];
if ( reCondition.test(argBefore) === false ) { return reflector(...args); }
arglist[argpos] = normalValue;
safe.uboLog(logPrefix, `Replaced argument:\nBefore: ${argBefore.trim()}\nAfter: ${normalValue}`);
return reflector(...args);
}); });
} }

View File

@ -180,12 +180,22 @@ const onScriptletMessageInjector = (( ) => {
if ( vAPI.bcSecret ) { return; } if ( vAPI.bcSecret ) { return; }
const bcSecret = new self.BroadcastChannel(name); const bcSecret = new self.BroadcastChannel(name);
bcSecret.onmessage = ev => { bcSecret.onmessage = ev => {
if ( self.vAPI && self.vAPI.messaging ) { const msg = ev.data;
self.vAPI.messaging.send('contentscript', ev.data); switch ( typeof msg ) {
} else { case 'string':
bcSecret.onmessage = null; if ( msg !== 'areyouready?' ) { break; }
bcSecret.postMessage('iamready!');
break;
case 'object':
if ( self.vAPI && self.vAPI.messaging ) {
self.vAPI.messaging.send('contentscript', msg);
} else {
bcSecret.onmessage = null;
}
break;
} }
}; };
bcSecret.postMessage('iamready!');
vAPI.bcSecret = bcSecret; vAPI.bcSecret = bcSecret;
}.toString(), }.toString(),
')(', ')(',

View File

@ -26,7 +26,7 @@
(( ) => { (( ) => {
if ( typeof vAPI !== 'object' || vAPI === null ) { return; } if ( typeof vAPI !== 'object' || vAPI === null ) { return; }
if ( vAPI.bcSecret instanceof self.BroadcastChannel === false ) { return; } if ( vAPI.bcSecret instanceof self.BroadcastChannel === false ) { return; }
vAPI.bcSecret.postMessage({ what: 'setScriptletLogLevel', level: 1 }); vAPI.bcSecret.postMessage('setScriptletLogLevelOne');
})(); })();

View File

@ -26,7 +26,7 @@
(( ) => { (( ) => {
if ( typeof vAPI !== 'object' || vAPI === null ) { return; } if ( typeof vAPI !== 'object' || vAPI === null ) { return; }
if ( vAPI.bcSecret instanceof self.BroadcastChannel === false ) { return; } if ( vAPI.bcSecret instanceof self.BroadcastChannel === false ) { return; }
vAPI.bcSecret.postMessage({ what: 'setScriptletLogLevel', level: 2 }); vAPI.bcSecret.postMessage('setScriptletLogLevelToTwo');
})(); })();