@blamejs/core 0.6.13 → 0.6.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,469 @@
1
+ "use strict";
2
+
3
+ var dns = require("node:dns");
4
+ var net = require("node:net");
5
+ var https = require("node:https");
6
+ var tls = require("node:tls");
7
+ var dnsPromises = dns.promises;
8
+
9
+ var C = require("./constants");
10
+ var validateOpts = require("./validate-opts");
11
+ var lazyRequire = require("./lazy-require");
12
+ var { defineClass } = require("./framework-error");
13
+
14
+ var DnsError = defineClass("DnsError", { alwaysPermanent: false });
15
+
16
+ var observability = lazyRequire(function () { return require("./observability"); });
17
+
18
+ var STATE = {
19
+ servers: null,
20
+ resultOrder: null,
21
+ family: 0,
22
+ lookupTimeoutMs: 0,
23
+ cacheTtlMs: 0,
24
+ cacheNegativeTtlMs: 0,
25
+ doh: null,
26
+ dot: null,
27
+ };
28
+
29
+ var POSITIVE_CACHE = new Map();
30
+ var NEGATIVE_CACHE = new Map();
31
+
32
+ function _now() { return Date.now(); }
33
+
34
+ function _cacheGet(host, family) {
35
+ var key = host + "/" + family;
36
+ var pos = POSITIVE_CACHE.get(key);
37
+ if (pos && pos.expiresAt > _now()) return { hit: true, value: pos.value };
38
+ if (pos) POSITIVE_CACHE.delete(key);
39
+ var neg = NEGATIVE_CACHE.get(key);
40
+ if (neg && neg.expiresAt > _now()) return { hit: true, error: neg.error };
41
+ if (neg) NEGATIVE_CACHE.delete(key);
42
+ return { hit: false };
43
+ }
44
+
45
+ function _cachePutPositive(host, family, value) {
46
+ if (STATE.cacheTtlMs <= 0) return;
47
+ POSITIVE_CACHE.set(host + "/" + family, {
48
+ value: value,
49
+ expiresAt: _now() + STATE.cacheTtlMs,
50
+ });
51
+ }
52
+
53
+ function _cachePutNegative(host, family, error) {
54
+ if (STATE.cacheTtlMs <= 0) return;
55
+ var ttl = STATE.cacheNegativeTtlMs > 0 ? STATE.cacheNegativeTtlMs : Math.min(STATE.cacheTtlMs, C.TIME.seconds(30));
56
+ NEGATIVE_CACHE.set(host + "/" + family, {
57
+ error: error,
58
+ expiresAt: _now() + ttl,
59
+ });
60
+ }
61
+
62
+ function _clearCache() {
63
+ POSITIVE_CACHE.clear();
64
+ NEGATIVE_CACHE.clear();
65
+ }
66
+
67
+ function setServers(serverList) {
68
+ if (!Array.isArray(serverList) || serverList.length === 0) {
69
+ throw new DnsError("dns/bad-servers", "dns.setServers: expected non-empty array of resolver IPs");
70
+ }
71
+ for (var i = 0; i < serverList.length; i++) {
72
+ var s = serverList[i];
73
+ if (typeof s !== "string" || s.length === 0) {
74
+ throw new DnsError("dns/bad-server", "dns.setServers[" + i + "]: expected non-empty string, got " + typeof s);
75
+ }
76
+ }
77
+ STATE.servers = serverList.slice();
78
+ try { dns.setServers(serverList); } catch (e) {
79
+ throw new DnsError("dns/setservers-failed", "dns.setServers failed: " + e.message);
80
+ }
81
+ _clearCache();
82
+ _emitObs("network.dns.servers.set", { count: serverList.length });
83
+ }
84
+
85
+ function getServers() {
86
+ if (STATE.servers) return STATE.servers.slice();
87
+ try { return dns.getServers(); } catch (_e) { return []; }
88
+ }
89
+
90
+ function setResultOrder(order) {
91
+ if (order !== "ipv4first" && order !== "verbatim" && order !== "ipv6first") {
92
+ throw new DnsError("dns/bad-result-order",
93
+ "dns.setResultOrder: expected 'ipv4first' | 'verbatim' | 'ipv6first', got " + JSON.stringify(order));
94
+ }
95
+ STATE.resultOrder = order;
96
+ if (order === "ipv6first") {
97
+ try { dns.setDefaultResultOrder("verbatim"); } catch (_e) {}
98
+ } else {
99
+ try { dns.setDefaultResultOrder(order); } catch (_e) {}
100
+ }
101
+ _clearCache();
102
+ _emitObs("network.dns.result_order.set", { order: order });
103
+ }
104
+
105
+ function setFamily(fam) {
106
+ if (fam !== 0 && fam !== 4 && fam !== 6) {
107
+ throw new DnsError("dns/bad-family", "dns.setFamily: expected 0 | 4 | 6, got " + JSON.stringify(fam));
108
+ }
109
+ STATE.family = fam;
110
+ _clearCache();
111
+ }
112
+
113
+ function setLookupTimeoutMs(ms) {
114
+ if (typeof ms !== "number" || !isFinite(ms) || ms < 0) {
115
+ throw new DnsError("dns/bad-timeout",
116
+ "dns.setLookupTimeoutMs: expected non-negative finite number, got " + JSON.stringify(ms));
117
+ }
118
+ STATE.lookupTimeoutMs = ms;
119
+ }
120
+
121
+ function setCacheTtlMs(ms, negativeMs) {
122
+ if (typeof ms !== "number" || !isFinite(ms) || ms < 0) {
123
+ throw new DnsError("dns/bad-cache-ttl",
124
+ "dns.setCacheTtlMs: expected non-negative finite number, got " + JSON.stringify(ms));
125
+ }
126
+ STATE.cacheTtlMs = ms;
127
+ if (negativeMs !== undefined) {
128
+ if (typeof negativeMs !== "number" || !isFinite(negativeMs) || negativeMs < 0) {
129
+ throw new DnsError("dns/bad-cache-ttl",
130
+ "dns.setCacheTtlMs negativeMs: expected non-negative finite number, got " + JSON.stringify(negativeMs));
131
+ }
132
+ STATE.cacheNegativeTtlMs = negativeMs;
133
+ }
134
+ if (ms === 0) _clearCache();
135
+ }
136
+
137
+ function useDnsOverHttps(opts) {
138
+ opts = opts || {};
139
+ validateOpts(opts, ["provider", "url"], "dns.useDnsOverHttps");
140
+ var url = opts.url;
141
+ if (!url && opts.provider) {
142
+ var p = String(opts.provider).toLowerCase();
143
+ if (p === "cloudflare") url = "https://cloudflare-dns.com/dns-query";
144
+ else if (p === "google") url = "https://dns.google/dns-query";
145
+ else if (p === "quad9") url = "https://dns.quad9.net/dns-query";
146
+ else throw new DnsError("dns/bad-doh-provider", "dns.useDnsOverHttps: unknown provider '" + opts.provider + "'");
147
+ }
148
+ if (typeof url !== "string" || url.indexOf("https://") !== 0) {
149
+ throw new DnsError("dns/bad-doh-url",
150
+ "dns.useDnsOverHttps: url must be an https:// string, got " + JSON.stringify(url));
151
+ }
152
+ STATE.doh = { url: url };
153
+ _clearCache();
154
+ _emitObs("network.dns.doh.set", { url: url });
155
+ }
156
+
157
+ function useDnsOverTls(opts) {
158
+ opts = opts || {};
159
+ validateOpts(opts, ["host", "port", "servername"], "dns.useDnsOverTls");
160
+ if (typeof opts.host !== "string" || opts.host.length === 0) {
161
+ throw new DnsError("dns/bad-dot-host", "dns.useDnsOverTls: host required");
162
+ }
163
+ STATE.dot = {
164
+ host: opts.host,
165
+ port: opts.port || 853,
166
+ servername: opts.servername || opts.host,
167
+ };
168
+ _clearCache();
169
+ _emitObs("network.dns.dot.set", { host: STATE.dot.host, port: STATE.dot.port });
170
+ }
171
+
172
+ function _withTimeout(promise, ms, host) {
173
+ if (ms <= 0) return promise;
174
+ return new Promise(function (resolve, reject) {
175
+ var timer = setTimeout(function () {
176
+ reject(new DnsError("dns/lookup-timeout", "dns lookup of '" + host + "' exceeded " + ms + "ms"));
177
+ }, ms);
178
+ timer.unref && timer.unref();
179
+ promise.then(
180
+ function (v) { clearTimeout(timer); resolve(v); },
181
+ function (e) { clearTimeout(timer); reject(e); }
182
+ );
183
+ });
184
+ }
185
+
186
+ function _encodeDnsQuery(host, qtype) {
187
+ var parts = host.split(".").filter(Boolean);
188
+ var nameLen = 1;
189
+ for (var i = 0; i < parts.length; i++) nameLen += 1 + Buffer.byteLength(parts[i], "ascii");
190
+ var buf = Buffer.alloc(12 + nameLen + 4);
191
+ var id = (Math.random() * 0xffff) | 0;
192
+ buf.writeUInt16BE(id, 0);
193
+ buf.writeUInt16BE(0x0100, 2);
194
+ buf.writeUInt16BE(1, 4);
195
+ var off = 12;
196
+ for (var p = 0; p < parts.length; p++) {
197
+ var s = parts[p];
198
+ buf.writeUInt8(Buffer.byteLength(s, "ascii"), off++);
199
+ off += buf.write(s, off, "ascii");
200
+ }
201
+ buf.writeUInt8(0, off++);
202
+ buf.writeUInt16BE(qtype, off); off += 2;
203
+ buf.writeUInt16BE(1, off);
204
+ return { buf: buf, id: id };
205
+ }
206
+
207
+ function _decodeDnsAnswer(buf, qtype) {
208
+ if (!Buffer.isBuffer(buf) || buf.length < 12) throw new DnsError("dns/bad-reply", "dns reply truncated");
209
+ var rcode = buf.readUInt8(3) & 0x0f;
210
+ if (rcode !== 0) throw new DnsError("dns/no-result", "dns reply rcode " + rcode);
211
+ var qdcount = buf.readUInt16BE(4);
212
+ var ancount = buf.readUInt16BE(6);
213
+ var off = 12;
214
+ for (var q = 0; q < qdcount; q++) {
215
+ while (off < buf.length && buf[off] !== 0) {
216
+ if ((buf[off] & 0xc0) === 0xc0) { off += 2; break; }
217
+ off += buf[off] + 1;
218
+ }
219
+ if (buf[off] === 0) off++;
220
+ off += 4;
221
+ }
222
+ var addrs = [];
223
+ for (var a = 0; a < ancount; a++) {
224
+ while (off < buf.length && buf[off] !== 0) {
225
+ if ((buf[off] & 0xc0) === 0xc0) { off += 2; break; }
226
+ off += buf[off] + 1;
227
+ }
228
+ if (buf[off] === 0) off++;
229
+ var rtype = buf.readUInt16BE(off); off += 2;
230
+ off += 2;
231
+ off += 4;
232
+ var rdlen = buf.readUInt16BE(off); off += 2;
233
+ if (rtype === qtype && qtype === 1 && rdlen === 4) {
234
+ addrs.push(buf[off] + "." + buf[off + 1] + "." + buf[off + 2] + "." + buf[off + 3]);
235
+ } else if (rtype === qtype && qtype === 28 && rdlen === 16) {
236
+ var groups = [];
237
+ for (var g = 0; g < 8; g++) {
238
+ groups.push(buf.readUInt16BE(off + g * 2).toString(16));
239
+ }
240
+ addrs.push(groups.join(":"));
241
+ }
242
+ off += rdlen;
243
+ }
244
+ return addrs;
245
+ }
246
+
247
+ async function _dohLookup(host, family) {
248
+ var qtype = family === 6 ? 28 : 1;
249
+ var enc = _encodeDnsQuery(host, qtype);
250
+ var b64 = enc.buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
251
+ var url = STATE.doh.url + (STATE.doh.url.indexOf("?") === -1 ? "?" : "&") + "dns=" + b64;
252
+ var u = new URL(url);
253
+ return new Promise(function (resolve, reject) {
254
+ var req = https.request({
255
+ hostname: u.hostname,
256
+ port: u.port || 443,
257
+ path: u.pathname + u.search,
258
+ method: "GET",
259
+ headers: { "accept": "application/dns-message" },
260
+ minVersion: "TLSv1.3",
261
+ ecdhCurve: C.TLS_GROUP_CURVE_STR,
262
+ }, function (res) {
263
+ var chunks = [];
264
+ res.on("data", function (c) { chunks.push(c); });
265
+ res.on("end", function () {
266
+ try {
267
+ var body = Buffer.concat(chunks);
268
+ if (res.statusCode !== 200) {
269
+ reject(new DnsError("dns/doh-http", "DoH HTTP " + res.statusCode + " for " + host));
270
+ return;
271
+ }
272
+ resolve(_decodeDnsAnswer(body, qtype));
273
+ } catch (e) { reject(e); }
274
+ });
275
+ });
276
+ req.on("error", function (e) { reject(new DnsError("dns/doh-failed", "DoH request failed: " + e.message)); });
277
+ req.end();
278
+ });
279
+ }
280
+
281
+ async function _dotLookup(host, family) {
282
+ var qtype = family === 6 ? 28 : 1;
283
+ var enc = _encodeDnsQuery(host, qtype);
284
+ return new Promise(function (resolve, reject) {
285
+ var sock = tls.connect({
286
+ host: STATE.dot.host,
287
+ port: STATE.dot.port,
288
+ servername: STATE.dot.servername,
289
+ minVersion: "TLSv1.3",
290
+ ecdhCurve: C.TLS_GROUP_CURVE_STR,
291
+ });
292
+ var lenBuf = Buffer.alloc(2);
293
+ lenBuf.writeUInt16BE(enc.buf.length, 0);
294
+ var got = [];
295
+ var expectLen = -1;
296
+ sock.on("secureConnect", function () {
297
+ sock.write(lenBuf);
298
+ sock.write(enc.buf);
299
+ });
300
+ sock.on("data", function (chunk) {
301
+ got.push(chunk);
302
+ var all = Buffer.concat(got);
303
+ if (expectLen === -1 && all.length >= 2) {
304
+ expectLen = all.readUInt16BE(0);
305
+ }
306
+ if (expectLen >= 0 && all.length >= expectLen + 2) {
307
+ try {
308
+ var ans = _decodeDnsAnswer(all.slice(2, 2 + expectLen), qtype);
309
+ sock.destroy();
310
+ resolve(ans);
311
+ } catch (e) { sock.destroy(); reject(e); }
312
+ }
313
+ });
314
+ sock.on("error", function (e) { reject(new DnsError("dns/dot-failed", "DoT failed: " + e.message)); });
315
+ });
316
+ }
317
+
318
+ function _orderAddrs(addrs) {
319
+ if (STATE.resultOrder === "ipv6first") {
320
+ addrs.sort(function (a, b) { return (b.family || 0) - (a.family || 0); });
321
+ } else if (STATE.resultOrder === "ipv4first") {
322
+ addrs.sort(function (a, b) { return (a.family || 0) - (b.family || 0); });
323
+ }
324
+ return addrs;
325
+ }
326
+
327
+ async function _dualStack(queryFn, host, family) {
328
+ if (family === 4 || family === 6) {
329
+ return _withTimeout(queryFn(host, family), STATE.lookupTimeoutMs, host);
330
+ }
331
+ var first = STATE.resultOrder === "ipv6first" ? 6 : 4;
332
+ var second = first === 4 ? 6 : 4;
333
+ var firstResult = await _withTimeout(queryFn(host, first), STATE.lookupTimeoutMs, host).catch(function () { return []; });
334
+ var secondResult = await _withTimeout(queryFn(host, second), STATE.lookupTimeoutMs, host).catch(function () { return []; });
335
+ return (firstResult || []).concat(secondResult || []);
336
+ }
337
+
338
+ async function lookup(host, opts) {
339
+ opts = opts || {};
340
+ validateOpts(opts, ["family", "all"], "dns.lookup");
341
+ var family = opts.family !== undefined ? opts.family : STATE.family;
342
+ if (net.isIP(host)) {
343
+ var fam = net.isIP(host);
344
+ var literal = { address: host, family: fam };
345
+ return opts.all ? [literal] : literal;
346
+ }
347
+ var cacheKey = family || 0;
348
+ var cached = _cacheGet(host, cacheKey);
349
+ if (cached.hit) {
350
+ if (cached.error) throw cached.error;
351
+ return opts.all ? cached.value : cached.value[0];
352
+ }
353
+ _emitObs("network.dns.lookup.requested", { family: cacheKey });
354
+ var startMs = _now();
355
+ try {
356
+ var addrs;
357
+ if (STATE.doh) {
358
+ addrs = await _dualStack(_dohLookup, host, family);
359
+ } else if (STATE.dot) {
360
+ addrs = await _dualStack(_dotLookup, host, family);
361
+ } else {
362
+ var nodeOpts = { all: true };
363
+ if (family === 4 || family === 6) nodeOpts.family = family;
364
+ addrs = await _withTimeout(dnsPromises.lookup(host, nodeOpts), STATE.lookupTimeoutMs, host);
365
+ if (!Array.isArray(addrs)) addrs = [addrs];
366
+ }
367
+ var normalized = (addrs || []).map(function (a) {
368
+ if (typeof a === "string") return { address: a, family: net.isIP(a) || 4 };
369
+ return { address: a.address || a, family: a.family || net.isIP(a.address || a) || 4 };
370
+ });
371
+ _orderAddrs(normalized);
372
+ if (normalized.length === 0) {
373
+ throw new DnsError("dns/no-result", "dns lookup of '" + host + "' returned no addresses");
374
+ }
375
+ _cachePutPositive(host, cacheKey, normalized);
376
+ _emitObs("network.dns.lookup.success", { latencyMs: _now() - startMs, count: normalized.length });
377
+ return opts.all ? normalized : normalized[0];
378
+ } catch (e) {
379
+ _cachePutNegative(host, cacheKey, e);
380
+ _emitObs("network.dns.lookup.failure", { latencyMs: _now() - startMs, code: e.code || "unknown" });
381
+ throw e;
382
+ }
383
+ }
384
+
385
+ async function _resolveProtocol(host, family) {
386
+ if (typeof host !== "string" || host.length === 0) {
387
+ throw new DnsError("dns/bad-host", "dns.resolve" + family + ": host required");
388
+ }
389
+ if (net.isIP(host)) {
390
+ if (net.isIP(host) !== family) {
391
+ throw new DnsError("dns/wrong-family", "dns.resolve" + family + ": IP literal '" + host + "' is not family " + family);
392
+ }
393
+ return [host];
394
+ }
395
+ _emitObs("network.dns.resolve.requested", { family: family });
396
+ var startMs = _now();
397
+ try {
398
+ var addrs;
399
+ if (STATE.doh) {
400
+ addrs = await _withTimeout(_dohLookup(host, family), STATE.lookupTimeoutMs, host);
401
+ } else if (STATE.dot) {
402
+ addrs = await _withTimeout(_dotLookup(host, family), STATE.lookupTimeoutMs, host);
403
+ } else {
404
+ var resolver = family === 6 ? dnsPromises.resolve6 : dnsPromises.resolve4;
405
+ addrs = await _withTimeout(resolver(host), STATE.lookupTimeoutMs, host);
406
+ }
407
+ if (!Array.isArray(addrs)) addrs = [addrs];
408
+ var normalized = addrs.map(function (a) { return typeof a === "string" ? a : (a.address || a); });
409
+ if (normalized.length === 0) {
410
+ throw new DnsError("dns/no-result", "dns.resolve" + family + " of '" + host + "' returned no addresses");
411
+ }
412
+ _emitObs("network.dns.resolve.success", { family: family, latencyMs: _now() - startMs, count: normalized.length });
413
+ return normalized;
414
+ } catch (e) {
415
+ _emitObs("network.dns.resolve.failure", { family: family, latencyMs: _now() - startMs, code: e.code || "unknown" });
416
+ if (e instanceof DnsError) throw e;
417
+ throw new DnsError("dns/resolve-failed",
418
+ "dns.resolve" + family + " of '" + host + "' failed: " + (e.message || String(e)));
419
+ }
420
+ }
421
+
422
+ async function resolve4(host) { return _resolveProtocol(host, 4); }
423
+ async function resolve6(host) { return _resolveProtocol(host, 6); }
424
+ async function resolveAaaa(host) { return _resolveProtocol(host, 6); }
425
+
426
+ function nodeLookup(host, options, callback) {
427
+ if (typeof options === "function") { callback = options; options = {}; }
428
+ options = options || {};
429
+ var fam = options.family !== undefined ? options.family : 0;
430
+ lookup(host, { family: fam, all: !!options.all }).then(
431
+ function (res) {
432
+ if (options.all) callback(null, res);
433
+ else callback(null, res.address, res.family);
434
+ },
435
+ function (err) { callback(err); }
436
+ );
437
+ }
438
+
439
+ function _emitObs(name, fields) {
440
+ try { observability().emit(name, fields || {}); } catch (_e) {}
441
+ }
442
+
443
+ function _stateForTest() { return STATE; }
444
+ function _resetForTest() {
445
+ STATE.servers = null; STATE.resultOrder = null; STATE.family = 0;
446
+ STATE.lookupTimeoutMs = 0; STATE.cacheTtlMs = 0; STATE.cacheNegativeTtlMs = 0;
447
+ STATE.doh = null; STATE.dot = null;
448
+ _clearCache();
449
+ }
450
+
451
+ module.exports = {
452
+ setServers: setServers,
453
+ getServers: getServers,
454
+ setResultOrder: setResultOrder,
455
+ setFamily: setFamily,
456
+ setLookupTimeoutMs: setLookupTimeoutMs,
457
+ setCacheTtlMs: setCacheTtlMs,
458
+ useDnsOverHttps: useDnsOverHttps,
459
+ useDnsOverTls: useDnsOverTls,
460
+ lookup: lookup,
461
+ resolve4: resolve4,
462
+ resolve6: resolve6,
463
+ resolveAaaa: resolveAaaa,
464
+ nodeLookup: nodeLookup,
465
+ clearCache: _clearCache,
466
+ DnsError: DnsError,
467
+ _stateForTest: _stateForTest,
468
+ _resetForTest: _resetForTest,
469
+ };