Skip to content

Commit bf591d9

Browse files
committed
Imrpove no-xhr-if scriptlet
Related issue: uBlockOrigin/uBlock-issues#2773 The `randomize` paramater introduced in 418087de9c is now named `directive`, and beside the `true` value which is meant to respond with a random 10-character string, it can now take the following value: war:[web_accessible_resource name] In order to mock the XHR response with a web accessible resource. For example: piquark6046.github.io##+js(no-xhr-if, adsbygoogle.js, war:googlesyndication_adsbygoogle.js) Will cause the XHR performed by the webpage to resolve to the content of `/web_accessible_resources/googlesyndication_adsbygoogle.js`. Should the resource not exist, the empty string will be returned.
1 parent c92cdd5 commit bf591d9

File tree

7 files changed

+150
-86
lines changed

7 files changed

+150
-86
lines changed

assets/resources/scriptlets.js

Lines changed: 85 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1935,21 +1935,55 @@ builtinScriptlets.push({
19351935
});
19361936
function noXhrIf(
19371937
propsToMatch = '',
1938-
randomize = ''
1938+
directive = ''
19391939
) {
19401940
if ( typeof propsToMatch !== 'string' ) { return; }
19411941
const xhrInstances = new WeakMap();
19421942
const propNeedles = parsePropertiesToMatch(propsToMatch, 'url');
19431943
const log = propNeedles.size === 0 ? console.log.bind(console) : undefined;
1944+
const warOrigin = scriptletGlobals.get('warOrigin');
1945+
const generateRandomString = len => {
1946+
let s = '';
1947+
do { s += Math.random().toString(36).slice(2); }
1948+
while ( s.length < 10 );
1949+
return s.slice(0, len);
1950+
};
1951+
const generateContent = async directive => {
1952+
if ( directive === 'true' ) {
1953+
return generateRandomString(10);
1954+
}
1955+
if ( directive.startsWith('war:') ) {
1956+
if ( warOrigin === undefined ) { return ''; }
1957+
const warName = directive.slice(4);
1958+
const fullpath = [ warOrigin, '/', warName ];
1959+
const warSecret = scriptletGlobals.get('warSecret') || '';
1960+
if ( warSecret !== '' ) {
1961+
fullpath.push('?secret=', warSecret);
1962+
}
1963+
return new Promise(resolve => {
1964+
const warXHR = new XMLHttpRequest();
1965+
warXHR.responseType = 'text';
1966+
warXHR.onloadend = ev => {
1967+
resolve(ev.target.responseText || '');
1968+
};
1969+
warXHR.open('GET', fullpath.join(''));
1970+
warXHR.send();
1971+
});
1972+
}
1973+
return '';
1974+
};
19441975
self.XMLHttpRequest = class extends self.XMLHttpRequest {
19451976
open(method, url, ...args) {
19461977
if ( log !== undefined ) {
19471978
log(`uBO: xhr.open(${method}, ${url}, ${args.join(', ')})`);
1948-
} else {
1949-
const haystack = { method, url };
1950-
if ( matchObjectProperties(propNeedles, haystack) ) {
1951-
xhrInstances.set(this, haystack);
1952-
}
1979+
return super.open(method, url, ...args);
1980+
}
1981+
if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
1982+
return super.open(method, url, ...args);
1983+
}
1984+
const haystack = { method, url };
1985+
if ( matchObjectProperties(propNeedles, haystack) ) {
1986+
xhrInstances.set(this, haystack);
19531987
}
19541988
return super.open(method, url, ...args);
19551989
}
@@ -1958,50 +1992,66 @@ function noXhrIf(
19581992
if ( haystack === undefined ) {
19591993
return super.send(...args);
19601994
}
1961-
Object.defineProperties(this, {
1962-
readyState: { value: 4 },
1963-
responseURL: { value: haystack.url },
1964-
status: { value: 200 },
1965-
statusText: { value: 'OK' },
1995+
let promise = Promise.resolve({
1996+
xhr: this,
1997+
directive,
1998+
props: {
1999+
readyState: { value: 4 },
2000+
response: { value: '' },
2001+
responseText: { value: '' },
2002+
responseXML: { value: null },
2003+
responseURL: { value: haystack.url },
2004+
status: { value: 200 },
2005+
statusText: { value: 'OK' },
2006+
},
19662007
});
1967-
let response = '';
1968-
let responseText = '';
1969-
let responseXML = null;
19702008
switch ( this.responseType ) {
19712009
case 'arraybuffer':
1972-
response = new ArrayBuffer(0);
2010+
promise = promise.then(details => {
2011+
details.props.response.value = new ArrayBuffer(0);
2012+
return details;
2013+
});
19732014
break;
19742015
case 'blob':
1975-
response = new Blob([]);
2016+
promise = promise.then(details => {
2017+
details.props.response.value = new Blob([]);
2018+
return details;
2019+
});
19762020
break;
19772021
case 'document': {
1978-
const parser = new DOMParser();
1979-
const doc = parser.parseFromString('', 'text/html');
1980-
response = doc;
1981-
responseXML = doc;
2022+
promise = promise.then(details => {
2023+
const parser = new DOMParser();
2024+
const doc = parser.parseFromString('', 'text/html');
2025+
details.props.response.value = doc;
2026+
details.props.responseXML.value = doc;
2027+
return details;
2028+
});
19822029
break;
19832030
}
19842031
case 'json':
1985-
response = {};
1986-
responseText = '{}';
2032+
promise = promise.then(details => {
2033+
details.props.response.value = {};
2034+
details.props.responseText.value = '{}';
2035+
return details;
2036+
});
19872037
break;
19882038
default:
1989-
if ( randomize !== 'true' ) { break; }
1990-
do {
1991-
response += Math.random().toString(36).slice(-2);
1992-
} while ( response.length < 10 );
1993-
response = response.slice(-10);
1994-
responseText = response;
2039+
if ( directive === '' ) { break; }
2040+
promise = promise.then(details => {
2041+
return generateContent(details.directive).then(text => {
2042+
details.props.response.value = text;
2043+
details.props.responseText.value = text;
2044+
return details;
2045+
});
2046+
});
19952047
break;
19962048
}
1997-
Object.defineProperties(this, {
1998-
response: { value: response },
1999-
responseText: { value: responseText },
2000-
responseXML: { value: responseXML },
2049+
promise.then(details => {
2050+
Object.defineProperties(details.xhr, details.props);
2051+
details.xhr.dispatchEvent(new Event('readystatechange'));
2052+
details.xhr.dispatchEvent(new Event('load'));
2053+
details.xhr.dispatchEvent(new Event('loadend'));
20012054
});
2002-
this.dispatchEvent(new Event('readystatechange'));
2003-
this.dispatchEvent(new Event('load'));
2004-
this.dispatchEvent(new Event('loadend'));
20052055
}
20062056
};
20072057
}

platform/common/vapi-background.js

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,24 +1167,32 @@ vAPI.messaging = {
11671167
// https://github.com/uBlockOrigin/uBlock-issues/issues/550
11681168
// Support using a new secret for every network request.
11691169

1170-
vAPI.warSecret = (( ) => {
1171-
const generateSecret = ( ) => {
1172-
return Math.floor(Math.random() * 982451653 + 982451653).toString(36);
1173-
};
1170+
{
1171+
// Generate a 6-character alphanumeric string, thus one random value out
1172+
// of 36^6 = over 2x10^9 values.
1173+
const generateSecret = ( ) =>
1174+
(Math.floor(Math.random() * 2176782336) + 2176782336).toString(36).slice(1);
11741175

11751176
const root = vAPI.getURL('/');
1176-
const secrets = [];
1177-
let lastSecretTime = 0;
1178-
1179-
const guard = function(details) {
1180-
const url = details.url;
1181-
const pos = secrets.findIndex(secret =>
1182-
url.lastIndexOf(`?secret=${secret}`) !== -1
1183-
);
1177+
const reSecret = /\?secret=(\w+)/;
1178+
const shortSecrets = [];
1179+
let lastShortSecretTime = 0;
1180+
1181+
// Long secrets are meant to be used multiple times, but for at most a few
1182+
// minutes. The realm is one value out of 36^18 = over 10^28 values.
1183+
const longSecrets = [ '', '' ];
1184+
let lastLongSecretTimeSlice = 0;
1185+
1186+
const guard = details => {
1187+
const match = reSecret.exec(details.url);
1188+
if ( match === null ) { return; }
1189+
const secret = match[1];
1190+
if ( longSecrets.includes(secret) ) { return; }
1191+
const pos = shortSecrets.indexOf(secret);
11841192
if ( pos === -1 ) {
11851193
return { cancel: true };
11861194
}
1187-
secrets.splice(pos, 1);
1195+
shortSecrets.splice(pos, 1);
11881196
};
11891197

11901198
browser.webRequest.onBeforeRequest.addListener(
@@ -1195,20 +1203,31 @@ vAPI.warSecret = (( ) => {
11951203
[ 'blocking' ]
11961204
);
11971205

1198-
return ( ) => {
1199-
if ( secrets.length !== 0 ) {
1200-
if ( (Date.now() - lastSecretTime) > 5000 ) {
1201-
secrets.splice(0);
1202-
} else if ( secrets.length > 256 ) {
1203-
secrets.splice(0, secrets.length - 192);
1206+
vAPI.warSecret = {
1207+
short: ( ) => {
1208+
if ( shortSecrets.length !== 0 ) {
1209+
if ( (Date.now() - lastShortSecretTime) > 5000 ) {
1210+
shortSecrets.splice(0);
1211+
} else if ( shortSecrets.length > 256 ) {
1212+
shortSecrets.splice(0, shortSecrets.length - 192);
1213+
}
12041214
}
1205-
}
1206-
lastSecretTime = Date.now();
1207-
const secret = generateSecret();
1208-
secrets.push(secret);
1209-
return secret;
1215+
lastShortSecretTime = Date.now();
1216+
const secret = generateSecret();
1217+
shortSecrets.push(secret);
1218+
return secret;
1219+
},
1220+
long: ( ) => {
1221+
const timeSlice = Date.now() >>> 19; // Changes every ~9 minutes
1222+
if ( timeSlice !== lastLongSecretTimeSlice ) {
1223+
longSecrets[1] = longSecrets[0];
1224+
longSecrets[0] = `${generateSecret()}${generateSecret()}${generateSecret()}`;
1225+
lastLongSecretTimeSlice = timeSlice;
1226+
}
1227+
return longSecrets[0];
1228+
},
12101229
};
1211-
})();
1230+
}
12121231

12131232
/******************************************************************************/
12141233

src/js/messaging.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,7 @@ const onMessage = function(request, sender, callback) {
984984
zap: µb.epickerArgs.zap,
985985
eprom: µb.epickerArgs.eprom,
986986
pickerURL: vAPI.getURL(
987-
`/web_accessible_resources/epicker-ui.html?secret=${vAPI.warSecret()}`
987+
`/web_accessible_resources/epicker-ui.html?secret=${vAPI.warSecret.short()}`
988988
),
989989
});
990990
µb.epickerArgs.target = '';

src/js/pagestore.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ const NetFilteringResultCache = class {
152152
entry.redirectURL.startsWith(this.extensionOriginURL)
153153
) {
154154
const redirectURL = new URL(entry.redirectURL);
155-
redirectURL.searchParams.set('secret', vAPI.warSecret());
155+
redirectURL.searchParams.set('secret', vAPI.warSecret.short());
156156
entry.redirectURL = redirectURL.href;
157157
}
158158
return entry;

src/js/redirect-engine.js

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,12 @@ const removeTopCommentBlock = text => {
6666
return text.replace(/^\/\*[\S\s]+?\n\*\/\s*/, '');
6767
};
6868

69-
// vAPI.warSecret() is optional, it could be absent in some environments,
69+
// vAPI.warSecret is optional, it could be absent in some environments,
7070
// i.e. nodejs for example. Probably the best approach is to have the
7171
// "web_accessible_resources secret" added outside by the client of this
7272
// module, but for now I just want to remove an obstacle to modularization.
7373
const warSecret = typeof vAPI === 'object' && vAPI !== null
74-
? vAPI.warSecret
74+
? vAPI.warSecret.short
7575
: ( ) => '';
7676

7777
const RESOURCES_SELFIE_VERSION = 7;
@@ -153,15 +153,7 @@ class RedirectEntry {
153153

154154
static fromDetails(details) {
155155
const r = new RedirectEntry();
156-
r.mime = details.mime;
157-
r.data = details.data;
158-
r.requiresTrust = details.requiresTrust === true;
159-
r.warURL = details.warURL !== undefined && details.warURL || undefined;
160-
r.params = details.params !== undefined && details.params || undefined;
161-
r.world = details.world || 'MAIN';
162-
if ( Array.isArray(details.dependencies) ) {
163-
r.dependencies.push(...details.dependencies);
164-
}
156+
Object.assign(r, details);
165157
return r;
166158
}
167159
}
@@ -331,17 +323,17 @@ class RedirectEngine {
331323
const fetches = [
332324
import('/assets/resources/scriptlets.js').then(module => {
333325
for ( const scriptlet of module.builtinScriptlets ) {
334-
const { name, aliases, fn } = scriptlet;
335-
const entry = RedirectEntry.fromDetails({
336-
mime: mimeFromName(name),
337-
data: fn.toString(),
338-
dependencies: scriptlet.dependencies,
339-
requiresTrust: scriptlet.requiresTrust === true,
340-
world: scriptlet.world || 'MAIN',
341-
});
342-
this.resources.set(name, entry);
343-
if ( Array.isArray(aliases) === false ) { continue; }
344-
for ( const alias of aliases ) {
326+
const details = {};
327+
details.mime = mimeFromName(scriptlet.name);
328+
details.data = scriptlet.fn.toString();
329+
for ( const [ k, v ] of Object.entries(scriptlet) ) {
330+
if ( k === 'fn' ) { continue; }
331+
details[k] = v;
332+
}
333+
const entry = RedirectEntry.fromDetails(details);
334+
this.resources.set(details.name, entry);
335+
if ( Array.isArray(details.aliases) === false ) { continue; }
336+
for ( const alias of details.aliases ) {
345337
this.aliases.set(alias, name);
346338
}
347339
}

src/js/scriptlet-filtering.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,10 @@ scriptletFilteringEngine.retrieve = function(request) {
383383
return { filters: cacheDetails.filters };
384384
}
385385

386-
const scriptletGlobals = [];
386+
const scriptletGlobals = [
387+
[ 'warOrigin', vAPI.getURL('/web_accessible_resources') ],
388+
[ 'warSecret', vAPI.warSecret.long() ],
389+
];
387390

388391
if ( isDevBuild === undefined ) {
389392
isDevBuild = vAPI.webextFlavor.soup.has('devbuild');

src/js/storage.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => {
11351135

11361136
const fetcher = (path, options = undefined) => {
11371137
if ( path.startsWith('/web_accessible_resources/') ) {
1138-
path += `?secret=${vAPI.warSecret()}`;
1138+
path += `?secret=${vAPI.warSecret.short()}`;
11391139
return io.fetch(path, options);
11401140
}
11411141
return io.fetchText(path);

0 commit comments

Comments
 (0)