Skip to content

Commit 6aeab2a

Browse files
committed
Improve prevent-fetch scriptlet
Related issue: uBlockOrigin/uBlock-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.
1 parent 74f54d0 commit 6aeab2a

File tree

1 file changed

+85
-44
lines changed

1 file changed

+85
-44
lines changed

assets/resources/scriptlets.js

Lines changed: 85 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ function safeSelf() {
5252
'Function_toStringFn': self.Function.prototype.toString,
5353
'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg),
5454
'Math_floor': Math.floor,
55+
'Math_max': Math.max,
56+
'Math_min': Math.min,
5557
'Math_random': Math.random,
5658
'Object_defineProperty': Object.defineProperty.bind(Object),
5759
'RegExp': self.RegExp,
@@ -242,6 +244,64 @@ function runAtHtmlElementFn(fn) {
242244

243245
/******************************************************************************/
244246

247+
// Reference:
248+
// https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr
249+
250+
builtinScriptlets.push({
251+
name: 'generate-content.fn',
252+
fn: generateContentFn,
253+
dependencies: [
254+
'safe-self.fn',
255+
],
256+
});
257+
function generateContentFn(directive) {
258+
const safe = safeSelf();
259+
const randomize = len => {
260+
const chunks = [];
261+
let textSize = 0;
262+
do {
263+
const s = safe.Math_random().toString(36).slice(2);
264+
chunks.push(s);
265+
textSize += s.length;
266+
}
267+
while ( textSize < len );
268+
return chunks.join(' ').slice(0, len);
269+
};
270+
if ( directive === 'true' ) {
271+
return Promise.resolve(randomize(10));
272+
}
273+
if ( directive.startsWith('length:') ) {
274+
const match = /^length:(\d+)(?:-(\d+))?$/.exec(directive);
275+
if ( match ) {
276+
const min = parseInt(match[1], 10);
277+
const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min;
278+
const len = safe.Math_min(min + extent * safe.Math_random(), 500000);
279+
return Promise.resolve(randomize(len | 0));
280+
}
281+
}
282+
if ( directive.startsWith('war:') && scriptletGlobals.has('warOrigin') ) {
283+
return new Promise(resolve => {
284+
const warOrigin = scriptletGlobals.get('warOrigin');
285+
const warName = directive.slice(4);
286+
const fullpath = [ warOrigin, '/', warName ];
287+
const warSecret = scriptletGlobals.get('warSecret');
288+
if ( warSecret !== undefined ) {
289+
fullpath.push('?secret=', warSecret);
290+
}
291+
const warXHR = new safe.XMLHttpRequest();
292+
warXHR.responseType = 'text';
293+
warXHR.onloadend = ev => {
294+
resolve(ev.target.responseText || '');
295+
};
296+
warXHR.open('GET', fullpath.join(''));
297+
warXHR.send();
298+
});
299+
}
300+
return Promise.resolve('');
301+
}
302+
303+
/******************************************************************************/
304+
245305
builtinScriptlets.push({
246306
name: 'abort-current-script-core.fn',
247307
fn: abortCurrentScriptCore,
@@ -1757,16 +1817,18 @@ builtinScriptlets.push({
17571817
],
17581818
fn: noFetchIf,
17591819
dependencies: [
1820+
'generate-content.fn',
17601821
'safe-self.fn',
17611822
],
17621823
});
17631824
function noFetchIf(
1764-
arg1 = '',
1825+
propsToMatch = '',
1826+
directive = ''
17651827
) {
1766-
if ( typeof arg1 !== 'string' ) { return; }
1828+
if ( typeof propsToMatch !== 'string' ) { return; }
17671829
const safe = safeSelf();
17681830
const needles = [];
1769-
for ( const condition of arg1.split(/\s+/) ) {
1831+
for ( const condition of propsToMatch.split(/\s+/) ) {
17701832
if ( condition === '' ) { continue; }
17711833
const pos = condition.indexOf(':');
17721834
let key, value;
@@ -1782,14 +1844,11 @@ function noFetchIf(
17821844
const log = needles.length === 0 ? console.log.bind(console) : undefined;
17831845
self.fetch = new Proxy(self.fetch, {
17841846
apply: function(target, thisArg, args) {
1847+
const details = args[0] instanceof self.Request
1848+
? args[0]
1849+
: Object.assign({ url: args[0] }, args[1]);
17851850
let proceed = true;
17861851
try {
1787-
let details;
1788-
if ( args[0] instanceof self.Request ) {
1789-
details = args[0];
1790-
} else {
1791-
details = Object.assign({ url: args[0] }, args[1]);
1792-
}
17931852
const props = new Map();
17941853
for ( const prop in details ) {
17951854
let v = details[prop];
@@ -1818,9 +1877,21 @@ function noFetchIf(
18181877
}
18191878
} catch(ex) {
18201879
}
1821-
return proceed
1822-
? Reflect.apply(target, thisArg, args)
1823-
: Promise.resolve(new Response());
1880+
if ( proceed ) {
1881+
return Reflect.apply(target, thisArg, args);
1882+
}
1883+
return generateContentFn(directive).then(text => {
1884+
const response = new Response(text, {
1885+
statusText: 'OK',
1886+
headers: {
1887+
'Content-Length': text.length,
1888+
}
1889+
});
1890+
Object.defineProperty(response, 'url', {
1891+
value: details.url
1892+
});
1893+
return response;
1894+
});
18241895
}
18251896
});
18261897
}
@@ -2259,6 +2330,7 @@ builtinScriptlets.push({
22592330
],
22602331
fn: noXhrIf,
22612332
dependencies: [
2333+
'generate-content.fn',
22622334
'match-object-properties.fn',
22632335
'parse-properties-to-match.fn',
22642336
'safe-self.fn',
@@ -2269,41 +2341,10 @@ function noXhrIf(
22692341
directive = ''
22702342
) {
22712343
if ( typeof propsToMatch !== 'string' ) { return; }
2272-
const safe = safeSelf();
22732344
const xhrInstances = new WeakMap();
22742345
const propNeedles = parsePropertiesToMatch(propsToMatch, 'url');
22752346
const log = propNeedles.size === 0 ? console.log.bind(console) : undefined;
22762347
const warOrigin = scriptletGlobals.get('warOrigin');
2277-
const generateRandomString = len => {
2278-
let s = '';
2279-
do { s += safe.Math_random().toString(36).slice(2); }
2280-
while ( s.length < 10 );
2281-
return s.slice(0, len);
2282-
};
2283-
const generateContent = async directive => {
2284-
if ( directive === 'true' ) {
2285-
return generateRandomString(10);
2286-
}
2287-
if ( directive.startsWith('war:') ) {
2288-
if ( warOrigin === undefined ) { return ''; }
2289-
return new Promise(resolve => {
2290-
const warName = directive.slice(4);
2291-
const fullpath = [ warOrigin, '/', warName ];
2292-
const warSecret = scriptletGlobals.get('warSecret');
2293-
if ( warSecret !== undefined ) {
2294-
fullpath.push('?secret=', warSecret);
2295-
}
2296-
const warXHR = new safe.XMLHttpRequest();
2297-
warXHR.responseType = 'text';
2298-
warXHR.onloadend = ev => {
2299-
resolve(ev.target.responseText || '');
2300-
};
2301-
warXHR.open('GET', fullpath.join(''));
2302-
warXHR.send();
2303-
});
2304-
}
2305-
return '';
2306-
};
23072348
self.XMLHttpRequest = class extends self.XMLHttpRequest {
23082349
open(method, url, ...args) {
23092350
if ( log !== undefined ) {
@@ -2370,7 +2411,7 @@ function noXhrIf(
23702411
default:
23712412
if ( directive === '' ) { break; }
23722413
promise = promise.then(details => {
2373-
return generateContent(details.directive).then(text => {
2414+
return generateContentFn(details.directive).then(text => {
23742415
details.props.response.value = text;
23752416
details.props.responseText.value = text;
23762417
return details;

0 commit comments

Comments
 (0)