diff options
Diffstat (limited to 'platform/firefox/vapi-background-ext.js')
-rw-r--r-- | platform/firefox/vapi-background-ext.js | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/platform/firefox/vapi-background-ext.js b/platform/firefox/vapi-background-ext.js new file mode 100644 index 0000000..8ecefc9 --- /dev/null +++ b/platform/firefox/vapi-background-ext.js @@ -0,0 +1,328 @@ +/******************************************************************************* + + uBlock Origin - a comprehensive, efficient content blocker + Copyright (C) 2017-present Raymond Hill + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see {http://www.gnu.org/licenses/}. + + Home: https://github.com/gorhill/uBlock +*/ + +/* globals browser */ + +'use strict'; + +/******************************************************************************/ + +import { + domainFromHostname, + hostnameFromNetworkURL, +} from './uri-utils.js'; + +/******************************************************************************/ + +// 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; + +// 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. + +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; + } + 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' ] + ); + } + 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 hn = hostnameFromNetworkURL(details.url); + const cnRecord = this.cnames.get(hn); + if ( cnRecord !== undefined ) { + return this.processCanonicalName(hn, cnRecord, details); + } + const documentUrl = details.documentUrl || details.url; + const isRootDocument = this.cnameIgnoreRootDocument && + hn === hostnameFromNetworkURL(documentUrl); + 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; + } +}; + +/******************************************************************************/ + +vAPI.scriptletsInjector = ((doc, details) => { + let script, url; + try { + const blob = new self.Blob( + [ details.scriptlets ], + { type: 'text/javascript; charset=utf-8' } + ); + url = self.URL.createObjectURL(blob); + script = doc.createElement('script'); + script.async = false; + script.src = url; + (doc.head || doc.documentElement || doc).append(script); + self.uBO_scriptletsInjected = details.filters; + } catch (ex) { + } + if ( url ) { + if ( script ) { script.remove(); } + self.URL.revokeObjectURL(url); + } +}).toString(); + +/******************************************************************************/ |