mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-10 01:02:08 +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(
|
||||
propsToMatch = '',
|
||||
randomize = ''
|
||||
directive = ''
|
||||
) {
|
||||
if ( typeof propsToMatch !== 'string' ) { return; }
|
||||
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 += 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 {
|
||||
open(method, url, ...args) {
|
||||
if ( log !== undefined ) {
|
||||
log(`uBO: xhr.open(${method}, ${url}, ${args.join(', ')})`);
|
||||
} else {
|
||||
const haystack = { method, url };
|
||||
if ( matchObjectProperties(propNeedles, haystack) ) {
|
||||
xhrInstances.set(this, haystack);
|
||||
}
|
||||
return super.open(method, url, ...args);
|
||||
}
|
||||
if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
|
||||
return super.open(method, url, ...args);
|
||||
}
|
||||
const haystack = { method, url };
|
||||
if ( matchObjectProperties(propNeedles, haystack) ) {
|
||||
xhrInstances.set(this, haystack);
|
||||
}
|
||||
return super.open(method, url, ...args);
|
||||
}
|
||||
|
@ -1958,50 +1992,66 @@ function noXhrIf(
|
|||
if ( haystack === undefined ) {
|
||||
return super.send(...args);
|
||||
}
|
||||
Object.defineProperties(this, {
|
||||
readyState: { value: 4 },
|
||||
responseURL: { value: haystack.url },
|
||||
status: { value: 200 },
|
||||
statusText: { value: 'OK' },
|
||||
let promise = Promise.resolve({
|
||||
xhr: this,
|
||||
directive,
|
||||
props: {
|
||||
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 ) {
|
||||
case 'arraybuffer':
|
||||
response = new ArrayBuffer(0);
|
||||
promise = promise.then(details => {
|
||||
details.props.response.value = new ArrayBuffer(0);
|
||||
return details;
|
||||
});
|
||||
break;
|
||||
case 'blob':
|
||||
response = new Blob([]);
|
||||
promise = promise.then(details => {
|
||||
details.props.response.value = new Blob([]);
|
||||
return details;
|
||||
});
|
||||
break;
|
||||
case 'document': {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString('', 'text/html');
|
||||
response = doc;
|
||||
responseXML = doc;
|
||||
promise = promise.then(details => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString('', 'text/html');
|
||||
details.props.response.value = doc;
|
||||
details.props.responseXML.value = doc;
|
||||
return details;
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'json':
|
||||
response = {};
|
||||
responseText = '{}';
|
||||
promise = promise.then(details => {
|
||||
details.props.response.value = {};
|
||||
details.props.responseText.value = '{}';
|
||||
return details;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
if ( randomize !== 'true' ) { break; }
|
||||
do {
|
||||
response += Math.random().toString(36).slice(-2);
|
||||
} while ( response.length < 10 );
|
||||
response = response.slice(-10);
|
||||
responseText = response;
|
||||
if ( directive === '' ) { break; }
|
||||
promise = promise.then(details => {
|
||||
return generateContent(details.directive).then(text => {
|
||||
details.props.response.value = text;
|
||||
details.props.responseText.value = text;
|
||||
return details;
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
Object.defineProperties(this, {
|
||||
response: { value: response },
|
||||
responseText: { value: responseText },
|
||||
responseXML: { value: responseXML },
|
||||
promise.then(details => {
|
||||
Object.defineProperties(details.xhr, details.props);
|
||||
details.xhr.dispatchEvent(new Event('readystatechange'));
|
||||
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
|
||||
// Support using a new secret for every network request.
|
||||
|
||||
vAPI.warSecret = (( ) => {
|
||||
const generateSecret = ( ) => {
|
||||
return Math.floor(Math.random() * 982451653 + 982451653).toString(36);
|
||||
};
|
||||
{
|
||||
// Generate a 6-character alphanumeric string, thus one random value out
|
||||
// of 36^6 = over 2x10^9 values.
|
||||
const generateSecret = ( ) =>
|
||||
(Math.floor(Math.random() * 2176782336) + 2176782336).toString(36).slice(1);
|
||||
|
||||
const root = vAPI.getURL('/');
|
||||
const secrets = [];
|
||||
let lastSecretTime = 0;
|
||||
const reSecret = /\?secret=(\w+)/;
|
||||
const shortSecrets = [];
|
||||
let lastShortSecretTime = 0;
|
||||
|
||||
const guard = function(details) {
|
||||
const url = details.url;
|
||||
const pos = secrets.findIndex(secret =>
|
||||
url.lastIndexOf(`?secret=${secret}`) !== -1
|
||||
);
|
||||
// Long secrets are meant to be used multiple times, but for at most a few
|
||||
// minutes. The realm is one value out of 36^18 = over 10^28 values.
|
||||
const longSecrets = [ '', '' ];
|
||||
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 ) {
|
||||
return { cancel: true };
|
||||
}
|
||||
secrets.splice(pos, 1);
|
||||
shortSecrets.splice(pos, 1);
|
||||
};
|
||||
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
|
@ -1195,20 +1203,31 @@ vAPI.warSecret = (( ) => {
|
|||
[ 'blocking' ]
|
||||
);
|
||||
|
||||
return ( ) => {
|
||||
if ( secrets.length !== 0 ) {
|
||||
if ( (Date.now() - lastSecretTime) > 5000 ) {
|
||||
secrets.splice(0);
|
||||
} else if ( secrets.length > 256 ) {
|
||||
secrets.splice(0, secrets.length - 192);
|
||||
vAPI.warSecret = {
|
||||
short: ( ) => {
|
||||
if ( shortSecrets.length !== 0 ) {
|
||||
if ( (Date.now() - lastShortSecretTime) > 5000 ) {
|
||||
shortSecrets.splice(0);
|
||||
} else if ( shortSecrets.length > 256 ) {
|
||||
shortSecrets.splice(0, shortSecrets.length - 192);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastSecretTime = Date.now();
|
||||
const secret = generateSecret();
|
||||
secrets.push(secret);
|
||||
return secret;
|
||||
lastShortSecretTime = Date.now();
|
||||
const secret = generateSecret();
|
||||
shortSecrets.push(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,
|
||||
eprom: µb.epickerArgs.eprom,
|
||||
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 = '';
|
||||
|
|
|
@ -152,7 +152,7 @@ const NetFilteringResultCache = class {
|
|||
entry.redirectURL.startsWith(this.extensionOriginURL)
|
||||
) {
|
||||
const redirectURL = new URL(entry.redirectURL);
|
||||
redirectURL.searchParams.set('secret', vAPI.warSecret());
|
||||
redirectURL.searchParams.set('secret', vAPI.warSecret.short());
|
||||
entry.redirectURL = redirectURL.href;
|
||||
}
|
||||
return entry;
|
||||
|
|
|
@ -66,12 +66,12 @@ const removeTopCommentBlock = text => {
|
|||
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
|
||||
// "web_accessible_resources secret" added outside by the client of this
|
||||
// module, but for now I just want to remove an obstacle to modularization.
|
||||
const warSecret = typeof vAPI === 'object' && vAPI !== null
|
||||
? vAPI.warSecret
|
||||
? vAPI.warSecret.short
|
||||
: ( ) => '';
|
||||
|
||||
const RESOURCES_SELFIE_VERSION = 7;
|
||||
|
@ -153,15 +153,7 @@ class RedirectEntry {
|
|||
|
||||
static fromDetails(details) {
|
||||
const r = new RedirectEntry();
|
||||
r.mime = details.mime;
|
||||
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);
|
||||
}
|
||||
Object.assign(r, details);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
@ -331,17 +323,17 @@ class RedirectEngine {
|
|||
const fetches = [
|
||||
import('/assets/resources/scriptlets.js').then(module => {
|
||||
for ( const scriptlet of module.builtinScriptlets ) {
|
||||
const { name, aliases, fn } = scriptlet;
|
||||
const entry = RedirectEntry.fromDetails({
|
||||
mime: mimeFromName(name),
|
||||
data: fn.toString(),
|
||||
dependencies: scriptlet.dependencies,
|
||||
requiresTrust: scriptlet.requiresTrust === true,
|
||||
world: scriptlet.world || 'MAIN',
|
||||
});
|
||||
this.resources.set(name, entry);
|
||||
if ( Array.isArray(aliases) === false ) { continue; }
|
||||
for ( const alias of aliases ) {
|
||||
const details = {};
|
||||
details.mime = mimeFromName(scriptlet.name);
|
||||
details.data = scriptlet.fn.toString();
|
||||
for ( const [ k, v ] of Object.entries(scriptlet) ) {
|
||||
if ( k === 'fn' ) { continue; }
|
||||
details[k] = v;
|
||||
}
|
||||
const entry = RedirectEntry.fromDetails(details);
|
||||
this.resources.set(details.name, entry);
|
||||
if ( Array.isArray(details.aliases) === false ) { continue; }
|
||||
for ( const alias of details.aliases ) {
|
||||
this.aliases.set(alias, name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -383,7 +383,10 @@ scriptletFilteringEngine.retrieve = function(request) {
|
|||
return { filters: cacheDetails.filters };
|
||||
}
|
||||
|
||||
const scriptletGlobals = [];
|
||||
const scriptletGlobals = [
|
||||
[ 'warOrigin', vAPI.getURL('/web_accessible_resources') ],
|
||||
[ 'warSecret', vAPI.warSecret.long() ],
|
||||
];
|
||||
|
||||
if ( isDevBuild === undefined ) {
|
||||
isDevBuild = vAPI.webextFlavor.soup.has('devbuild');
|
||||
|
|
|
@ -1135,7 +1135,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => {
|
|||
|
||||
const fetcher = (path, options = undefined) => {
|
||||
if ( path.startsWith('/web_accessible_resources/') ) {
|
||||
path += `?secret=${vAPI.warSecret()}`;
|
||||
path += `?secret=${vAPI.warSecret.short()}`;
|
||||
return io.fetch(path, options);
|
||||
}
|
||||
return io.fetchText(path);
|
||||
|
|
Loading…
Reference in a new issue