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 {

4042

constructor() {

4143

super();

4244

this.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;

4552

this.cnameIgnoreList = null;

4653

this.cnameIgnore1stParty = true;

4754

this.cnameIgnoreExceptions = true;

4855

this.cnameIgnoreRootDocument = true;

49-

this.cnameMaxTTL = 120;

5056

this.cnameReplayFullURL = false;

51-

this.cnameFlushTime = Date.now() + this.cnameMaxTTL * 60000;

5257

}

58+5359

setOptions(options) {

5460

super.setOptions(options);

5561

if ( 'cnameUncloakEnabled' in options ) {

56-

cnameUncloakEnabled =

57-

this.canUncloakCnames &&

62+

this.cnameUncloakEnabled =

5863

options.cnameUncloakEnabled !== false;

5964

}

6065

if ( 'cnameIgnoreList' in options ) {

@@ -73,15 +78,13 @@ vAPI.Net = class extends vAPI.Net {

7378

this.cnameIgnoreRootDocument =

7479

options.cnameIgnoreRootDocument !== false;

7580

}

76-

if ( 'cnameMaxTTL' in options ) {

77-

this.cnameMaxTTL = options.cnameMaxTTL || 120;

78-

}

7981

if ( 'cnameReplayFullURL' in options ) {

8082

this.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+8588

normalizeDetails(details) {

8689

const type = details.type;

8790

@@ -104,6 +107,7 @@ vAPI.Net = class extends vAPI.Net {

104107

}

105108

}

106109

}

110+107111

denormalizeTypes(types) {

108112

if ( types.length === 0 ) {

109113

return Array.from(this.validTypes);

@@ -122,75 +126,19 @@ vAPI.Net = class extends vAPI.Net {

122126

}

123127

return Array.from(out);

124128

}

129+125130

canonicalNameFromHostname(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+182137

regexFromStrList(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' ) {

189139

return null;

190140

}

191-

if ( list === '*' ) {

192-

return /^./;

193-

}

141+

if ( list === '*' ) { return /^./; }

194142

return new RegExp(

195143

'(?:^|\\.)(?:' +

196144

list.trim()

@@ -200,9 +148,14 @@ vAPI.Net = class extends vAPI.Net {

200148

')$'

201149

);

202150

}

151+203152

onBeforeSuspendableRequest(details) {

153+

const hn = hostnameFromNetworkURL(details.url);

154+

const dnsEntry = this.dnsFromCache(hn);

155+

if ( dnsEntry?.ip ) {

156+

details.ip = dnsEntry.ip;

157+

}

204158

const r = super.onBeforeSuspendableRequest(details);

205-

if ( cnameUncloakEnabled === false ) { return r; }

206159

if ( r !== undefined ) {

207160

if (

208161

r.cancel === true ||

@@ -212,25 +165,128 @@ vAPI.Net = class extends vAPI.Net {

212165

return 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+234290

suspendOneRequest(details) {

235291

const pending = {

236292

details: Object.assign({}, details),

@@ -243,6 +299,7 @@ vAPI.Net = class extends vAPI.Net {

243299

this.pendingRequests.push(pending);

244300

return pending.promise;

245301

}

302+246303

unsuspendAllRequests(discard = false) {

247304

const pendingRequests = this.pendingRequests;

248305

this.pendingRequests = [];

@@ -254,6 +311,7 @@ vAPI.Net = class extends vAPI.Net {

254311

);

255312

}

256313

}

314+257315

static canSuspend() {

258316

return true;

259317

}