From 6aeab2adbc95e1e892867ad719b02cf1d1dc9a61 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sat, 25 Nov 2023 11:13:57 -0500 Subject: [PATCH] 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. --- assets/resources/scriptlets.js | 129 ++++++++++++++++++++++----------- 1 file changed, 85 insertions(+), 44 deletions(-) diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 5d671606a..45d8fdae3 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -52,6 +52,8 @@ function safeSelf() { 'Function_toStringFn': self.Function.prototype.toString, 'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg), 'Math_floor': Math.floor, + 'Math_max': Math.max, + 'Math_min': Math.min, 'Math_random': Math.random, 'Object_defineProperty': Object.defineProperty.bind(Object), '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({ name: 'abort-current-script-core.fn', fn: abortCurrentScriptCore, @@ -1757,16 +1817,18 @@ builtinScriptlets.push({ ], fn: noFetchIf, dependencies: [ + 'generate-content.fn', 'safe-self.fn', ], }); function noFetchIf( - arg1 = '', + propsToMatch = '', + directive = '' ) { - if ( typeof arg1 !== 'string' ) { return; } + if ( typeof propsToMatch !== 'string' ) { return; } const safe = safeSelf(); const needles = []; - for ( const condition of arg1.split(/\s+/) ) { + for ( const condition of propsToMatch.split(/\s+/) ) { if ( condition === '' ) { continue; } const pos = condition.indexOf(':'); let key, value; @@ -1782,14 +1844,11 @@ function noFetchIf( const log = needles.length === 0 ? console.log.bind(console) : undefined; self.fetch = new Proxy(self.fetch, { apply: function(target, thisArg, args) { + const details = args[0] instanceof self.Request + ? args[0] + : Object.assign({ url: args[0] }, args[1]); let proceed = true; 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(); for ( const prop in details ) { let v = details[prop]; @@ -1818,9 +1877,21 @@ function noFetchIf( } } catch(ex) { } - return proceed - ? Reflect.apply(target, thisArg, args) - : Promise.resolve(new Response()); + if ( proceed ) { + return Reflect.apply(target, thisArg, args); + } + 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, dependencies: [ + 'generate-content.fn', 'match-object-properties.fn', 'parse-properties-to-match.fn', 'safe-self.fn', @@ -2269,41 +2341,10 @@ function noXhrIf( directive = '' ) { if ( typeof propsToMatch !== 'string' ) { return; } - const safe = safeSelf(); const xhrInstances = new WeakMap(); const propNeedles = parsePropertiesToMatch(propsToMatch, 'url'); const log = propNeedles.size === 0 ? console.log.bind(console) : undefined; 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 { open(method, url, ...args) { if ( log !== undefined ) { @@ -2370,7 +2411,7 @@ function noXhrIf( default: if ( directive === '' ) { break; } promise = promise.then(details => { - return generateContent(details.directive).then(text => { + return generateContentFn(details.directive).then(text => { details.props.response.value = text; details.props.responseText.value = text; return details;