summaryrefslogtreecommitdiffstats
path: root/platform/firefox/vapi-background-ext.js
diff options
context:
space:
mode:
Diffstat (limited to 'platform/firefox/vapi-background-ext.js')
-rw-r--r--platform/firefox/vapi-background-ext.js328
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();
+
+/******************************************************************************/