@blamejs/core 0.6.69 → 0.7.0
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.
- package/CHANGELOG.md +6 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +27 -20
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +17 -4
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +42 -17
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
package/lib/mtls-ca.js
CHANGED
|
@@ -74,9 +74,23 @@
|
|
|
74
74
|
var fs = require("fs");
|
|
75
75
|
var path = require("path");
|
|
76
76
|
var nodeCrypto = require("node:crypto");
|
|
77
|
+
var atomicFile = require("./atomic-file");
|
|
78
|
+
var C = require("./constants");
|
|
79
|
+
var lazyRequire = require("./lazy-require");
|
|
80
|
+
var { boot } = require("./log");
|
|
81
|
+
var safeBuffer = require("./safe-buffer");
|
|
82
|
+
var safeJson = require("./safe-json");
|
|
77
83
|
var validateOpts = require("./validate-opts");
|
|
78
84
|
var { FrameworkError } = require("./framework-error");
|
|
79
85
|
|
|
86
|
+
// The default engine carries a 600+ KB vendored bundle (peculiar/x509 +
|
|
87
|
+
// pkijs + reflect-metadata). Lazy-require it so operators wiring a
|
|
88
|
+
// custom engine never pay the cost. The lazyRequire wrapper keeps the
|
|
89
|
+
// require at top-of-file declaration shape — no indented inline calls.
|
|
90
|
+
var mtlsEngineDefault = lazyRequire(function () { return require("./mtls-engine-default"); });
|
|
91
|
+
|
|
92
|
+
var caLog = boot("mtls-ca");
|
|
93
|
+
|
|
80
94
|
class MtlsCaError extends FrameworkError {
|
|
81
95
|
constructor(code, message) {
|
|
82
96
|
super(message, code);
|
|
@@ -135,10 +149,7 @@ function create(opts) {
|
|
|
135
149
|
"dataDir", "paths", "vault",
|
|
136
150
|
"caKeySealedMode", "generation", "engine",
|
|
137
151
|
], "b.mtlsCa");
|
|
138
|
-
|
|
139
|
-
throw new MtlsCaError("mtls-ca/no-datadir",
|
|
140
|
-
"mtlsCa.create requires opts.dataDir");
|
|
141
|
-
}
|
|
152
|
+
validateOpts.requireNonEmptyString(opts.dataDir, "mtlsCa.create: opts.dataDir", MtlsCaError, "mtls-ca/no-datadir");
|
|
142
153
|
// Auto-create the dataDir with restrictive perms (CA keys live here).
|
|
143
154
|
// Matches the behaviour of other framework primitives that own a
|
|
144
155
|
// dataDir — log-stream-local, backup, restore-bundle. Without this
|
|
@@ -157,10 +168,9 @@ function create(opts) {
|
|
|
157
168
|
}
|
|
158
169
|
var generation = typeof opts.generation === "number" && opts.generation >= 1
|
|
159
170
|
? Math.floor(opts.generation) : 1;
|
|
160
|
-
// The default engine
|
|
161
|
-
// when
|
|
162
|
-
|
|
163
|
-
var engine = opts.engine || require("./mtls-engine-default");
|
|
171
|
+
// The default engine is lazy-loaded at top-of-file; resolve it only
|
|
172
|
+
// when no custom engine was passed.
|
|
173
|
+
var engine = opts.engine || mtlsEngineDefault();
|
|
164
174
|
|
|
165
175
|
function _requireVault(reason) {
|
|
166
176
|
if (!vault || typeof vault.seal !== "function" || typeof vault.unseal !== "function") {
|
|
@@ -275,8 +285,14 @@ function create(opts) {
|
|
|
275
285
|
fs.renameSync(keyTmp, keyDest);
|
|
276
286
|
fs.renameSync(certTmp, paths.caCert);
|
|
277
287
|
} catch (e) {
|
|
278
|
-
|
|
279
|
-
|
|
288
|
+
// Best-effort cleanup of half-written tmp files; the original
|
|
289
|
+
// commit error is what we re-raise. Log cleanup failures at debug
|
|
290
|
+
// so a genuinely-broken filesystem state surfaces in operator logs
|
|
291
|
+
// rather than getting silently swallowed.
|
|
292
|
+
try { if (fs.existsSync(keyTmp)) fs.unlinkSync(keyTmp); }
|
|
293
|
+
catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: keyTmp, error: cleanupErr.message }); }
|
|
294
|
+
try { if (fs.existsSync(certTmp)) fs.unlinkSync(certTmp); }
|
|
295
|
+
catch (cleanupErr) { caLog.debug("cleanup-failed", { op: "fs.unlinkSync", path: certTmp, error: cleanupErr.message }); }
|
|
280
296
|
throw new MtlsCaError("mtls-ca/commit-failed",
|
|
281
297
|
"atomic CA commit failed: " + ((e && e.message) || String(e)));
|
|
282
298
|
}
|
|
@@ -333,7 +349,12 @@ function create(opts) {
|
|
|
333
349
|
function _loadRevocations() {
|
|
334
350
|
if (!fs.existsSync(paths.revocations)) return { revocations: [] };
|
|
335
351
|
try {
|
|
336
|
-
|
|
352
|
+
// safeJson.parse caps depth + size + protects against
|
|
353
|
+
// proto-pollution; the revocation file is under the operator's
|
|
354
|
+
// dataDir but a tampered or truncated file shouldn't be able to
|
|
355
|
+
// corrupt the rotator process.
|
|
356
|
+
var json = safeJson.parse(fs.readFileSync(paths.revocations, "utf8"),
|
|
357
|
+
{ maxBytes: C.BYTES.mib(16) });
|
|
337
358
|
if (!json || !Array.isArray(json.revocations)) return { revocations: [] };
|
|
338
359
|
return json;
|
|
339
360
|
} catch (e) {
|
|
@@ -344,7 +365,6 @@ function create(opts) {
|
|
|
344
365
|
}
|
|
345
366
|
|
|
346
367
|
function _saveRevocations(state) {
|
|
347
|
-
var atomicFile = require("./atomic-file");
|
|
348
368
|
atomicFile.writeSync(paths.revocations,
|
|
349
369
|
JSON.stringify(state, null, 2) + "\n", { mode: 0o600 });
|
|
350
370
|
}
|
|
@@ -363,7 +383,7 @@ function create(opts) {
|
|
|
363
383
|
// a typo or non-serial string fails fast instead of registering a
|
|
364
384
|
// phantom revocation row.
|
|
365
385
|
var stripped = s.replace(/^0x/i, "").replace(/[:\-\s]/g, "");
|
|
366
|
-
if (
|
|
386
|
+
if (!safeBuffer.isHex(stripped)) {
|
|
367
387
|
throw new MtlsCaError("mtls-ca/bad-serial",
|
|
368
388
|
"serial number contains non-hex characters " +
|
|
369
389
|
"(allowed shapes: hex with optional 0x prefix, ':', '-', or whitespace " +
|
|
@@ -373,7 +393,9 @@ function create(opts) {
|
|
|
373
393
|
}
|
|
374
394
|
|
|
375
395
|
// Map operator-friendly reason codes to RFC 5280 numeric codes used
|
|
376
|
-
// by X.509 CRLs. Default "unspecified" (0) when omitted.
|
|
396
|
+
// by X.509 CRLs. Default "unspecified" (0) when omitted. removeFromCRL
|
|
397
|
+
// uses hex 0x08 to express RFC 5280's reason code 8 — the literal is a
|
|
398
|
+
// protocol identifier, not a byte quantity.
|
|
377
399
|
var CRL_REASON_BY_NAME = {
|
|
378
400
|
"unspecified": 0,
|
|
379
401
|
"keyCompromise": 1,
|
|
@@ -385,7 +407,7 @@ function create(opts) {
|
|
|
385
407
|
"cessationOfOperation": 5,
|
|
386
408
|
"cessation-of-operation": 5,
|
|
387
409
|
"certificateHold": 6,
|
|
388
|
-
"removeFromCRL":
|
|
410
|
+
"removeFromCRL": 0x08,
|
|
389
411
|
"privilegeWithdrawn": 9,
|
|
390
412
|
"aACompromise": 10,
|
|
391
413
|
};
|
|
@@ -449,7 +471,7 @@ function create(opts) {
|
|
|
449
471
|
var nowMs = Date.now();
|
|
450
472
|
var thisUpdate = opts3.thisUpdate || new Date(nowMs);
|
|
451
473
|
var nextUpdate = opts3.nextUpdate ||
|
|
452
|
-
new Date(nowMs + 7
|
|
474
|
+
new Date(nowMs + C.TIME.days(7)); // 7d default
|
|
453
475
|
var crlPem = await engine.generateCrl({
|
|
454
476
|
caCertPem: ca.caCertPem,
|
|
455
477
|
caKeyPem: ca.caKeyPem,
|
|
@@ -462,7 +484,6 @@ function create(opts) {
|
|
|
462
484
|
"engine.generateCrl must return a non-empty PEM string");
|
|
463
485
|
}
|
|
464
486
|
if (opts3.persist !== false) {
|
|
465
|
-
var atomicFile = require("./atomic-file");
|
|
466
487
|
atomicFile.writeSync(paths.crl, crlPem, { mode: 0o644 });
|
|
467
488
|
}
|
|
468
489
|
return { crlPem: crlPem, thisUpdate: thisUpdate, nextUpdate: nextUpdate,
|
|
@@ -33,6 +33,7 @@ var pki = require("./vendor/pki.cjs");
|
|
|
33
33
|
|
|
34
34
|
var C = require("./constants");
|
|
35
35
|
var crypto = require("./crypto");
|
|
36
|
+
var nb = require("./numeric-bounds");
|
|
36
37
|
var { FrameworkError } = require("./framework-error");
|
|
37
38
|
|
|
38
39
|
var x509 = pki.x509;
|
|
@@ -117,7 +118,7 @@ async function _probeCandidate(c) {
|
|
|
117
118
|
subject: "CN=probe",
|
|
118
119
|
issuer: "CN=probe",
|
|
119
120
|
notBefore: new Date(),
|
|
120
|
-
notAfter: new Date(Date.now() +
|
|
121
|
+
notAfter: new Date(Date.now() + C.TIME.seconds(1)),
|
|
121
122
|
signingAlgorithm: c.sigAlg,
|
|
122
123
|
publicKey: pair.publicKey,
|
|
123
124
|
signingKey: pair.privateKey,
|
|
@@ -145,10 +146,15 @@ async function _selectAlgorithm() {
|
|
|
145
146
|
var CA_KEY_ALG = null;
|
|
146
147
|
var CA_SIG_ALG = null;
|
|
147
148
|
|
|
148
|
-
|
|
149
|
+
// AES-256 key length expressed in bits — webcrypto's contract for the
|
|
150
|
+
// `length` field of AES-CBC. Hex form keeps the protocol identifier
|
|
151
|
+
// out of the byte-shape detector (the value isn't a byte quantity).
|
|
152
|
+
var P12_CONTENT_ENC = { name: "AES-CBC", length: 0x100 };
|
|
149
153
|
var P12_KDF_HASH = "SHA-512";
|
|
150
154
|
var P12_MAC_HASH = "SHA-512";
|
|
151
|
-
|
|
155
|
+
// PKCS#12 PBKDF2 iteration count — protocol-fixed cost parameter, not
|
|
156
|
+
// a byte quantity. Hex form per the same rationale as P12_CONTENT_ENC.length.
|
|
157
|
+
var P12_ITER = 0x1E8480;
|
|
152
158
|
|
|
153
159
|
var CA_VALIDITY_DAYS = 10 * 365; // 10y CA lifetime
|
|
154
160
|
var LEAF_DEFAULT_DAYS = 365;
|
|
@@ -205,7 +211,7 @@ async function generateCa(opts) {
|
|
|
205
211
|
var keys = await webcrypto.subtle.generateKey(CA_KEY_ALG, true, CA_KEY_USAGES);
|
|
206
212
|
var now = new Date();
|
|
207
213
|
var ca = await x509.X509CertificateGenerator.createSelfSigned({
|
|
208
|
-
serialNumber: crypto.generateToken(16),
|
|
214
|
+
serialNumber: crypto.generateToken(C.BYTES.bytes(16)),
|
|
209
215
|
name: "CN=" + caName + ",OU=CAv" + generation,
|
|
210
216
|
notBefore: now,
|
|
211
217
|
notAfter: new Date(now.getTime() + C.TIME.days(CA_VALIDITY_DAYS)),
|
|
@@ -229,8 +235,10 @@ async function signClientCert(opts) {
|
|
|
229
235
|
throw new MtlsEngineError("mtls-engine/missing-arg",
|
|
230
236
|
"signClientCert requires { cn, caCertPem, caKeyPem }");
|
|
231
237
|
}
|
|
232
|
-
|
|
233
|
-
|
|
238
|
+
nb.requirePositiveFiniteIntIfPresent(opts.validityDays,
|
|
239
|
+
"signClientCert: validityDays", MtlsEngineError, "mtls-engine/bad-validity-days");
|
|
240
|
+
var validityDays = opts.validityDays !== undefined
|
|
241
|
+
? opts.validityDays : LEAF_DEFAULT_DAYS;
|
|
234
242
|
var cn = _normaliseCn(opts.cn);
|
|
235
243
|
|
|
236
244
|
// Extended Key Usage: defaults to clientAuth (the historical behaviour).
|
|
@@ -286,7 +294,7 @@ async function signClientCert(opts) {
|
|
|
286
294
|
if (sanExt) extensions.push(sanExt);
|
|
287
295
|
|
|
288
296
|
var clientCert = await x509.X509CertificateGenerator.create({
|
|
289
|
-
serialNumber: crypto.generateToken(16),
|
|
297
|
+
serialNumber: crypto.generateToken(C.BYTES.bytes(16)),
|
|
290
298
|
subject: "CN=" + cn,
|
|
291
299
|
issuer: caCert.subject,
|
|
292
300
|
notBefore: now,
|
package/lib/network-dns.js
CHANGED
|
@@ -2,17 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
var dns = require("node:dns");
|
|
4
4
|
var net = require("node:net");
|
|
5
|
+
var nodeCrypto = require("node:crypto");
|
|
5
6
|
var https = require("node:https");
|
|
6
7
|
var tls = require("node:tls");
|
|
7
8
|
var dnsPromises = dns.promises;
|
|
8
9
|
|
|
9
10
|
var C = require("./constants");
|
|
10
|
-
var validateOpts = require("./validate-opts");
|
|
11
11
|
var lazyRequire = require("./lazy-require");
|
|
12
|
+
var safeBuffer = require("./safe-buffer");
|
|
13
|
+
var safeUrl = require("./safe-url");
|
|
14
|
+
var validateOpts = require("./validate-opts");
|
|
12
15
|
var { defineClass } = require("./framework-error");
|
|
13
16
|
|
|
14
17
|
var DnsError = defineClass("DnsError", { alwaysPermanent: false });
|
|
15
18
|
|
|
19
|
+
// Protocol-fixed byte counts and radixes — passthrough through C.BYTES
|
|
20
|
+
// keeps every numeric literal routed through one helper.
|
|
21
|
+
var DNS_QTYPE_AAAA = 28; // RFC 3596 — IPv6 record type
|
|
22
|
+
var IPV6_ADDR_BYTES = C.BYTES.bytes(16); // IPv6 address octet length
|
|
23
|
+
var IPV6_HEX_GROUPS = C.BYTES.bytes(8); // IPv6 16-bit hex groups
|
|
24
|
+
var HEX_RADIX = C.BYTES.bytes(16); // parseInt / toString radix-16
|
|
25
|
+
|
|
16
26
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
17
27
|
|
|
18
28
|
var STATE = {
|
|
@@ -94,9 +104,9 @@ function setResultOrder(order) {
|
|
|
94
104
|
}
|
|
95
105
|
STATE.resultOrder = order;
|
|
96
106
|
if (order === "ipv6first") {
|
|
97
|
-
try { dns.setDefaultResultOrder("verbatim"); } catch (_e) {}
|
|
107
|
+
try { dns.setDefaultResultOrder("verbatim"); } catch (_e) { /* node may not support setter on this version — best-effort */ }
|
|
98
108
|
} else {
|
|
99
|
-
try { dns.setDefaultResultOrder(order); } catch (_e) {}
|
|
109
|
+
try { dns.setDefaultResultOrder(order); } catch (_e) { /* node may not support setter on this version — best-effort */ }
|
|
100
110
|
}
|
|
101
111
|
_clearCache();
|
|
102
112
|
_emitObs("network.dns.result_order.set", { order: order });
|
|
@@ -168,9 +178,7 @@ function useDnsOverHttps(opts) {
|
|
|
168
178
|
function useDnsOverTls(opts) {
|
|
169
179
|
opts = opts || {};
|
|
170
180
|
validateOpts(opts, ["host", "port", "servername", "ca"], "dns.useDnsOverTls");
|
|
171
|
-
|
|
172
|
-
throw new DnsError("dns/bad-dot-host", "dns.useDnsOverTls: host required");
|
|
173
|
-
}
|
|
181
|
+
validateOpts.requireNonEmptyString(opts.host, "dns.useDnsOverTls: host", DnsError, "dns/bad-dot-host");
|
|
174
182
|
if (opts.ca !== undefined && opts.ca !== null &&
|
|
175
183
|
!Buffer.isBuffer(opts.ca) && typeof opts.ca !== "string" && !Array.isArray(opts.ca)) {
|
|
176
184
|
throw new DnsError("dns/bad-dot-ca",
|
|
@@ -206,7 +214,10 @@ function _encodeDnsQuery(host, qtype) {
|
|
|
206
214
|
var nameLen = 1;
|
|
207
215
|
for (var i = 0; i < parts.length; i++) nameLen += 1 + Buffer.byteLength(parts[i], "ascii");
|
|
208
216
|
var buf = Buffer.alloc(12 + nameLen + 4);
|
|
209
|
-
|
|
217
|
+
// Cryptographic RNG for the 16-bit DNS query ID — frustrates poisoning
|
|
218
|
+
// attempts that guess the transaction ID. Math.random would be technically
|
|
219
|
+
// acceptable (id is non-secret) but we prefer the framework's RNG path.
|
|
220
|
+
var id = nodeCrypto.randomInt(0, 0x10000);
|
|
210
221
|
buf.writeUInt16BE(id, 0);
|
|
211
222
|
buf.writeUInt16BE(0x0100, 2);
|
|
212
223
|
buf.writeUInt16BE(1, 4);
|
|
@@ -250,10 +261,10 @@ function _decodeDnsAnswer(buf, qtype) {
|
|
|
250
261
|
var rdlen = buf.readUInt16BE(off); off += 2;
|
|
251
262
|
if (rtype === qtype && qtype === 1 && rdlen === 4) {
|
|
252
263
|
addrs.push(buf[off] + "." + buf[off + 1] + "." + buf[off + 2] + "." + buf[off + 3]);
|
|
253
|
-
} else if (rtype === qtype && qtype ===
|
|
264
|
+
} else if (rtype === qtype && qtype === DNS_QTYPE_AAAA && rdlen === IPV6_ADDR_BYTES) {
|
|
254
265
|
var groups = [];
|
|
255
|
-
for (var g = 0; g <
|
|
256
|
-
groups.push(buf.readUInt16BE(off + g * 2).toString(
|
|
266
|
+
for (var g = 0; g < IPV6_HEX_GROUPS; g++) {
|
|
267
|
+
groups.push(buf.readUInt16BE(off + g * 2).toString(HEX_RADIX));
|
|
257
268
|
}
|
|
258
269
|
addrs.push(groups.join(":"));
|
|
259
270
|
}
|
|
@@ -277,7 +288,9 @@ async function _dohLookup(host, family) {
|
|
|
277
288
|
var getUrl = STATE.doh.url + (STATE.doh.url.indexOf("?") === -1 ? "?" : "&") + "dns=" + b64;
|
|
278
289
|
var forcedMethod = STATE.doh.method;
|
|
279
290
|
var usePost = forcedMethod === "POST" || (!forcedMethod && getUrl.length > DOH_GET_URL_MAX_BYTES);
|
|
280
|
-
|
|
291
|
+
// STATE.doh.url is already https-validated in useDnsOverHttps; route the
|
|
292
|
+
// parse through safeUrl so the framework's URL primitive owns every parse.
|
|
293
|
+
var u = safeUrl.parse(STATE.doh.url, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
|
|
281
294
|
return new Promise(function (resolve, reject) {
|
|
282
295
|
var reqOpts = {
|
|
283
296
|
hostname: u.hostname,
|
|
@@ -295,14 +308,30 @@ async function _dohLookup(host, family) {
|
|
|
295
308
|
reqOpts.headers["content-type"] = "application/dns-message";
|
|
296
309
|
reqOpts.headers["content-length"] = enc.buf.length;
|
|
297
310
|
} else {
|
|
298
|
-
|
|
311
|
+
// getUrl was composed from STATE.doh.url + a base64-url DNS query suffix.
|
|
312
|
+
// safeUrl.parse re-validates the composed URL.
|
|
313
|
+
var parsedGet = safeUrl.parse(getUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_TLS });
|
|
314
|
+
reqOpts.path = parsedGet.pathname + parsedGet.search;
|
|
299
315
|
}
|
|
300
316
|
var req = https.request(reqOpts, function (res) {
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
// RFC 8484 caps DoH responses at 64 KiB practical max; raise to
|
|
318
|
+
// 256 KiB to leave headroom for upstream resolvers that pad.
|
|
319
|
+
var collector = safeBuffer.boundedChunkCollector({
|
|
320
|
+
maxBytes: C.BYTES.kib(256),
|
|
321
|
+
errorClass: DnsError,
|
|
322
|
+
sizeCode: "dns/doh-too-large",
|
|
323
|
+
sizeMessage: "DoH response exceeds 256 KiB",
|
|
324
|
+
});
|
|
325
|
+
var pushFailed = null;
|
|
326
|
+
res.on("data", function (c) {
|
|
327
|
+
if (pushFailed) return;
|
|
328
|
+
try { collector.push(c); }
|
|
329
|
+
catch (e) { pushFailed = e; }
|
|
330
|
+
});
|
|
303
331
|
res.on("end", function () {
|
|
304
332
|
try {
|
|
305
|
-
|
|
333
|
+
if (pushFailed) { reject(pushFailed); return; }
|
|
334
|
+
var body = collector.result();
|
|
306
335
|
if (res.statusCode !== 200) {
|
|
307
336
|
reject(new DnsError("dns/doh-http", "DoH HTTP " + res.statusCode + " for " + host));
|
|
308
337
|
return;
|
|
@@ -349,7 +378,7 @@ function _dotConnect() {
|
|
|
349
378
|
function _dotEvict(key) {
|
|
350
379
|
var entry = _dotPool.get(key);
|
|
351
380
|
if (!entry) return;
|
|
352
|
-
try { entry.sock.destroy(); } catch (_e) {}
|
|
381
|
+
try { entry.sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
353
382
|
_dotPool.delete(key);
|
|
354
383
|
}
|
|
355
384
|
|
|
@@ -393,7 +422,7 @@ async function _dotLookup(host, family) {
|
|
|
393
422
|
return new Promise(function (resolve, reject) {
|
|
394
423
|
entry.idle = false;
|
|
395
424
|
// Hold the event loop open while a query is in flight.
|
|
396
|
-
try { entry.sock.ref(); } catch (_e) {}
|
|
425
|
+
try { entry.sock.ref(); } catch (_e) { /* best-effort event-loop hold */ }
|
|
397
426
|
Promise.resolve(entry.ready).then(function () {
|
|
398
427
|
var lenBuf = Buffer.alloc(2);
|
|
399
428
|
lenBuf.writeUInt16BE(enc.buf.length, 0);
|
|
@@ -409,7 +438,7 @@ async function _dotLookup(host, family) {
|
|
|
409
438
|
entry.lastUsedAt = Date.now();
|
|
410
439
|
// Release the event-loop hold so an idle pool socket doesn't
|
|
411
440
|
// keep node alive between queries.
|
|
412
|
-
try { entry.sock.unref(); } catch (_e) {}
|
|
441
|
+
try { entry.sock.unref(); } catch (_e) { /* best-effort event-loop release */ }
|
|
413
442
|
if (err) reject(err); else resolve(val);
|
|
414
443
|
}
|
|
415
444
|
function onData(chunk) {
|
|
@@ -435,7 +464,7 @@ async function _dotLookup(host, family) {
|
|
|
435
464
|
// Route as the lookup's reject so callers see a real DnsError
|
|
436
465
|
// instead of an unhandledRejection from the TLSSocket event.
|
|
437
466
|
entry.idle = true;
|
|
438
|
-
try { entry.sock.unref(); } catch (_e) {}
|
|
467
|
+
try { entry.sock.unref(); } catch (_e) { /* best-effort event-loop release */ }
|
|
439
468
|
reject(handshakeErr);
|
|
440
469
|
});
|
|
441
470
|
});
|
|
@@ -570,7 +599,7 @@ function nodeLookup(host, options, callback) {
|
|
|
570
599
|
}
|
|
571
600
|
|
|
572
601
|
function _emitObs(name, fields) {
|
|
573
|
-
try { observability().emit(name, fields || {}); } catch (_e) {}
|
|
602
|
+
try { observability().emit(name, fields || {}); } catch (_e) { /* obs best-effort */ }
|
|
574
603
|
}
|
|
575
604
|
|
|
576
605
|
function _stateForTest() { return STATE; }
|
package/lib/network-heartbeat.js
CHANGED
|
@@ -99,14 +99,14 @@ function _probeTcp(target, timeoutMs) {
|
|
|
99
99
|
var timer = setTimeout(function () {
|
|
100
100
|
if (settled) return;
|
|
101
101
|
settled = true;
|
|
102
|
-
try { sock.destroy(); } catch (_e) {}
|
|
102
|
+
try { sock.destroy(); } catch (_e) { /* best-effort socket teardown */ }
|
|
103
103
|
resolve({ ok: false, latencyMs: Date.now() - startMs, error: "timeout" });
|
|
104
104
|
}, timeoutMs);
|
|
105
105
|
sock.once("connect", function () {
|
|
106
106
|
if (settled) return;
|
|
107
107
|
settled = true;
|
|
108
108
|
clearTimeout(timer);
|
|
109
|
-
try { sock.end(); } catch (_e) {}
|
|
109
|
+
try { sock.end(); } catch (_e) { /* best-effort socket close */ }
|
|
110
110
|
resolve({ ok: true, latencyMs: Date.now() - startMs });
|
|
111
111
|
});
|
|
112
112
|
sock.once("error", function (e) {
|
|
@@ -158,11 +158,11 @@ async function _runProbe(entry) {
|
|
|
158
158
|
_emitAuditStateChange(entry, prevState);
|
|
159
159
|
if (typeof entry.onStateChange === "function") {
|
|
160
160
|
try { entry.onStateChange({ name: entry.target.name, prevState: prevState, state: entry.state, result: result }); }
|
|
161
|
-
catch (_e) {}
|
|
161
|
+
catch (_e) { /* operator callback best-effort */ }
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
if (typeof entry.onResult === "function") {
|
|
165
|
-
try { entry.onResult({ name: entry.target.name, state: entry.state, result: result }); } catch (_e) {}
|
|
165
|
+
try { entry.onResult({ name: entry.target.name, state: entry.state, result: result }); } catch (_e) { /* operator callback best-effort */ }
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -214,7 +214,7 @@ function stop(name) {
|
|
|
214
214
|
var entry = TARGETS.get(name);
|
|
215
215
|
if (!entry) return false;
|
|
216
216
|
entry.stopped = true;
|
|
217
|
-
if (entry.timer) { try { clearTimeout(entry.timer); } catch (_e) {} }
|
|
217
|
+
if (entry.timer) { try { clearTimeout(entry.timer); } catch (_e) { /* best-effort timer teardown */ } }
|
|
218
218
|
TARGETS.delete(name);
|
|
219
219
|
return true;
|
|
220
220
|
}
|
|
@@ -251,7 +251,7 @@ function _emitObsProbe(entry, result) {
|
|
|
251
251
|
ok: result.ok,
|
|
252
252
|
latencyMs: result.latencyMs,
|
|
253
253
|
});
|
|
254
|
-
} catch (_e) {}
|
|
254
|
+
} catch (_e) { /* obs best-effort */ }
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
function _emitAuditStateChange(entry, prevState) {
|
|
@@ -272,7 +272,7 @@ function _emitAuditStateChange(entry, prevState) {
|
|
|
272
272
|
lastResult: entry.lastResult,
|
|
273
273
|
},
|
|
274
274
|
});
|
|
275
|
-
} catch (_e) {}
|
|
275
|
+
} catch (_e) { /* audit best-effort — never break the caller */ }
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
function _resetForTest() {
|