diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 84b83cbfe..48c4b9a8f 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -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')); } }; } diff --git a/platform/common/vapi-background.js b/platform/common/vapi-background.js index 9cb8f901e..334b52050 100644 --- a/platform/common/vapi-background.js +++ b/platform/common/vapi-background.js @@ -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]; + }, }; -})(); +} /******************************************************************************/ diff --git a/src/js/messaging.js b/src/js/messaging.js index 4ba80a24e..1b394d088 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -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 = ''; diff --git a/src/js/pagestore.js b/src/js/pagestore.js index 0dbdabc70..e51dde815 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -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; diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index 9be351b4e..5a6e480a2 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -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); } } diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index 207af9fb5..19c8d1308 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -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'); diff --git a/src/js/storage.js b/src/js/storage.js index 3f742c015..7266868f4 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -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);