diff --git a/platform/firefox/vapi-background-ext.js b/platform/firefox/vapi-background-ext.js index e468581e4..b4569f41c 100644 --- a/platform/firefox/vapi-background-ext.js +++ b/platform/firefox/vapi-background-ext.js @@ -32,278 +32,273 @@ import { /******************************************************************************/ -(( ) => { - // https://github.com/uBlockOrigin/uBlock-issues/issues/407 - if ( vAPI.webextFlavor.soup.has('firefox') === false ) { return; } +// Canonical name-uncloaking feature. +let cnameUncloakEnabled = browser.dns instanceof Object; +let cnameUncloakProxied = false; - // Canonical name-uncloaking feature. - let cnameUncloakEnabled = browser.dns instanceof Object; - let cnameUncloakProxied = false; +// https://github.com/uBlockOrigin/uBlock-issues/issues/911 +// We detect here whether network requests are proxied, and if so, +// de-aliasing of hostnames will be disabled to avoid possible +// DNS leaks. +const proxyDetector = function(details) { + if ( details.proxyInfo instanceof Object ) { + cnameUncloakEnabled = false; + proxyDetectorTryCount = 0; + } + if ( proxyDetectorTryCount === 0 ) { + browser.webRequest.onHeadersReceived.removeListener(proxyDetector); + return; + } + proxyDetectorTryCount -= 1; +}; +let proxyDetectorTryCount = 0; - // https://github.com/uBlockOrigin/uBlock-issues/issues/911 - // We detect here whether network requests are proxied, and if so, - // de-aliasing of hostnames will be disabled to avoid possible - // DNS leaks. - const proxyDetector = function(details) { - if ( details.proxyInfo instanceof Object ) { - cnameUncloakEnabled = false; - proxyDetectorTryCount = 0; - } - if ( proxyDetectorTryCount === 0 ) { - browser.webRequest.onHeadersReceived.removeListener(proxyDetector); - return; - } - proxyDetectorTryCount -= 1; - }; - let proxyDetectorTryCount = 0; +// Related issues: +// - https://github.com/gorhill/uBlock/issues/1327 +// - https://github.com/uBlockOrigin/uBlock-issues/issues/128 +// - https://bugzilla.mozilla.org/show_bug.cgi?id=1503721 - // Related issues: - // - https://github.com/gorhill/uBlock/issues/1327 - // - https://github.com/uBlockOrigin/uBlock-issues/issues/128 - // - https://bugzilla.mozilla.org/show_bug.cgi?id=1503721 +// Extend base class to normalize as per platform. - // Extend base class to normalize as per platform. - - vAPI.Net = class extends vAPI.Net { - constructor() { - super(); - this.pendingRequests = []; - this.canUncloakCnames = browser.dns instanceof Object; - this.cnames = new Map([ [ '', null ] ]); - this.cnameIgnoreList = null; - this.cnameIgnore1stParty = true; - this.cnameIgnoreExceptions = true; - this.cnameIgnoreRootDocument = true; - this.cnameMaxTTL = 120; - this.cnameReplayFullURL = false; - this.cnameFlushTime = Date.now() + this.cnameMaxTTL * 60000; +vAPI.Net = class extends vAPI.Net { + constructor() { + super(); + this.pendingRequests = []; + this.canUncloakCnames = browser.dns instanceof Object; + this.cnames = new Map([ [ '', null ] ]); + this.cnameIgnoreList = null; + this.cnameIgnore1stParty = true; + this.cnameIgnoreExceptions = true; + this.cnameIgnoreRootDocument = true; + this.cnameMaxTTL = 120; + this.cnameReplayFullURL = false; + this.cnameFlushTime = Date.now() + this.cnameMaxTTL * 60000; + } + setOptions(options) { + super.setOptions(options); + if ( 'cnameUncloakEnabled' in options ) { + cnameUncloakEnabled = + this.canUncloakCnames && + options.cnameUncloakEnabled !== false; } - setOptions(options) { - super.setOptions(options); - if ( 'cnameUncloakEnabled' in options ) { - cnameUncloakEnabled = - this.canUncloakCnames && - options.cnameUncloakEnabled !== false; - } - if ( 'cnameUncloakProxied' in options ) { - cnameUncloakProxied = options.cnameUncloakProxied === true; - } - if ( 'cnameIgnoreList' in options ) { - this.cnameIgnoreList = - this.regexFromStrList(options.cnameIgnoreList); - } - if ( 'cnameIgnore1stParty' in options ) { - this.cnameIgnore1stParty = - options.cnameIgnore1stParty !== false; - } - if ( 'cnameIgnoreExceptions' in options ) { - this.cnameIgnoreExceptions = - options.cnameIgnoreExceptions !== false; - } - if ( 'cnameIgnoreRootDocument' in options ) { - this.cnameIgnoreRootDocument = - options.cnameIgnoreRootDocument !== false; - } - if ( 'cnameMaxTTL' in options ) { - this.cnameMaxTTL = options.cnameMaxTTL || 120; - } - if ( 'cnameReplayFullURL' in options ) { - this.cnameReplayFullURL = options.cnameReplayFullURL === true; - } - this.cnames.clear(); this.cnames.set('', null); - this.cnameFlushTime = Date.now() + this.cnameMaxTTL * 60000; - // https://github.com/uBlockOrigin/uBlock-issues/issues/911 - // Install/remove proxy detector. - if ( vAPI.webextFlavor.major < 80 ) { - const wrohr = browser.webRequest.onHeadersReceived; - if ( cnameUncloakEnabled === false || cnameUncloakProxied ) { - if ( wrohr.hasListener(proxyDetector) ) { - wrohr.removeListener(proxyDetector); - } - } else if ( wrohr.hasListener(proxyDetector) === false ) { - wrohr.addListener( - proxyDetector, - { urls: [ '*://*/*' ] }, - [ 'blocking' ] - ); + if ( 'cnameUncloakProxied' in options ) { + cnameUncloakProxied = options.cnameUncloakProxied === true; + } + if ( 'cnameIgnoreList' in options ) { + this.cnameIgnoreList = + this.regexFromStrList(options.cnameIgnoreList); + } + if ( 'cnameIgnore1stParty' in options ) { + this.cnameIgnore1stParty = + options.cnameIgnore1stParty !== false; + } + if ( 'cnameIgnoreExceptions' in options ) { + this.cnameIgnoreExceptions = + options.cnameIgnoreExceptions !== false; + } + if ( 'cnameIgnoreRootDocument' in options ) { + this.cnameIgnoreRootDocument = + options.cnameIgnoreRootDocument !== false; + } + if ( 'cnameMaxTTL' in options ) { + this.cnameMaxTTL = options.cnameMaxTTL || 120; + } + if ( 'cnameReplayFullURL' in options ) { + this.cnameReplayFullURL = options.cnameReplayFullURL === true; + } + this.cnames.clear(); this.cnames.set('', null); + this.cnameFlushTime = Date.now() + this.cnameMaxTTL * 60000; + // https://github.com/uBlockOrigin/uBlock-issues/issues/911 + // Install/remove proxy detector. + if ( vAPI.webextFlavor.major < 80 ) { + const wrohr = browser.webRequest.onHeadersReceived; + if ( cnameUncloakEnabled === false || cnameUncloakProxied ) { + if ( wrohr.hasListener(proxyDetector) ) { + wrohr.removeListener(proxyDetector); } - proxyDetectorTryCount = 32; - } - } - normalizeDetails(details) { - const type = details.type; - - if ( type === 'imageset' ) { - details.type = 'image'; - return; - } - - // https://github.com/uBlockOrigin/uBlock-issues/issues/345 - // Re-categorize an embedded object as a `sub_frame` if its - // content type is that of a HTML document. - if ( type === 'object' && Array.isArray(details.responseHeaders) ) { - for ( const header of details.responseHeaders ) { - if ( header.name.toLowerCase() === 'content-type' ) { - if ( header.value.startsWith('text/html') ) { - details.type = 'sub_frame'; - } - break; - } - } - } - } - denormalizeTypes(types) { - if ( types.length === 0 ) { - return Array.from(this.validTypes); - } - const out = new Set(); - for ( const type of types ) { - if ( this.validTypes.has(type) ) { - out.add(type); - } - if ( type === 'image' && this.validTypes.has('imageset') ) { - out.add('imageset'); - } - if ( type === 'sub_frame' ) { - out.add('object'); - } - } - return Array.from(out); - } - canonicalNameFromHostname(hn) { - const cnRecord = this.cnames.get(hn); - if ( cnRecord !== undefined && cnRecord !== null ) { - return cnRecord.cname; - } - } - processCanonicalName(hn, cnRecord, details) { - if ( cnRecord === null ) { return; } - if ( cnRecord.isRootDocument ) { return; } - const hnBeg = details.url.indexOf(hn); - if ( hnBeg === -1 ) { return; } - const oldURL = details.url; - let newURL = oldURL.slice(0, hnBeg) + cnRecord.cname; - const hnEnd = hnBeg + hn.length; - if ( this.cnameReplayFullURL ) { - newURL += oldURL.slice(hnEnd); - } else { - const pathBeg = oldURL.indexOf('/', hnEnd); - if ( pathBeg !== -1 ) { - newURL += oldURL.slice(hnEnd, pathBeg + 1); - } - } - details.url = newURL; - details.aliasURL = oldURL; - return super.onBeforeSuspendableRequest(details); - } - recordCanonicalName(hn, record, isRootDocument) { - if ( (this.cnames.size & 0b111111) === 0 ) { - const now = Date.now(); - if ( now >= this.cnameFlushTime ) { - this.cnames.clear(); this.cnames.set('', null); - this.cnameFlushTime = now + this.cnameMaxTTL * 60000; - } - } - let cname = - typeof record.canonicalName === 'string' && - record.canonicalName !== hn - ? record.canonicalName - : ''; - if ( - cname !== '' && - this.cnameIgnore1stParty && - domainFromHostname(cname) === domainFromHostname(hn) - ) { - cname = ''; - } - if ( - cname !== '' && - this.cnameIgnoreList !== null && - this.cnameIgnoreList.test(cname) - ) { - cname = ''; - } - const cnRecord = cname !== '' ? { cname, isRootDocument } : null; - this.cnames.set(hn, cnRecord); - return cnRecord; - } - regexFromStrList(list) { - if ( - typeof list !== 'string' || - list.length === 0 || - list === 'unset' || - browser.dns instanceof Object === false - ) { - return null; - } - if ( list === '*' ) { - return /^./; - } - return new RegExp( - '(?:^|\.)(?:' + - list.trim() - .split(/\s+/) - .map(a => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - .join('|') + - ')$' - ); - } - onBeforeSuspendableRequest(details) { - const r = super.onBeforeSuspendableRequest(details); - if ( cnameUncloakEnabled === false ) { return r; } - if ( r !== undefined ) { - if ( - r.cancel === true || - r.redirectUrl !== undefined || - this.cnameIgnoreExceptions - ) { - return r; - } - } - const isRootDocument = details.type === 'main_frame' && - this.cnameIgnoreRootDocument; - const hn = hostnameFromNetworkURL(details.url); - const cnRecord = this.cnames.get(hn); - if ( cnRecord !== undefined ) { - return this.processCanonicalName(hn, cnRecord, details); - } - return browser.dns.resolve(hn, [ 'canonical_name' ]).then( - rec => { - const cnRecord = this.recordCanonicalName(hn, rec, isRootDocument); - return this.processCanonicalName(hn, cnRecord, details); - }, - ( ) => { - this.cnames.set(hn, null); - } - ); - } - suspendOneRequest(details) { - const pending = { - details: Object.assign({}, details), - resolve: undefined, - promise: undefined - }; - pending.promise = new Promise(resolve => { - pending.resolve = resolve; - }); - this.pendingRequests.push(pending); - return pending.promise; - } - unsuspendAllRequests(discard = false) { - const pendingRequests = this.pendingRequests; - this.pendingRequests = []; - for ( const entry of pendingRequests ) { - entry.resolve( - discard !== true - ? this.onBeforeSuspendableRequest(entry.details) - : undefined + } else if ( wrohr.hasListener(proxyDetector) === false ) { + wrohr.addListener( + proxyDetector, + { urls: [ '*://*/*' ] }, + [ 'blocking' ] ); } + proxyDetectorTryCount = 32; } - static canSuspend() { - return true; + } + normalizeDetails(details) { + const type = details.type; + + if ( type === 'imageset' ) { + details.type = 'image'; + return; } - }; -})(); + + // https://github.com/uBlockOrigin/uBlock-issues/issues/345 + // Re-categorize an embedded object as a `sub_frame` if its + // content type is that of a HTML document. + if ( type === 'object' && Array.isArray(details.responseHeaders) ) { + for ( const header of details.responseHeaders ) { + if ( header.name.toLowerCase() === 'content-type' ) { + if ( header.value.startsWith('text/html') ) { + details.type = 'sub_frame'; + } + break; + } + } + } + } + denormalizeTypes(types) { + if ( types.length === 0 ) { + return Array.from(this.validTypes); + } + const out = new Set(); + for ( const type of types ) { + if ( this.validTypes.has(type) ) { + out.add(type); + } + if ( type === 'image' && this.validTypes.has('imageset') ) { + out.add('imageset'); + } + if ( type === 'sub_frame' ) { + out.add('object'); + } + } + return Array.from(out); + } + canonicalNameFromHostname(hn) { + const cnRecord = this.cnames.get(hn); + if ( cnRecord !== undefined && cnRecord !== null ) { + return cnRecord.cname; + } + } + processCanonicalName(hn, cnRecord, details) { + if ( cnRecord === null ) { return; } + if ( cnRecord.isRootDocument ) { return; } + const hnBeg = details.url.indexOf(hn); + if ( hnBeg === -1 ) { return; } + const oldURL = details.url; + let newURL = oldURL.slice(0, hnBeg) + cnRecord.cname; + const hnEnd = hnBeg + hn.length; + if ( this.cnameReplayFullURL ) { + newURL += oldURL.slice(hnEnd); + } else { + const pathBeg = oldURL.indexOf('/', hnEnd); + if ( pathBeg !== -1 ) { + newURL += oldURL.slice(hnEnd, pathBeg + 1); + } + } + details.url = newURL; + details.aliasURL = oldURL; + return super.onBeforeSuspendableRequest(details); + } + recordCanonicalName(hn, record, isRootDocument) { + if ( (this.cnames.size & 0b111111) === 0 ) { + const now = Date.now(); + if ( now >= this.cnameFlushTime ) { + this.cnames.clear(); this.cnames.set('', null); + this.cnameFlushTime = now + this.cnameMaxTTL * 60000; + } + } + let cname = + typeof record.canonicalName === 'string' && + record.canonicalName !== hn + ? record.canonicalName + : ''; + if ( + cname !== '' && + this.cnameIgnore1stParty && + domainFromHostname(cname) === domainFromHostname(hn) + ) { + cname = ''; + } + if ( + cname !== '' && + this.cnameIgnoreList !== null && + this.cnameIgnoreList.test(cname) + ) { + cname = ''; + } + const cnRecord = cname !== '' ? { cname, isRootDocument } : null; + this.cnames.set(hn, cnRecord); + return cnRecord; + } + regexFromStrList(list) { + if ( + typeof list !== 'string' || + list.length === 0 || + list === 'unset' || + browser.dns instanceof Object === false + ) { + return null; + } + if ( list === '*' ) { + return /^./; + } + return new RegExp( + '(?:^|\.)(?:' + + list.trim() + .split(/\s+/) + .map(a => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + .join('|') + + ')$' + ); + } + onBeforeSuspendableRequest(details) { + const r = super.onBeforeSuspendableRequest(details); + if ( cnameUncloakEnabled === false ) { return r; } + if ( r !== undefined ) { + if ( + r.cancel === true || + r.redirectUrl !== undefined || + this.cnameIgnoreExceptions + ) { + return r; + } + } + const isRootDocument = details.type === 'main_frame' && + this.cnameIgnoreRootDocument; + const hn = hostnameFromNetworkURL(details.url); + const cnRecord = this.cnames.get(hn); + if ( cnRecord !== undefined ) { + return this.processCanonicalName(hn, cnRecord, details); + } + return browser.dns.resolve(hn, [ 'canonical_name' ]).then( + rec => { + const cnRecord = this.recordCanonicalName(hn, rec, isRootDocument); + return this.processCanonicalName(hn, cnRecord, details); + }, + ( ) => { + this.cnames.set(hn, null); + } + ); + } + suspendOneRequest(details) { + const pending = { + details: Object.assign({}, details), + resolve: undefined, + promise: undefined + }; + pending.promise = new Promise(resolve => { + pending.resolve = resolve; + }); + this.pendingRequests.push(pending); + return pending.promise; + } + unsuspendAllRequests(discard = false) { + const pendingRequests = this.pendingRequests; + this.pendingRequests = []; + for ( const entry of pendingRequests ) { + entry.resolve( + discard !== true + ? this.onBeforeSuspendableRequest(entry.details) + : undefined + ); + } + } + static canSuspend() { + return true; + } +}; /******************************************************************************/