/******************************************************************************* 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 ( typeof self.uBO_scriptletsInjected === 'string' ) { 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 (ex) { } 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 (ex) { } 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(''); }; })(); /******************************************************************************/