Rewrite cname uncloaking code to account for new `ipaddress=` option · gorhill/uBlock@6acf97b
@@ -26,8 +26,10 @@ import {
26262727/******************************************************************************/
282829-// Canonical name-uncloaking feature.
30-let cnameUncloakEnabled = browser.dns instanceof Object;
29+const dnsAPI = browser.dns;
30+31+const isPromise = o => o instanceof Promise;
32+const reIPv4 = /^\d+\.\d+\.\d+\.\d+$/
31333234// Related issues:
3335// - https://github.com/gorhill/uBlock/issues/1327
@@ -40,21 +42,24 @@ vAPI.Net = class extends vAPI.Net {
4042constructor() {
4143super();
4244this.pendingRequests = [];
43-this.canUncloakCnames = browser.dns instanceof Object;
44-this.cnames = new Map([ [ '', null ] ]);
45+this.dnsList = []; // ring buffer
46+this.dnsWritePtr = 0; // next write pointer in ring buffer
47+this.dnsMaxCount = 256; // max size of ring buffer
48+this.dnsDict = new Map(); // hn to index in ring buffer
49+this.dnsEntryTTL = 60000; // delay after which an entry is obsolete
50+this.canUncloakCnames = true;
51+this.cnameUncloakEnabled = true;
4552this.cnameIgnoreList = null;
4653this.cnameIgnore1stParty = true;
4754this.cnameIgnoreExceptions = true;
4855this.cnameIgnoreRootDocument = true;
49-this.cnameMaxTTL = 120;
5056this.cnameReplayFullURL = false;
51-this.cnameFlushTime = Date.now() + this.cnameMaxTTL * 60000;
5257}
58+5359setOptions(options) {
5460super.setOptions(options);
5561if ( 'cnameUncloakEnabled' in options ) {
56-cnameUncloakEnabled =
57-this.canUncloakCnames &&
62+this.cnameUncloakEnabled =
5863options.cnameUncloakEnabled !== false;
5964}
6065if ( 'cnameIgnoreList' in options ) {
@@ -73,15 +78,13 @@ vAPI.Net = class extends vAPI.Net {
7378this.cnameIgnoreRootDocument =
7479options.cnameIgnoreRootDocument !== false;
7580}
76-if ( 'cnameMaxTTL' in options ) {
77-this.cnameMaxTTL = options.cnameMaxTTL || 120;
78-}
7981if ( 'cnameReplayFullURL' in options ) {
8082this.cnameReplayFullURL = options.cnameReplayFullURL === true;
8183}
82-this.cnames.clear(); this.cnames.set('', null);
83-this.cnameFlushTime = Date.now() + this.cnameMaxTTL * 60000;
84+this.dnsList.fill(null);
85+this.dnsDict.clear();
8486}
87+8588normalizeDetails(details) {
8689const type = details.type;
8790@@ -104,6 +107,7 @@ vAPI.Net = class extends vAPI.Net {
104107}
105108}
106109}
110+107111denormalizeTypes(types) {
108112if ( types.length === 0 ) {
109113return Array.from(this.validTypes);
@@ -122,75 +126,19 @@ vAPI.Net = class extends vAPI.Net {
122126}
123127return Array.from(out);
124128}
129+125130canonicalNameFromHostname(hn) {
126-const cnRecord = this.cnames.get(hn);
127-if ( cnRecord !== undefined && cnRecord !== null ) {
128-return cnRecord.cname;
129-}
130-}
131-processCanonicalName(hn, cnRecord, details) {
132-if ( cnRecord === null ) { return; }
133-if ( cnRecord.isRootDocument ) { return; }
134-const hnBeg = details.url.indexOf(hn);
135-if ( hnBeg === -1 ) { return; }
136-const oldURL = details.url;
137-let newURL = oldURL.slice(0, hnBeg) + cnRecord.cname;
138-const hnEnd = hnBeg + hn.length;
139-if ( this.cnameReplayFullURL ) {
140-newURL += oldURL.slice(hnEnd);
141-} else {
142-const pathBeg = oldURL.indexOf('/', hnEnd);
143-if ( pathBeg !== -1 ) {
144-newURL += oldURL.slice(hnEnd, pathBeg + 1);
145-}
146-}
147-details.url = newURL;
148-details.aliasURL = oldURL;
149-return super.onBeforeSuspendableRequest(details);
150-}
151-recordCanonicalName(hn, record, isRootDocument) {
152-if ( (this.cnames.size & 0b111111) === 0 ) {
153-const now = Date.now();
154-if ( now >= this.cnameFlushTime ) {
155-this.cnames.clear(); this.cnames.set('', null);
156-this.cnameFlushTime = now + this.cnameMaxTTL * 60000;
157-}
158-}
159-let cname =
160-typeof record.canonicalName === 'string' &&
161-record.canonicalName !== hn
162- ? record.canonicalName
163- : '';
164-if (
165-cname !== '' &&
166-this.cnameIgnore1stParty &&
167-domainFromHostname(cname) === domainFromHostname(hn)
168-) {
169-cname = '';
170-}
171-if (
172-cname !== '' &&
173-this.cnameIgnoreList !== null &&
174-this.cnameIgnoreList.test(cname)
175-) {
176-cname = '';
177-}
178-const cnRecord = cname !== '' ? { cname, isRootDocument } : null;
179-this.cnames.set(hn, cnRecord);
180-return cnRecord;
131+if ( hn === '' ) { return; }
132+const dnsEntry = this.dnsFromCache(hn);
133+if ( isPromise(dnsEntry) ) { return; }
134+return dnsEntry?.cname;
181135}
136+182137regexFromStrList(list) {
183-if (
184-typeof list !== 'string' ||
185-list.length === 0 ||
186-list === 'unset' ||
187-browser.dns instanceof Object === false
188-) {
138+if ( typeof list !== 'string' || list.length === 0 || list === 'unset' ) {
189139return null;
190140}
191-if ( list === '*' ) {
192-return /^./;
193-}
141+if ( list === '*' ) { return /^./; }
194142return new RegExp(
195143'(?:^|\\.)(?:' +
196144list.trim()
@@ -200,9 +148,14 @@ vAPI.Net = class extends vAPI.Net {
200148')$'
201149);
202150}
151+203152onBeforeSuspendableRequest(details) {
153+const hn = hostnameFromNetworkURL(details.url);
154+const dnsEntry = this.dnsFromCache(hn);
155+if ( dnsEntry?.ip ) {
156+details.ip = dnsEntry.ip;
157+}
204158const r = super.onBeforeSuspendableRequest(details);
205-if ( cnameUncloakEnabled === false ) { return r; }
206159if ( r !== undefined ) {
207160if (
208161r.cancel === true ||
@@ -212,25 +165,128 @@ vAPI.Net = class extends vAPI.Net {
212165return r;
213166}
214167}
215-const hn = hostnameFromNetworkURL(details.url);
216-const cnRecord = this.cnames.get(hn);
217-if ( cnRecord !== undefined ) {
218-return this.processCanonicalName(hn, cnRecord, details);
168+if ( dnsEntry !== undefined ) {
169+ if ( isPromise(dnsEntry) === false ) {
170+ return this.onAfterDNSResolution(hn, details, dnsEntry);
171+}
219172}
220-if ( details.proxyInfo && details.proxyInfo.proxyDNS ) { return; }
221-const documentUrl = details.documentUrl || details.url;
222-const isRootDocument = this.cnameIgnoreRootDocument &&
223-hn === hostnameFromNetworkURL(documentUrl);
224-return browser.dns.resolve(hn, [ 'canonical_name' ]).then(
225-rec => {
226-const cnRecord = this.recordCanonicalName(hn, rec, isRootDocument);
227-return this.processCanonicalName(hn, cnRecord, details);
228-},
229-( ) => {
230-this.cnames.set(hn, null);
173+if ( this.dnsShouldResolve(hn) === false ) { return; }
174+if ( details.proxyInfo?.proxyDNS ) { return; }
175+const promise = dnsEntry || this.dnsResolve(hn, details);
176+return promise.then(( ) => this.onAfterDNSResolution(hn, details));
177+}
178+179+onAfterDNSResolution(hn, details, dnsEntry) {
180+if ( dnsEntry === undefined ) {
181+dnsEntry = this.dnsFromCache(hn);
182+if ( dnsEntry === undefined || isPromise(dnsEntry) ) { return; }
183+}
184+let proceed = false;
185+if ( dnsEntry.cname && this.cnameUncloakEnabled ) {
186+const newURL = this.uncloakURL(hn, dnsEntry, details);
187+if ( newURL ) {
188+details.aliasURL = details.url;
189+details.url = newURL;
190+proceed = true;
231191}
192+}
193+if ( dnsEntry.ip && details.ip !== dnsEntry.ip ) {
194+details.ip = dnsEntry.ip
195+proceed = true;
196+}
197+if ( proceed === false ) { return; }
198+// Must call method on base class
199+return super.onBeforeSuspendableRequest(details);
200+}
201+202+dnsToCache(hn, record, details) {
203+const i = this.dnsDict.get(hn);
204+if ( i === undefined ) { return; }
205+const dnsEntry = {
206+ hn,
207+until: Date.now() + this.dnsEntryTTL,
208+};
209+if ( record ) {
210+const cname = this.cnameFromRecord(hn, record, details);
211+if ( cname ) { dnsEntry.cname = cname; }
212+const ip = this.ipFromRecord(record);
213+if ( ip ) { dnsEntry.ip = ip; }
214+}
215+this.dnsList[i] = dnsEntry;
216+return dnsEntry;
217+}
218+219+dnsFromCache(hn) {
220+const i = this.dnsDict.get(hn);
221+if ( i === undefined ) { return; }
222+const dnsEntry = this.dnsList[i];
223+if ( dnsEntry === null ) { return; }
224+if ( isPromise(dnsEntry) ) { return dnsEntry; }
225+if ( dnsEntry.hn !== hn ) { return; }
226+if ( dnsEntry.until >= Date.now() ) { return dnsEntry; }
227+this.dnsList[i] = null;
228+this.dnsDict.delete(hn)
229+}
230+231+dnsShouldResolve(hn) {
232+if ( hn === '' ) { return false; }
233+const c0 = hn.charCodeAt(0);
234+if ( c0 === 0x5B /* [ */ ) { return false; }
235+if ( c0 > 0x39 /* 9 */ ) { return true; }
236+return reIPv4.test(hn) === false;
237+}
238+239+dnsResolve(hn, details) {
240+const i = this.dnsWritePtr++;
241+this.dnsWritePtr %= this.dnsMaxCount;
242+this.dnsDict.set(hn, i);
243+const promise = dnsAPI.resolve(hn, [ 'canonical_name' ]).then(
244+rec => this.dnsToCache(hn, rec, details),
245+( ) => this.dnsToCache(hn)
232246);
247+return (this.dnsList[i] = promise);
233248}
249+250+cnameFromRecord(hn, record, details) {
251+const cn = record.canonicalName;
252+if ( cn === undefined ) { return; }
253+if ( cn === hn ) { return; }
254+if ( this.cnameIgnore1stParty ) {
255+if ( domainFromHostname(cn) === domainFromHostname(hn) ) { return; }
256+}
257+if ( this.cnameIgnoreList !== null ) {
258+if ( this.cnameIgnoreList.test(cn) === false ) { return; }
259+}
260+if ( this.cnameIgnoreRootDocument ) {
261+const origin = hostnameFromNetworkURL(details.documentUrl || details.url);
262+if ( hn === origin ) { return; }
263+}
264+return cn;
265+}
266+267+uncloakURL(hn, dnsEntry, details) {
268+const hnBeg = details.url.indexOf(hn);
269+if ( hnBeg === -1 ) { return; }
270+const oldURL = details.url;
271+const newURL = oldURL.slice(0, hnBeg) + dnsEntry.cname;
272+const hnEnd = hnBeg + hn.length;
273+if ( this.cnameReplayFullURL ) {
274+return newURL + oldURL.slice(hnEnd);
275+}
276+const pathBeg = oldURL.indexOf('/', hnEnd);
277+if ( pathBeg !== -1 ) {
278+return newURL + oldURL.slice(hnEnd, pathBeg + 1);
279+}
280+return newURL;
281+}
282+283+ipFromRecord(record) {
284+const { addresses } = record;
285+if ( Array.isArray(addresses) === false ) { return; }
286+if ( addresses.length === 0 ) { return; }
287+return addresses[0];
288+}
289+234290suspendOneRequest(details) {
235291const pending = {
236292details: Object.assign({}, details),
@@ -243,6 +299,7 @@ vAPI.Net = class extends vAPI.Net {
243299this.pendingRequests.push(pending);
244300return pending.promise;
245301}
302+246303unsuspendAllRequests(discard = false) {
247304const pendingRequests = this.pendingRequests;
248305this.pendingRequests = [];
@@ -254,6 +311,7 @@ vAPI.Net = class extends vAPI.Net {
254311);
255312}
256313}
314+257315static canSuspend() {
258316return true;
259317}