summaryrefslogtreecommitdiff
path: root/data/extensions/uBlock0@raymondhill.net/js/vapi-background-ext.js
diff options
context:
space:
mode:
Diffstat (limited to 'data/extensions/uBlock0@raymondhill.net/js/vapi-background-ext.js')
-rw-r--r--data/extensions/uBlock0@raymondhill.net/js/vapi-background-ext.js427
1 files changed, 427 insertions, 0 deletions
diff --git a/data/extensions/uBlock0@raymondhill.net/js/vapi-background-ext.js b/data/extensions/uBlock0@raymondhill.net/js/vapi-background-ext.js
new file mode 100644
index 0000000..a5a80b8
--- /dev/null
+++ b/data/extensions/uBlock0@raymondhill.net/js/vapi-background-ext.js
@@ -0,0 +1,427 @@
+/*******************************************************************************
+
+ 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
+*/
+
+import {
+ domainFromHostname,
+ hostnameFromNetworkURL,
+} from './uri-utils.js';
+
+/******************************************************************************/
+
+const dnsAPI = browser.dns || {
+ resolve() {
+ return Promise.resolve();
+ }
+};
+
+const isPromise = o => o instanceof Promise;
+const isResolvedObject = o => o instanceof Object &&
+ o instanceof Promise === false;
+const reIPv4 = /^\d+\.\d+\.\d+\.\d+$/
+const skipDNS = proxyInfo =>
+ proxyInfo && (proxyInfo.proxyDNS || proxyInfo.type?.charCodeAt(0) === 0x68 /* h */);
+
+/******************************************************************************/
+
+// 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.dnsList = []; // ring buffer
+ this.dnsWritePtr = 0; // next write pointer in ring buffer
+ this.dnsMaxCount = 512; // max size of ring buffer
+ this.dnsDict = new Map(); // hn to index in ring buffer
+ this.dnsCacheTTL = 600; // TTL in seconds
+ this.canUncloakCnames = true;
+ this.cnameUncloakEnabled = true;
+ this.cnameIgnoreList = null;
+ this.cnameIgnore1stParty = true;
+ this.cnameIgnoreExceptions = true;
+ this.cnameIgnoreRootDocument = true;
+ this.cnameReplayFullURL = false;
+ this.dnsResolveEnabled = true;
+ }
+
+ setOptions(options) {
+ super.setOptions(options);
+ if ( 'cnameUncloakEnabled' in options ) {
+ this.cnameUncloakEnabled =
+ options.cnameUncloakEnabled !== false;
+ }
+ 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 ( 'cnameReplayFullURL' in options ) {
+ this.cnameReplayFullURL = options.cnameReplayFullURL === true;
+ }
+ if ( 'dnsCacheTTL' in options ) {
+ this.dnsCacheTTL = options.dnsCacheTTL;
+ }
+ if ( 'dnsResolveEnabled' in options ) {
+ this.dnsResolveEnabled = options.dnsResolveEnabled === true;
+ }
+ this.dnsList.fill(null);
+ this.dnsDict.clear();
+ }
+
+ normalizeDetails(details) {
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/3379
+ if ( skipDNS(details.proxyInfo) && details.ip === '0.0.0.0' ) {
+ details.ip = null;
+ }
+ const type = details.type;
+ if ( type === 'imageset' ) {
+ details.type = 'image';
+ return;
+ }
+ if ( type !== 'object' ) { return; }
+ // Try to extract type from response headers if present.
+ if ( details.responseHeaders === undefined ) { return; }
+ const ctype = this.headerValue(details.responseHeaders, 'content-type');
+ // 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 ( ctype === 'text/html' ) {
+ details.type = 'sub_frame';
+ }
+ }
+
+ 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) {
+ if ( hn === '' ) { return; }
+ const dnsEntry = this.dnsFromCache(hn, true);
+ if ( isResolvedObject(dnsEntry) === false ) { return; }
+ return dnsEntry.cname;
+ }
+
+ regexFromStrList(list) {
+ if ( typeof list !== 'string' || list.length === 0 || list === 'unset' ) {
+ return null;
+ }
+ if ( list === '*' ) { return /^./; }
+ return new RegExp(
+ '(?:^|\\.)(?:' +
+ list.trim()
+ .split(/\s+/)
+ .map(a => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
+ .join('|') +
+ ')$'
+ );
+ }
+
+ onBeforeSuspendableRequest(details) {
+ const hn = hostnameFromNetworkURL(details.url);
+ const dnsEntry = this.dnsFromCache(hn);
+ if ( isResolvedObject(dnsEntry) && dnsEntry.ip ) {
+ details.ip = dnsEntry.ip;
+ }
+ const r = super.onBeforeSuspendableRequest(details);
+ if ( r !== undefined ) {
+ if (
+ r.cancel === true ||
+ r.redirectUrl !== undefined ||
+ this.cnameIgnoreExceptions
+ ) {
+ return r;
+ }
+ }
+ if ( isResolvedObject(dnsEntry) ) {
+ return this.onAfterDNSResolution(hn, details, dnsEntry);
+ }
+ if ( skipDNS(details.proxyInfo) ) { return; }
+ if ( this.dnsShouldResolve(hn) === false ) { return; }
+ const promise = dnsEntry || this.dnsResolve(hn, details);
+ return promise.then(( ) => this.onAfterDNSResolution(hn, details));
+ }
+
+ onAfterDNSResolution(hn, details, dnsEntry) {
+ if ( dnsEntry === undefined ) {
+ dnsEntry = this.dnsFromCache(hn);
+ if ( isResolvedObject(dnsEntry) === false ) { return; }
+ }
+ let proceed = false;
+ if ( dnsEntry.cname && this.cnameUncloakEnabled ) {
+ const newURL = this.uncloakURL(hn, dnsEntry, details);
+ if ( newURL ) {
+ details.aliasURL = details.url;
+ details.url = newURL;
+ proceed = true;
+ }
+ }
+ if ( dnsEntry.ip && details.ip !== dnsEntry.ip ) {
+ details.ip = dnsEntry.ip
+ proceed = true;
+ }
+ if ( proceed === false ) { return; }
+ // Must call method on base class
+ return super.onBeforeSuspendableRequest(details);
+ }
+
+ dnsToCache(hn, record, details) {
+ const dnsEntry = { hn, until: Date.now() + this.dnsCacheTTL * 1000 };
+ if ( record ) {
+ const cname = this.cnameFromRecord(hn, record, details);
+ if ( cname ) { dnsEntry.cname = cname; }
+ const ip = this.ipFromRecord(record);
+ if ( ip ) { dnsEntry.ip = ip; }
+ }
+ this.dnsSetCache(-1, hn, dnsEntry);
+ return dnsEntry;
+ }
+
+ dnsFromCache(hn, passive = false) {
+ const i = this.dnsDict.get(hn);
+ if ( i === undefined ) { return; }
+ if ( isPromise(i) ) { return i; }
+ const dnsEntry = this.dnsList[i];
+ if ( dnsEntry !== null && dnsEntry.hn === hn ) {
+ if ( passive || dnsEntry.until >= Date.now() ) {
+ return dnsEntry;
+ }
+ }
+ this.dnsSetCache(i);
+ }
+
+ dnsSetCache(i, hn, after) {
+ if ( i < 0 ) {
+ const j = this.dnsDict.get(hn);
+ if ( typeof j === 'number' ) {
+ this.dnsList[j] = after;
+ return;
+ }
+ i = this.dnsWritePtr++;
+ this.dnsWritePtr %= this.dnsMaxCount;
+ }
+ const before = this.dnsList[i];
+ if ( before ) {
+ this.dnsDict.delete(before.hn);
+ }
+ if ( after ) {
+ this.dnsDict.set(hn, i);
+ this.dnsList[i] = after;
+ } else {
+ if ( hn ) { this.dnsDict.delete(hn); }
+ this.dnsList[i] = null;
+ }
+ }
+
+ dnsShouldResolve(hn) {
+ if ( this.dnsResolveEnabled === false ) { return false; }
+ if ( hn === '' ) { return false; }
+ const c0 = hn.charCodeAt(0);
+ if ( c0 === 0x5B /* [ */ ) { return false; }
+ if ( c0 > 0x39 /* 9 */ ) { return true; }
+ return reIPv4.test(hn) === false;
+ }
+
+ dnsResolve(hn, details) {
+ const promise = dnsAPI.resolve(hn, [ 'canonical_name' ]).then(
+ rec => this.dnsToCache(hn, rec, details),
+ ( ) => this.dnsToCache(hn)
+ );
+ this.dnsDict.set(hn, promise);
+ return promise;
+ }
+
+ cnameFromRecord(hn, record, details) {
+ const cn = record.canonicalName;
+ if ( cn === undefined ) { return; }
+ if ( cn === hn ) { return; }
+ if ( this.cnameIgnore1stParty ) {
+ if ( domainFromHostname(cn) === domainFromHostname(hn) ) { return; }
+ }
+ if ( this.cnameIgnoreList !== null ) {
+ if ( this.cnameIgnoreList.test(cn) === false ) { return; }
+ }
+ if ( this.cnameIgnoreRootDocument ) {
+ const origin = hostnameFromNetworkURL(details.documentUrl || details.url);
+ if ( hn === origin ) { return; }
+ }
+ return cn;
+ }
+
+ uncloakURL(hn, dnsEntry, details) {
+ const hnBeg = details.url.indexOf(hn);
+ if ( hnBeg === -1 ) { return; }
+ const oldURL = details.url;
+ const newURL = oldURL.slice(0, hnBeg) + dnsEntry.cname;
+ const hnEnd = hnBeg + hn.length;
+ if ( this.cnameReplayFullURL ) {
+ return newURL + oldURL.slice(hnEnd);
+ }
+ const pathBeg = oldURL.indexOf('/', hnEnd);
+ if ( pathBeg !== -1 ) {
+ return newURL + oldURL.slice(hnEnd, pathBeg + 1);
+ }
+ return newURL;
+ }
+
+ ipFromRecord(record) {
+ const { addresses } = record;
+ if ( Array.isArray(addresses) === false ) { return; }
+ if ( addresses.length === 0 ) { return; }
+ return addresses.join('\n');
+ }
+
+ 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 = (( ) => {
+ const parts = [
+ '(',
+ function(details) {
+ if ( self.uBO_scriptletsInjected !== undefined ) { return; }
+ const doc = document;
+ const { location } = doc;
+ if ( location === null ) { return; }
+ const { hostname } = location;
+ if ( hostname !== '' && details.hostname !== hostname ) { return; }
+ // Use a page world sentinel to verify that execution was
+ // successful
+ const { sentinel } = details;
+ let script;
+ try {
+ const code = [
+ `self['${sentinel}'] = true;`,
+ details.scriptlets,
+ ].join('\n');
+ script = doc.createElement('script');
+ script.appendChild(doc.createTextNode(code));
+ (doc.head || doc.documentElement).appendChild(script);
+ } catch {
+ }
+ if ( script ) {
+ script.remove();
+ script.textContent = '';
+ script = undefined;
+ }
+ if ( self.wrappedJSObject[sentinel] ) {
+ delete self.wrappedJSObject[sentinel];
+ self.uBO_scriptletsInjected = details.filters;
+ return 0;
+ }
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/235
+ // Fall back to blob injection if execution through direct
+ // injection failed
+ let 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 {
+ }
+ if ( url ) {
+ if ( script ) { script.remove(); }
+ self.URL.revokeObjectURL(url);
+ }
+ return 0;
+ }.toString(),
+ ')(',
+ 'json-slot',
+ ');',
+ ];
+ const jsonSlot = parts.indexOf('json-slot');
+ return (hostname, details) => {
+ parts[jsonSlot] = JSON.stringify({
+ hostname,
+ scriptlets: details.mainWorld,
+ filters: details.filters,
+ sentinel: vAPI.generateSecret(3),
+ });
+ return parts.join('');
+ };
+})();
+
+/******************************************************************************/