Improve `prevent-fetch` scriptlet

Related issue:
https://github.com/uBlockOrigin/uBlock-issues/issues/2526

Improvements:

Support fulfilling the response with the content of a
`web_accessible_resources` resource, using the syntax already
supported by `prevent-xhr`: `war:[name of resource]`

Support fulfilling the response with randomized text with length
specified using `length:min[-max]` directive.
This commit is contained in:
Raymond Hill 2023-11-25 11:13:57 -05:00
parent 74f54d0633
commit 6aeab2adbc
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
1 changed files with 85 additions and 44 deletions

View File

@ -52,6 +52,8 @@ function safeSelf() {
'Function_toStringFn': self.Function.prototype.toString, 'Function_toStringFn': self.Function.prototype.toString,
'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg), 'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg),
'Math_floor': Math.floor, 'Math_floor': Math.floor,
'Math_max': Math.max,
'Math_min': Math.min,
'Math_random': Math.random, 'Math_random': Math.random,
'Object_defineProperty': Object.defineProperty.bind(Object), 'Object_defineProperty': Object.defineProperty.bind(Object),
'RegExp': self.RegExp, 'RegExp': self.RegExp,
@ -242,6 +244,64 @@ function runAtHtmlElementFn(fn) {
/******************************************************************************/ /******************************************************************************/
// Reference:
// https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr
builtinScriptlets.push({
name: 'generate-content.fn',
fn: generateContentFn,
dependencies: [
'safe-self.fn',
],
});
function generateContentFn(directive) {
const safe = safeSelf();
const randomize = len => {
const chunks = [];
let textSize = 0;
do {
const s = safe.Math_random().toString(36).slice(2);
chunks.push(s);
textSize += s.length;
}
while ( textSize < len );
return chunks.join(' ').slice(0, len);
};
if ( directive === 'true' ) {
return Promise.resolve(randomize(10));
}
if ( directive.startsWith('length:') ) {
const match = /^length:(\d+)(?:-(\d+))?$/.exec(directive);
if ( match ) {
const min = parseInt(match[1], 10);
const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min;
const len = safe.Math_min(min + extent * safe.Math_random(), 500000);
return Promise.resolve(randomize(len | 0));
}
}
if ( directive.startsWith('war:') && scriptletGlobals.has('warOrigin') ) {
return new Promise(resolve => {
const warOrigin = scriptletGlobals.get('warOrigin');
const warName = directive.slice(4);
const fullpath = [ warOrigin, '/', warName ];
const warSecret = scriptletGlobals.get('warSecret');
if ( warSecret !== undefined ) {
fullpath.push('?secret=', warSecret);
}
const warXHR = new safe.XMLHttpRequest();
warXHR.responseType = 'text';
warXHR.onloadend = ev => {
resolve(ev.target.responseText || '');
};
warXHR.open('GET', fullpath.join(''));
warXHR.send();
});
}
return Promise.resolve('');
}
/******************************************************************************/
builtinScriptlets.push({ builtinScriptlets.push({
name: 'abort-current-script-core.fn', name: 'abort-current-script-core.fn',
fn: abortCurrentScriptCore, fn: abortCurrentScriptCore,
@ -1757,16 +1817,18 @@ builtinScriptlets.push({
], ],
fn: noFetchIf, fn: noFetchIf,
dependencies: [ dependencies: [
'generate-content.fn',
'safe-self.fn', 'safe-self.fn',
], ],
}); });
function noFetchIf( function noFetchIf(
arg1 = '', propsToMatch = '',
directive = ''
) { ) {
if ( typeof arg1 !== 'string' ) { return; } if ( typeof propsToMatch !== 'string' ) { return; }
const safe = safeSelf(); const safe = safeSelf();
const needles = []; const needles = [];
for ( const condition of arg1.split(/\s+/) ) { for ( const condition of propsToMatch.split(/\s+/) ) {
if ( condition === '' ) { continue; } if ( condition === '' ) { continue; }
const pos = condition.indexOf(':'); const pos = condition.indexOf(':');
let key, value; let key, value;
@ -1782,14 +1844,11 @@ function noFetchIf(
const log = needles.length === 0 ? console.log.bind(console) : undefined; const log = needles.length === 0 ? console.log.bind(console) : undefined;
self.fetch = new Proxy(self.fetch, { self.fetch = new Proxy(self.fetch, {
apply: function(target, thisArg, args) { apply: function(target, thisArg, args) {
const details = args[0] instanceof self.Request
? args[0]
: Object.assign({ url: args[0] }, args[1]);
let proceed = true; let proceed = true;
try { try {
let details;
if ( args[0] instanceof self.Request ) {
details = args[0];
} else {
details = Object.assign({ url: args[0] }, args[1]);
}
const props = new Map(); const props = new Map();
for ( const prop in details ) { for ( const prop in details ) {
let v = details[prop]; let v = details[prop];
@ -1818,9 +1877,21 @@ function noFetchIf(
} }
} catch(ex) { } catch(ex) {
} }
return proceed if ( proceed ) {
? Reflect.apply(target, thisArg, args) return Reflect.apply(target, thisArg, args);
: Promise.resolve(new Response()); }
return generateContentFn(directive).then(text => {
const response = new Response(text, {
statusText: 'OK',
headers: {
'Content-Length': text.length,
}
});
Object.defineProperty(response, 'url', {
value: details.url
});
return response;
});
} }
}); });
} }
@ -2259,6 +2330,7 @@ builtinScriptlets.push({
], ],
fn: noXhrIf, fn: noXhrIf,
dependencies: [ dependencies: [
'generate-content.fn',
'match-object-properties.fn', 'match-object-properties.fn',
'parse-properties-to-match.fn', 'parse-properties-to-match.fn',
'safe-self.fn', 'safe-self.fn',
@ -2269,41 +2341,10 @@ function noXhrIf(
directive = '' directive = ''
) { ) {
if ( typeof propsToMatch !== 'string' ) { return; } if ( typeof propsToMatch !== 'string' ) { return; }
const safe = safeSelf();
const xhrInstances = new WeakMap(); const xhrInstances = new WeakMap();
const propNeedles = parsePropertiesToMatch(propsToMatch, 'url'); const propNeedles = parsePropertiesToMatch(propsToMatch, 'url');
const log = propNeedles.size === 0 ? console.log.bind(console) : undefined; const log = propNeedles.size === 0 ? console.log.bind(console) : undefined;
const warOrigin = scriptletGlobals.get('warOrigin'); const warOrigin = scriptletGlobals.get('warOrigin');
const generateRandomString = len => {
let s = '';
do { s += safe.Math_random().toString(36).slice(2); }
while ( s.length < 10 );
return s.slice(0, len);
};
const generateContent = async directive => {
if ( directive === 'true' ) {
return generateRandomString(10);
}
if ( directive.startsWith('war:') ) {
if ( warOrigin === undefined ) { return ''; }
return new Promise(resolve => {
const warName = directive.slice(4);
const fullpath = [ warOrigin, '/', warName ];
const warSecret = scriptletGlobals.get('warSecret');
if ( warSecret !== undefined ) {
fullpath.push('?secret=', warSecret);
}
const warXHR = new safe.XMLHttpRequest();
warXHR.responseType = 'text';
warXHR.onloadend = ev => {
resolve(ev.target.responseText || '');
};
warXHR.open('GET', fullpath.join(''));
warXHR.send();
});
}
return '';
};
self.XMLHttpRequest = class extends self.XMLHttpRequest { self.XMLHttpRequest = class extends self.XMLHttpRequest {
open(method, url, ...args) { open(method, url, ...args) {
if ( log !== undefined ) { if ( log !== undefined ) {
@ -2370,7 +2411,7 @@ function noXhrIf(
default: default:
if ( directive === '' ) { break; } if ( directive === '' ) { break; }
promise = promise.then(details => { promise = promise.then(details => {
return generateContent(details.directive).then(text => { return generateContentFn(details.directive).then(text => {
details.props.response.value = text; details.props.response.value = text;
details.props.responseText.value = text; details.props.responseText.value = text;
return details; return details;