mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-13 02:14:17 +01:00
Imrpove no-xhr-if
scriptlet
Related issue: https://github.com/uBlockOrigin/uBlock-issues/issues/2773 The `randomize` paramater introduced in https://github.com/gorhill/uBlock/commit/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.
This commit is contained in:
parent
c92cdd5818
commit
bf591d93fb
7 changed files with 149 additions and 85 deletions
|
@ -1935,21 +1935,55 @@ builtinScriptlets.push({
|
||||||
});
|
});
|
||||||
function noXhrIf(
|
function noXhrIf(
|
||||||
propsToMatch = '',
|
propsToMatch = '',
|
||||||
randomize = ''
|
directive = ''
|
||||||
) {
|
) {
|
||||||
if ( typeof propsToMatch !== 'string' ) { return; }
|
if ( typeof propsToMatch !== 'string' ) { return; }
|
||||||
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 generateRandomString = len => {
|
||||||
|
let s = '';
|
||||||
|
do { s += 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 ''; }
|
||||||
|
const warName = directive.slice(4);
|
||||||
|
const fullpath = [ warOrigin, '/', warName ];
|
||||||
|
const warSecret = scriptletGlobals.get('warSecret') || '';
|
||||||
|
if ( warSecret !== '' ) {
|
||||||
|
fullpath.push('?secret=', warSecret);
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const warXHR = new 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 ) {
|
||||||
log(`uBO: xhr.open(${method}, ${url}, ${args.join(', ')})`);
|
log(`uBO: xhr.open(${method}, ${url}, ${args.join(', ')})`);
|
||||||
} else {
|
return super.open(method, url, ...args);
|
||||||
const haystack = { method, url };
|
}
|
||||||
if ( matchObjectProperties(propNeedles, haystack) ) {
|
if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
|
||||||
xhrInstances.set(this, haystack);
|
return super.open(method, url, ...args);
|
||||||
}
|
}
|
||||||
|
const haystack = { method, url };
|
||||||
|
if ( matchObjectProperties(propNeedles, haystack) ) {
|
||||||
|
xhrInstances.set(this, haystack);
|
||||||
}
|
}
|
||||||
return super.open(method, url, ...args);
|
return super.open(method, url, ...args);
|
||||||
}
|
}
|
||||||
|
@ -1958,50 +1992,66 @@ function noXhrIf(
|
||||||
if ( haystack === undefined ) {
|
if ( haystack === undefined ) {
|
||||||
return super.send(...args);
|
return super.send(...args);
|
||||||
}
|
}
|
||||||
Object.defineProperties(this, {
|
let promise = Promise.resolve({
|
||||||
readyState: { value: 4 },
|
xhr: this,
|
||||||
responseURL: { value: haystack.url },
|
directive,
|
||||||
status: { value: 200 },
|
props: {
|
||||||
statusText: { value: 'OK' },
|
readyState: { value: 4 },
|
||||||
|
response: { value: '' },
|
||||||
|
responseText: { value: '' },
|
||||||
|
responseXML: { value: null },
|
||||||
|
responseURL: { value: haystack.url },
|
||||||
|
status: { value: 200 },
|
||||||
|
statusText: { value: 'OK' },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let response = '';
|
|
||||||
let responseText = '';
|
|
||||||
let responseXML = null;
|
|
||||||
switch ( this.responseType ) {
|
switch ( this.responseType ) {
|
||||||
case 'arraybuffer':
|
case 'arraybuffer':
|
||||||
response = new ArrayBuffer(0);
|
promise = promise.then(details => {
|
||||||
|
details.props.response.value = new ArrayBuffer(0);
|
||||||
|
return details;
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'blob':
|
case 'blob':
|
||||||
response = new Blob([]);
|
promise = promise.then(details => {
|
||||||
|
details.props.response.value = new Blob([]);
|
||||||
|
return details;
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'document': {
|
case 'document': {
|
||||||
const parser = new DOMParser();
|
promise = promise.then(details => {
|
||||||
const doc = parser.parseFromString('', 'text/html');
|
const parser = new DOMParser();
|
||||||
response = doc;
|
const doc = parser.parseFromString('', 'text/html');
|
||||||
responseXML = doc;
|
details.props.response.value = doc;
|
||||||
|
details.props.responseXML.value = doc;
|
||||||
|
return details;
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'json':
|
case 'json':
|
||||||
response = {};
|
promise = promise.then(details => {
|
||||||
responseText = '{}';
|
details.props.response.value = {};
|
||||||
|
details.props.responseText.value = '{}';
|
||||||
|
return details;
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if ( randomize !== 'true' ) { break; }
|
if ( directive === '' ) { break; }
|
||||||
do {
|
promise = promise.then(details => {
|
||||||
response += Math.random().toString(36).slice(-2);
|
return generateContent(details.directive).then(text => {
|
||||||
} while ( response.length < 10 );
|
details.props.response.value = text;
|
||||||
response = response.slice(-10);
|
details.props.responseText.value = text;
|
||||||
responseText = response;
|
return details;
|
||||||
|
});
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Object.defineProperties(this, {
|
promise.then(details => {
|
||||||
response: { value: response },
|
Object.defineProperties(details.xhr, details.props);
|
||||||
responseText: { value: responseText },
|
details.xhr.dispatchEvent(new Event('readystatechange'));
|
||||||
responseXML: { value: responseXML },
|
details.xhr.dispatchEvent(new Event('load'));
|
||||||
|
details.xhr.dispatchEvent(new Event('loadend'));
|
||||||
});
|
});
|
||||||
this.dispatchEvent(new Event('readystatechange'));
|
|
||||||
this.dispatchEvent(new Event('load'));
|
|
||||||
this.dispatchEvent(new Event('loadend'));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1167,24 +1167,32 @@ vAPI.messaging = {
|
||||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/550
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/550
|
||||||
// Support using a new secret for every network request.
|
// Support using a new secret for every network request.
|
||||||
|
|
||||||
vAPI.warSecret = (( ) => {
|
{
|
||||||
const generateSecret = ( ) => {
|
// Generate a 6-character alphanumeric string, thus one random value out
|
||||||
return Math.floor(Math.random() * 982451653 + 982451653).toString(36);
|
// of 36^6 = over 2x10^9 values.
|
||||||
};
|
const generateSecret = ( ) =>
|
||||||
|
(Math.floor(Math.random() * 2176782336) + 2176782336).toString(36).slice(1);
|
||||||
|
|
||||||
const root = vAPI.getURL('/');
|
const root = vAPI.getURL('/');
|
||||||
const secrets = [];
|
const reSecret = /\?secret=(\w+)/;
|
||||||
let lastSecretTime = 0;
|
const shortSecrets = [];
|
||||||
|
let lastShortSecretTime = 0;
|
||||||
|
|
||||||
const guard = function(details) {
|
// Long secrets are meant to be used multiple times, but for at most a few
|
||||||
const url = details.url;
|
// minutes. The realm is one value out of 36^18 = over 10^28 values.
|
||||||
const pos = secrets.findIndex(secret =>
|
const longSecrets = [ '', '' ];
|
||||||
url.lastIndexOf(`?secret=${secret}`) !== -1
|
let lastLongSecretTimeSlice = 0;
|
||||||
);
|
|
||||||
|
const guard = details => {
|
||||||
|
const match = reSecret.exec(details.url);
|
||||||
|
if ( match === null ) { return; }
|
||||||
|
const secret = match[1];
|
||||||
|
if ( longSecrets.includes(secret) ) { return; }
|
||||||
|
const pos = shortSecrets.indexOf(secret);
|
||||||
if ( pos === -1 ) {
|
if ( pos === -1 ) {
|
||||||
return { cancel: true };
|
return { cancel: true };
|
||||||
}
|
}
|
||||||
secrets.splice(pos, 1);
|
shortSecrets.splice(pos, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
browser.webRequest.onBeforeRequest.addListener(
|
browser.webRequest.onBeforeRequest.addListener(
|
||||||
|
@ -1195,20 +1203,31 @@ vAPI.warSecret = (( ) => {
|
||||||
[ 'blocking' ]
|
[ 'blocking' ]
|
||||||
);
|
);
|
||||||
|
|
||||||
return ( ) => {
|
vAPI.warSecret = {
|
||||||
if ( secrets.length !== 0 ) {
|
short: ( ) => {
|
||||||
if ( (Date.now() - lastSecretTime) > 5000 ) {
|
if ( shortSecrets.length !== 0 ) {
|
||||||
secrets.splice(0);
|
if ( (Date.now() - lastShortSecretTime) > 5000 ) {
|
||||||
} else if ( secrets.length > 256 ) {
|
shortSecrets.splice(0);
|
||||||
secrets.splice(0, secrets.length - 192);
|
} else if ( shortSecrets.length > 256 ) {
|
||||||
|
shortSecrets.splice(0, shortSecrets.length - 192);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
lastShortSecretTime = Date.now();
|
||||||
lastSecretTime = Date.now();
|
const secret = generateSecret();
|
||||||
const secret = generateSecret();
|
shortSecrets.push(secret);
|
||||||
secrets.push(secret);
|
return secret;
|
||||||
return secret;
|
},
|
||||||
|
long: ( ) => {
|
||||||
|
const timeSlice = Date.now() >>> 19; // Changes every ~9 minutes
|
||||||
|
if ( timeSlice !== lastLongSecretTimeSlice ) {
|
||||||
|
longSecrets[1] = longSecrets[0];
|
||||||
|
longSecrets[0] = `${generateSecret()}${generateSecret()}${generateSecret()}`;
|
||||||
|
lastLongSecretTimeSlice = timeSlice;
|
||||||
|
}
|
||||||
|
return longSecrets[0];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
})();
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
|
|
|
@ -984,7 +984,7 @@ const onMessage = function(request, sender, callback) {
|
||||||
zap: µb.epickerArgs.zap,
|
zap: µb.epickerArgs.zap,
|
||||||
eprom: µb.epickerArgs.eprom,
|
eprom: µb.epickerArgs.eprom,
|
||||||
pickerURL: vAPI.getURL(
|
pickerURL: vAPI.getURL(
|
||||||
`/web_accessible_resources/epicker-ui.html?secret=${vAPI.warSecret()}`
|
`/web_accessible_resources/epicker-ui.html?secret=${vAPI.warSecret.short()}`
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
µb.epickerArgs.target = '';
|
µb.epickerArgs.target = '';
|
||||||
|
|
|
@ -152,7 +152,7 @@ const NetFilteringResultCache = class {
|
||||||
entry.redirectURL.startsWith(this.extensionOriginURL)
|
entry.redirectURL.startsWith(this.extensionOriginURL)
|
||||||
) {
|
) {
|
||||||
const redirectURL = new URL(entry.redirectURL);
|
const redirectURL = new URL(entry.redirectURL);
|
||||||
redirectURL.searchParams.set('secret', vAPI.warSecret());
|
redirectURL.searchParams.set('secret', vAPI.warSecret.short());
|
||||||
entry.redirectURL = redirectURL.href;
|
entry.redirectURL = redirectURL.href;
|
||||||
}
|
}
|
||||||
return entry;
|
return entry;
|
||||||
|
|
|
@ -66,12 +66,12 @@ const removeTopCommentBlock = text => {
|
||||||
return text.replace(/^\/\*[\S\s]+?\n\*\/\s*/, '');
|
return text.replace(/^\/\*[\S\s]+?\n\*\/\s*/, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
// vAPI.warSecret() is optional, it could be absent in some environments,
|
// vAPI.warSecret is optional, it could be absent in some environments,
|
||||||
// i.e. nodejs for example. Probably the best approach is to have the
|
// i.e. nodejs for example. Probably the best approach is to have the
|
||||||
// "web_accessible_resources secret" added outside by the client of this
|
// "web_accessible_resources secret" added outside by the client of this
|
||||||
// module, but for now I just want to remove an obstacle to modularization.
|
// module, but for now I just want to remove an obstacle to modularization.
|
||||||
const warSecret = typeof vAPI === 'object' && vAPI !== null
|
const warSecret = typeof vAPI === 'object' && vAPI !== null
|
||||||
? vAPI.warSecret
|
? vAPI.warSecret.short
|
||||||
: ( ) => '';
|
: ( ) => '';
|
||||||
|
|
||||||
const RESOURCES_SELFIE_VERSION = 7;
|
const RESOURCES_SELFIE_VERSION = 7;
|
||||||
|
@ -153,15 +153,7 @@ class RedirectEntry {
|
||||||
|
|
||||||
static fromDetails(details) {
|
static fromDetails(details) {
|
||||||
const r = new RedirectEntry();
|
const r = new RedirectEntry();
|
||||||
r.mime = details.mime;
|
Object.assign(r, details);
|
||||||
r.data = details.data;
|
|
||||||
r.requiresTrust = details.requiresTrust === true;
|
|
||||||
r.warURL = details.warURL !== undefined && details.warURL || undefined;
|
|
||||||
r.params = details.params !== undefined && details.params || undefined;
|
|
||||||
r.world = details.world || 'MAIN';
|
|
||||||
if ( Array.isArray(details.dependencies) ) {
|
|
||||||
r.dependencies.push(...details.dependencies);
|
|
||||||
}
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -331,17 +323,17 @@ class RedirectEngine {
|
||||||
const fetches = [
|
const fetches = [
|
||||||
import('/assets/resources/scriptlets.js').then(module => {
|
import('/assets/resources/scriptlets.js').then(module => {
|
||||||
for ( const scriptlet of module.builtinScriptlets ) {
|
for ( const scriptlet of module.builtinScriptlets ) {
|
||||||
const { name, aliases, fn } = scriptlet;
|
const details = {};
|
||||||
const entry = RedirectEntry.fromDetails({
|
details.mime = mimeFromName(scriptlet.name);
|
||||||
mime: mimeFromName(name),
|
details.data = scriptlet.fn.toString();
|
||||||
data: fn.toString(),
|
for ( const [ k, v ] of Object.entries(scriptlet) ) {
|
||||||
dependencies: scriptlet.dependencies,
|
if ( k === 'fn' ) { continue; }
|
||||||
requiresTrust: scriptlet.requiresTrust === true,
|
details[k] = v;
|
||||||
world: scriptlet.world || 'MAIN',
|
}
|
||||||
});
|
const entry = RedirectEntry.fromDetails(details);
|
||||||
this.resources.set(name, entry);
|
this.resources.set(details.name, entry);
|
||||||
if ( Array.isArray(aliases) === false ) { continue; }
|
if ( Array.isArray(details.aliases) === false ) { continue; }
|
||||||
for ( const alias of aliases ) {
|
for ( const alias of details.aliases ) {
|
||||||
this.aliases.set(alias, name);
|
this.aliases.set(alias, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -383,7 +383,10 @@ scriptletFilteringEngine.retrieve = function(request) {
|
||||||
return { filters: cacheDetails.filters };
|
return { filters: cacheDetails.filters };
|
||||||
}
|
}
|
||||||
|
|
||||||
const scriptletGlobals = [];
|
const scriptletGlobals = [
|
||||||
|
[ 'warOrigin', vAPI.getURL('/web_accessible_resources') ],
|
||||||
|
[ 'warSecret', vAPI.warSecret.long() ],
|
||||||
|
];
|
||||||
|
|
||||||
if ( isDevBuild === undefined ) {
|
if ( isDevBuild === undefined ) {
|
||||||
isDevBuild = vAPI.webextFlavor.soup.has('devbuild');
|
isDevBuild = vAPI.webextFlavor.soup.has('devbuild');
|
||||||
|
|
|
@ -1135,7 +1135,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => {
|
||||||
|
|
||||||
const fetcher = (path, options = undefined) => {
|
const fetcher = (path, options = undefined) => {
|
||||||
if ( path.startsWith('/web_accessible_resources/') ) {
|
if ( path.startsWith('/web_accessible_resources/') ) {
|
||||||
path += `?secret=${vAPI.warSecret()}`;
|
path += `?secret=${vAPI.warSecret.short()}`;
|
||||||
return io.fetch(path, options);
|
return io.fetch(path, options);
|
||||||
}
|
}
|
||||||
return io.fetchText(path);
|
return io.fetchText(path);
|
||||||
|
|
Loading…
Reference in a new issue