Rewrite cname uncloaking code to account for new ipaddress= option · gorhill/uBlock@6acf97b (original) (raw)

`@@ -26,8 +26,10 @@ import {

`

26

26

``

27

27

`/******************************************************************************/

`

28

28

``

29

``

`-

// 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+$/

`

31

33

``

32

34

`// Related issues:

`

33

35

`// - https://github.com/gorhill/uBlock/issues/1327

`

`@@ -40,21 +42,24 @@ vAPI.Net = class extends vAPI.Net {

`

40

42

`constructor() {

`

41

43

`super();

`

42

44

`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;

`

45

52

`this.cnameIgnoreList = null;

`

46

53

`this.cnameIgnore1stParty = true;

`

47

54

`this.cnameIgnoreExceptions = true;

`

48

55

`this.cnameIgnoreRootDocument = true;

`

49

``

`-

this.cnameMaxTTL = 120;

`

50

56

`this.cnameReplayFullURL = false;

`

51

``

`-

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

`

52

57

`}

`

``

58

+

53

59

`setOptions(options) {

`

54

60

`super.setOptions(options);

`

55

61

`if ( 'cnameUncloakEnabled' in options ) {

`

56

``

`-

cnameUncloakEnabled =

`

57

``

`-

this.canUncloakCnames &&

`

``

62

`+

this.cnameUncloakEnabled =

`

58

63

`options.cnameUncloakEnabled !== false;

`

59

64

`}

`

60

65

`if ( 'cnameIgnoreList' in options ) {

`

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

`

73

78

`this.cnameIgnoreRootDocument =

`

74

79

`options.cnameIgnoreRootDocument !== false;

`

75

80

`}

`

76

``

`-

if ( 'cnameMaxTTL' in options ) {

`

77

``

`-

this.cnameMaxTTL = options.cnameMaxTTL || 120;

`

78

``

`-

}

`

79

81

`if ( 'cnameReplayFullURL' in options ) {

`

80

82

`this.cnameReplayFullURL = options.cnameReplayFullURL === true;

`

81

83

`}

`

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();

`

84

86

`}

`

``

87

+

85

88

`normalizeDetails(details) {

`

86

89

`const type = details.type;

`

87

90

``

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

`

104

107

`}

`

105

108

`}

`

106

109

`}

`

``

110

+

107

111

`denormalizeTypes(types) {

`

108

112

`if ( types.length === 0 ) {

`

109

113

`return Array.from(this.validTypes);

`

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

`

122

126

`}

`

123

127

`return Array.from(out);

`

124

128

`}

`

``

129

+

125

130

`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;

`

181

135

`}

`

``

136

+

182

137

`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' ) {

`

189

139

`return null;

`

190

140

`}

`

191

``

`-

if ( list === '*' ) {

`

192

``

`-

return /^./;

`

193

``

`-

}

`

``

141

`+

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

`

194

142

`return new RegExp(

`

195

143

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

`

196

144

`list.trim()

`

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

`

200

148

`')$'

`

201

149

`);

`

202

150

`}

`

``

151

+

203

152

`onBeforeSuspendableRequest(details) {

`

``

153

`+

const hn = hostnameFromNetworkURL(details.url);

`

``

154

`+

const dnsEntry = this.dnsFromCache(hn);

`

``

155

`+

if ( dnsEntry?.ip ) {

`

``

156

`+

details.ip = dnsEntry.ip;

`

``

157

`+

}

`

204

158

`const r = super.onBeforeSuspendableRequest(details);

`

205

``

`-

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

`

206

159

`if ( r !== undefined ) {

`

207

160

`if (

`

208

161

`r.cancel === true ||

`

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

`

212

165

`return r;

`

213

166

`}

`

214

167

`}

`

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

`+

}

`

219

172

`}

`

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;

`

231

191

`}

`

``

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)

`

232

246

`);

`

``

247

`+

return (this.dnsList[i] = promise);

`

233

248

`}

`

``

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

+

234

290

`suspendOneRequest(details) {

`

235

291

`const pending = {

`

236

292

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

`

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

`

243

299

`this.pendingRequests.push(pending);

`

244

300

`return pending.promise;

`

245

301

`}

`

``

302

+

246

303

`unsuspendAllRequests(discard = false) {

`

247

304

`const pendingRequests = this.pendingRequests;

`

248

305

`this.pendingRequests = [];

`

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

`

254

311

`);

`

255

312

`}

`

256

313

`}

`

``

314

+

257

315

`static canSuspend() {

`

258

316

`return true;

`

259

317

`}

`