@blamejs/blamejs-shop 0.4.84 → 0.4.86
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 +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/inventory-locations.js +86 -0
- package/lib/order.js +4 -2
- package/lib/security-middleware.js +1 -0
- package/lib/stock-transfers.js +21 -14
- package/lib/vendor/MANIFEST.json +71 -39
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +11 -3
- package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
- package/lib/vendor/blamejs/lib/auth/password.js +1 -0
- package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
- package/lib/vendor/blamejs/lib/daemon.js +4 -1
- package/lib/vendor/blamejs/lib/external-db.js +131 -0
- package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
- package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
- package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
- package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
- package/lib/vendor/blamejs/lib/mail.js +39 -0
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
- package/lib/vendor/blamejs/lib/network-dns.js +47 -2
- package/lib/vendor/blamejs/lib/network-nts.js +16 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
- package/lib/vendor/blamejs/lib/outbox.js +29 -0
- package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
- package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
- package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
- package/lib/vendor/blamejs/lib/session.js +85 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
- package/lib/webhooks.js +38 -9
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SMTP client (b.mail.transports.smtp) + proxy CONNECT tunnel
|
|
4
|
+
* (b.network.proxy.agentFor) framing-buffer + wall-clock bounds.
|
|
5
|
+
*
|
|
6
|
+
* Both paths accumulate bytes off a peer socket until a delimiter
|
|
7
|
+
* (CRLF / CRLFCRLF). Without a cap, a peer that streams bytes and never
|
|
8
|
+
* sends the delimiter grows the accumulator without limit (OOM). Without
|
|
9
|
+
* an absolute deadline, a slow-trickle peer resets the per-socket idle
|
|
10
|
+
* timer forever and the operation hangs.
|
|
11
|
+
*
|
|
12
|
+
* These drive the real shipped consumer surface:
|
|
13
|
+
* - b.mail.transports.smtp(...).send(msg) against a hostile net.Server
|
|
14
|
+
* - b.network.proxy.agentFor(url).createConnection(...) against a
|
|
15
|
+
* hostile CONNECT proxy
|
|
16
|
+
*
|
|
17
|
+
* RED (pre-fix): unbounded `buffer += data` / `Buffer.concat` → the send
|
|
18
|
+
* never settles (the server streams forever); the test times out / OOMs.
|
|
19
|
+
* GREEN (post-fix): the send rejects promptly with a bounded-error code.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
var net = require("node:net");
|
|
23
|
+
|
|
24
|
+
var helpers = require("../helpers");
|
|
25
|
+
var b = helpers.b;
|
|
26
|
+
var check = helpers.check;
|
|
27
|
+
|
|
28
|
+
function listen(server) {
|
|
29
|
+
return new Promise(function (resolve, reject) {
|
|
30
|
+
server.on("error", reject);
|
|
31
|
+
server.listen(0, "127.0.0.1", function () {
|
|
32
|
+
resolve(server.address().port);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function closeServer(server) {
|
|
38
|
+
return new Promise(function (resolve) {
|
|
39
|
+
try { server.close(function () { resolve(); }); }
|
|
40
|
+
catch (_e) { resolve(); }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- (1) SMTP: framing buffer is bounded against a never-CRLF peer ----
|
|
45
|
+
|
|
46
|
+
async function testSmtpResponseTooLargeBounded() {
|
|
47
|
+
var streamTimer = null;
|
|
48
|
+
// A server that completes the TCP handshake, sends a 220 greeting (so
|
|
49
|
+
// the client moves past connect), then on the first client command
|
|
50
|
+
// streams non-CRLF bytes forever.
|
|
51
|
+
var server = net.createServer(function (sock) {
|
|
52
|
+
// Client destroys mid-blast at the cap; swallow the resulting ECONNRESET.
|
|
53
|
+
sock.on("error", function () { if (streamTimer) { clearInterval(streamTimer); streamTimer = null; } });
|
|
54
|
+
sock.write("220 evil.example ESMTP\r\n");
|
|
55
|
+
sock.on("data", function () {
|
|
56
|
+
// Client just sent EHLO; respond with an unbounded non-CRLF blast.
|
|
57
|
+
var blast = Buffer.alloc(16 * 1024, 0x41); // 16 KiB of 'A', no CRLF
|
|
58
|
+
streamTimer = setInterval(function () {
|
|
59
|
+
if (!sock.writable) { clearInterval(streamTimer); return; }
|
|
60
|
+
try { sock.write(blast); } catch (_e) { clearInterval(streamTimer); }
|
|
61
|
+
}, 1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
var port = await listen(server);
|
|
65
|
+
|
|
66
|
+
var transport = b.mail.transports.smtp({
|
|
67
|
+
host: "127.0.0.1",
|
|
68
|
+
port: port,
|
|
69
|
+
timeoutMs: 60000, // idle timer kept high so it can't mask the cap
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
var err = null;
|
|
73
|
+
try {
|
|
74
|
+
await transport.send({
|
|
75
|
+
from: "ops@example.com",
|
|
76
|
+
to: ["alice@dest.example"],
|
|
77
|
+
subject: "hi",
|
|
78
|
+
text: "body",
|
|
79
|
+
});
|
|
80
|
+
} catch (e) { err = e; }
|
|
81
|
+
|
|
82
|
+
if (streamTimer) clearInterval(streamTimer);
|
|
83
|
+
await closeServer(server);
|
|
84
|
+
|
|
85
|
+
check("smtp: never-CRLF peer rejects (does not hang/OOM)", err !== null);
|
|
86
|
+
check("smtp: rejects with response-too-large",
|
|
87
|
+
err && typeof err.message === "string" &&
|
|
88
|
+
err.message.indexOf("response-too-large") !== -1);
|
|
89
|
+
check("smtp: error is a MailError", err && err.isMailError === true);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- (2) SMTP: absolute transaction deadline fires on a trickle peer ----
|
|
93
|
+
|
|
94
|
+
async function testSmtpTransactionDeadlineBounded() {
|
|
95
|
+
var trickleTimer = null;
|
|
96
|
+
// A server that greets, then for each command emits a SINGLE
|
|
97
|
+
// continuation byte-pair periodically — never a final response. With a
|
|
98
|
+
// generous idle timeout but a tiny maxTransactionMs the send must still
|
|
99
|
+
// fail on the wall-clock deadline.
|
|
100
|
+
var server = net.createServer(function (sock) {
|
|
101
|
+
// Client destroys at the transaction deadline; swallow the ECONNRESET.
|
|
102
|
+
sock.on("error", function () { if (trickleTimer) { clearInterval(trickleTimer); trickleTimer = null; } });
|
|
103
|
+
sock.write("220 slow.example ESMTP\r\n");
|
|
104
|
+
sock.on("data", function () {
|
|
105
|
+
// Keep the socket "alive" with periodic continuation lines so the
|
|
106
|
+
// idle timer never trips, but never complete the EHLO response.
|
|
107
|
+
if (trickleTimer) return;
|
|
108
|
+
trickleTimer = setInterval(function () {
|
|
109
|
+
if (!sock.writable) { clearInterval(trickleTimer); return; }
|
|
110
|
+
try { sock.write("250-keepalive\r\n"); } catch (_e) { clearInterval(trickleTimer); }
|
|
111
|
+
}, 20);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
var port = await listen(server);
|
|
115
|
+
|
|
116
|
+
var transport = b.mail.transports.smtp({
|
|
117
|
+
host: "127.0.0.1",
|
|
118
|
+
port: port,
|
|
119
|
+
timeoutMs: 60000, // idle timer never trips (trickle keeps it warm)
|
|
120
|
+
maxTransactionMs: 300, // absolute deadline — must fire
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
var started = Date.now();
|
|
124
|
+
var err = null;
|
|
125
|
+
try {
|
|
126
|
+
await transport.send({
|
|
127
|
+
from: "ops@example.com",
|
|
128
|
+
to: ["alice@dest.example"],
|
|
129
|
+
subject: "hi",
|
|
130
|
+
text: "body",
|
|
131
|
+
});
|
|
132
|
+
} catch (e) { err = e; }
|
|
133
|
+
var elapsed = Date.now() - started;
|
|
134
|
+
|
|
135
|
+
if (trickleTimer) clearInterval(trickleTimer);
|
|
136
|
+
await closeServer(server);
|
|
137
|
+
|
|
138
|
+
check("smtp: trickle peer rejects (does not hang)", err !== null);
|
|
139
|
+
check("smtp: rejects with transaction-timeout",
|
|
140
|
+
err && typeof err.message === "string" &&
|
|
141
|
+
err.message.indexOf("transaction-timeout") !== -1);
|
|
142
|
+
check("smtp: deadline fired near maxTransactionMs (well under idle)", elapsed < 5000);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- (3) maxTransactionMs is validated at config time ----
|
|
146
|
+
|
|
147
|
+
function testMaxTransactionMsValidated() {
|
|
148
|
+
var threw = null;
|
|
149
|
+
try {
|
|
150
|
+
b.mail.transports.smtp({ host: "127.0.0.1", maxTransactionMs: -5 });
|
|
151
|
+
} catch (e) { threw = e; }
|
|
152
|
+
check("smtp: negative maxTransactionMs throws MailError",
|
|
153
|
+
threw && threw.isMailError === true && threw.code === "mail/smtp-misconfigured");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- (4) Proxy CONNECT: framing buffer bounded against a never-CRLFCRLF proxy ----
|
|
157
|
+
|
|
158
|
+
async function testProxyConnectHeadersTooLargeBounded() {
|
|
159
|
+
var streamTimer = null;
|
|
160
|
+
// A fake proxy that accepts the CONNECT, then streams bytes that never
|
|
161
|
+
// contain the CRLFCRLF header terminator.
|
|
162
|
+
var server = net.createServer(function (sock) {
|
|
163
|
+
// Client tears down once the cap trips; swallow the resulting ECONNRESET.
|
|
164
|
+
sock.on("error", function () { if (streamTimer) { clearInterval(streamTimer); streamTimer = null; } });
|
|
165
|
+
sock.on("data", function () {
|
|
166
|
+
if (streamTimer) return;
|
|
167
|
+
var blast = Buffer.alloc(16 * 1024, 0x42); // 16 KiB of 'B', no CRLFCRLF
|
|
168
|
+
streamTimer = setInterval(function () {
|
|
169
|
+
if (!sock.writable) { clearInterval(streamTimer); return; }
|
|
170
|
+
try { sock.write(blast); } catch (_e) { clearInterval(streamTimer); }
|
|
171
|
+
}, 1);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
var port = await listen(server);
|
|
175
|
+
|
|
176
|
+
b.network.proxy.set({ http: "http://127.0.0.1:" + port });
|
|
177
|
+
var agent = b.network.proxy.agentFor("http://target.example/");
|
|
178
|
+
|
|
179
|
+
var err = null;
|
|
180
|
+
await new Promise(function (resolve) {
|
|
181
|
+
agent.createConnection({ host: "target.example", port: 80 }, function (e, sock) {
|
|
182
|
+
err = e;
|
|
183
|
+
if (sock) { try { sock.destroy(); } catch (_e) { /* ignore */ } }
|
|
184
|
+
resolve();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (streamTimer) clearInterval(streamTimer);
|
|
189
|
+
b.network.proxy._resetForTest();
|
|
190
|
+
await closeServer(server);
|
|
191
|
+
|
|
192
|
+
check("proxy: never-CRLFCRLF reply rejects (does not hang/OOM)", err !== null);
|
|
193
|
+
check("proxy: rejects with connect-headers-too-large",
|
|
194
|
+
err && err.code === "proxy/connect-headers-too-large");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- (5) Proxy CONNECT: ABSOLUTE wall-clock deadline (Codex P2, #362) ----
|
|
198
|
+
// A proxy that trickles a byte just inside every idle window but never sends
|
|
199
|
+
// CRLFCRLF would reset an idle socket.setTimeout forever and never trip the
|
|
200
|
+
// 64 KiB cap. The connect must still fail via an ABSOLUTE deadline. RED when
|
|
201
|
+
// the bound is socket.setTimeout (idle): the connect hangs past the deadline.
|
|
202
|
+
async function testProxyConnectAbsoluteDeadlineBounded() {
|
|
203
|
+
var trickle = null;
|
|
204
|
+
var server = net.createServer(function (sock) {
|
|
205
|
+
// The client tears the socket down at the deadline; writing to it after
|
|
206
|
+
// emits ECONNRESET on the server side — expected, swallow it so the test
|
|
207
|
+
// process doesn't crash on an unhandled 'error'.
|
|
208
|
+
sock.on("error", function () { if (trickle) { clearInterval(trickle); trickle = null; } });
|
|
209
|
+
sock.on("data", function () {
|
|
210
|
+
if (trickle) return;
|
|
211
|
+
// One byte every 50ms — keeps an idle timer warm, stays far under the
|
|
212
|
+
// 64 KiB header cap, never sends the CRLFCRLF terminator.
|
|
213
|
+
trickle = setInterval(function () {
|
|
214
|
+
if (!sock.writable) { clearInterval(trickle); return; }
|
|
215
|
+
try { sock.write("x"); } catch (_e) { clearInterval(trickle); }
|
|
216
|
+
}, 50);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
var port = await listen(server);
|
|
220
|
+
|
|
221
|
+
b.network.proxy._setConnectTimeoutForTest(300); // small absolute deadline
|
|
222
|
+
b.network.proxy.set({ http: "http://127.0.0.1:" + port });
|
|
223
|
+
var agent = b.network.proxy.agentFor("http://target.example/");
|
|
224
|
+
|
|
225
|
+
var started = Date.now();
|
|
226
|
+
var err = null;
|
|
227
|
+
await new Promise(function (resolve) {
|
|
228
|
+
agent.createConnection({ host: "target.example", port: 80 }, function (e, sock) {
|
|
229
|
+
err = e;
|
|
230
|
+
if (sock) { try { sock.destroy(); } catch (_e) { /* ignore */ } }
|
|
231
|
+
resolve();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
var elapsed = Date.now() - started;
|
|
235
|
+
|
|
236
|
+
if (trickle) clearInterval(trickle);
|
|
237
|
+
b.network.proxy._resetForTest();
|
|
238
|
+
await closeServer(server);
|
|
239
|
+
|
|
240
|
+
check("proxy: trickling CONNECT reply still fails (absolute deadline)", err !== null);
|
|
241
|
+
check("proxy: rejects with connect-timeout", err && err.code === "proxy/connect-timeout");
|
|
242
|
+
check("proxy: bounded by the absolute deadline (~timeout, not held open by trickle)",
|
|
243
|
+
elapsed < 3000);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---- Run ----
|
|
247
|
+
|
|
248
|
+
async function run() {
|
|
249
|
+
await testSmtpResponseTooLargeBounded();
|
|
250
|
+
await testSmtpTransactionDeadlineBounded();
|
|
251
|
+
testMaxTransactionMsValidated();
|
|
252
|
+
await testProxyConnectHeadersTooLargeBounded();
|
|
253
|
+
await testProxyConnectAbsoluteDeadlineBounded();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = { run: run };
|
|
257
|
+
|
|
258
|
+
if (require.main === module) {
|
|
259
|
+
run().then(
|
|
260
|
+
function () { console.log("[mail-proxy-framing-bounds] OK — " + helpers.getChecks() + " checks passed"); },
|
|
261
|
+
function (e) { console.error("FAIL:", (e && e.stack) || e); process.exit(1); }
|
|
262
|
+
);
|
|
263
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* network-dns: every lookup has an effective wall-clock deadline out of
|
|
4
|
+
* the box, and a stalled upstream tears the socket down.
|
|
5
|
+
*
|
|
6
|
+
* Regression guard for the CWE-400 class: `lookupTimeoutMs` defaulted to
|
|
7
|
+
* 0 (no deadline) and `_withTimeout` was a no-op at 0, so a
|
|
8
|
+
* header-then-stall / accept-then-never-reply upstream hung the request
|
|
9
|
+
* forever. The raw transport sockets also had no req.setTimeout /
|
|
10
|
+
* socket.setTimeout, so even a rejected promise leaked the fd.
|
|
11
|
+
*
|
|
12
|
+
* Drives the real `b.network.dns.resolve(host, "SVCB", {transport:
|
|
13
|
+
* "system"})` consumer path against a TCP black-hole responder — the
|
|
14
|
+
* same raw-query code path (`_systemRawQuery`) an operator runs — and
|
|
15
|
+
* confirms a stalled upstream is torn down within the deadline rather
|
|
16
|
+
* than hanging forever.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
var net = require("node:net");
|
|
20
|
+
|
|
21
|
+
var helpers = require("../helpers");
|
|
22
|
+
var check = helpers.check;
|
|
23
|
+
var b = helpers.b;
|
|
24
|
+
|
|
25
|
+
var C = require("../../lib/constants");
|
|
26
|
+
|
|
27
|
+
var dnsModule = b.network.dns;
|
|
28
|
+
|
|
29
|
+
// A TCP server that accepts the connection but never sends a reply (and
|
|
30
|
+
// never closes) — models a stalled upstream resolver. Returns the live
|
|
31
|
+
// sockets it accepted so the test can assert teardown.
|
|
32
|
+
function _startBlackHoleResponder() {
|
|
33
|
+
return new Promise(function (resolve) {
|
|
34
|
+
var accepted = [];
|
|
35
|
+
var srv = net.createServer(function (sock) {
|
|
36
|
+
accepted.push(sock);
|
|
37
|
+
// Swallow the query; deliberately never reply, never end.
|
|
38
|
+
sock.on("data", function () { /* black hole */ });
|
|
39
|
+
sock.on("error", function () { /* fixture best-effort */ });
|
|
40
|
+
});
|
|
41
|
+
srv.unref();
|
|
42
|
+
srv.listen(0, "127.0.0.1", function () {
|
|
43
|
+
resolve({ srv: srv, port: srv.address().port, accepted: accepted });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function _throwsAsync(fn, expectedCodeSubstr) {
|
|
49
|
+
try { await fn(); return false; }
|
|
50
|
+
catch (e) {
|
|
51
|
+
if (!expectedCodeSubstr) return true;
|
|
52
|
+
var hay = (e.code || "") + " " + (e.message || "");
|
|
53
|
+
return hay.indexOf(expectedCodeSubstr) !== -1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function run() {
|
|
58
|
+
dnsModule._resetForTest();
|
|
59
|
+
|
|
60
|
+
// ----------------------------------------------------------------
|
|
61
|
+
// The default deadline is applied (not 0 = no-timeout).
|
|
62
|
+
// ----------------------------------------------------------------
|
|
63
|
+
check("default lookupTimeoutMs is a non-zero wall-clock deadline (10s)",
|
|
64
|
+
dnsModule._stateForTest().lookupTimeoutMs === C.TIME.seconds(10));
|
|
65
|
+
|
|
66
|
+
// ----------------------------------------------------------------
|
|
67
|
+
// A stalled upstream tears the socket down within the deadline AND
|
|
68
|
+
// rejects the consumer call — without any operator setLookupTimeoutMs.
|
|
69
|
+
// We shorten the deadline to keep the test fast; the *mechanism* under
|
|
70
|
+
// test (setTimeout teardown + non-zero default _withTimeout) is the
|
|
71
|
+
// same one the 10s default arms. On the buggy tree this hangs forever.
|
|
72
|
+
// ----------------------------------------------------------------
|
|
73
|
+
var hole = await _startBlackHoleResponder();
|
|
74
|
+
dnsModule.useSystemResolver();
|
|
75
|
+
dnsModule.setServers(["127.0.0.1:" + hole.port]);
|
|
76
|
+
dnsModule.setLookupTimeoutMs(500);
|
|
77
|
+
|
|
78
|
+
var rejected = await _throwsAsync(function () {
|
|
79
|
+
return dnsModule.resolve("stall.example.com", "SVCB", { transport: "system" });
|
|
80
|
+
}, "dns/lookup-timeout");
|
|
81
|
+
check("resolve(...,'SVCB',system) against a black-hole upstream rejects with dns/lookup-timeout (does not hang)",
|
|
82
|
+
rejected);
|
|
83
|
+
|
|
84
|
+
// The accepted socket(s) must be destroyed — the deadline teardown,
|
|
85
|
+
// not just a promise-level reject leaving the fd alive.
|
|
86
|
+
await helpers.waitUntil(function () {
|
|
87
|
+
return hole.accepted.length > 0 &&
|
|
88
|
+
hole.accepted.every(function (s) { return s.destroyed; });
|
|
89
|
+
}, {
|
|
90
|
+
timeoutMs: 5000,
|
|
91
|
+
label: "network-dns timeout: stalled upstream socket torn down",
|
|
92
|
+
});
|
|
93
|
+
check("stalled upstream socket is torn down (fd not leaked)",
|
|
94
|
+
hole.accepted.length > 0 && hole.accepted.every(function (s) { return s.destroyed; }));
|
|
95
|
+
|
|
96
|
+
hole.srv.close();
|
|
97
|
+
dnsModule._resetForTest();
|
|
98
|
+
|
|
99
|
+
// ----------------------------------------------------------------
|
|
100
|
+
// Operator opt-out (0) still disables the deadline — the override path
|
|
101
|
+
// survives the default change.
|
|
102
|
+
// ----------------------------------------------------------------
|
|
103
|
+
dnsModule.setLookupTimeoutMs(0);
|
|
104
|
+
check("operator can disable the deadline (lookupTimeoutMs = 0)",
|
|
105
|
+
dnsModule._stateForTest().lookupTimeoutMs === 0);
|
|
106
|
+
dnsModule._resetForTest();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { run: run };
|
|
110
|
+
|
|
111
|
+
if (require.main === module) {
|
|
112
|
+
run().then(
|
|
113
|
+
function () { console.log("[network-dns-lookup-timeout-default] OK — " + helpers.getChecks() + " checks passed"); },
|
|
114
|
+
function (e) { console.error(e && e.stack || e); process.exit(1); }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.network.dns.resolver — per-query upstream timeout.
|
|
4
|
+
*
|
|
5
|
+
* A non-responsive / slow / stalling DoH endpoint (or a transport whose
|
|
6
|
+
* lookup never resolves — the wire-level analogue of a server that sends
|
|
7
|
+
* 200 headers then never ends the body) must not hold the await pending
|
|
8
|
+
* forever. Every query() and every followCnames hop is bounded by the
|
|
9
|
+
* create({ timeoutMs }) wall-clock deadline. RED on the pre-fix tree:
|
|
10
|
+
* queryA() never settles and the test hangs until the harness kills it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
var helpers = require("../helpers");
|
|
14
|
+
var b = helpers.b;
|
|
15
|
+
var check = helpers.check;
|
|
16
|
+
|
|
17
|
+
// A transport whose lookup never resolves — models a DoH endpoint that
|
|
18
|
+
// accepts the connection, sends headers, then stalls the body forever.
|
|
19
|
+
function _hangingTransport() {
|
|
20
|
+
return {
|
|
21
|
+
lookupCalls: 0,
|
|
22
|
+
lookup: function () {
|
|
23
|
+
this.lookupCalls += 1;
|
|
24
|
+
return new Promise(function () { /* never settles */ });
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Bound a promise so a regressed (hanging) tree fails as a timeout instead of
|
|
30
|
+
// stalling the whole suite. Polls via helpers.waitUntil (not a fixed
|
|
31
|
+
// setTimeout sleep) for the promise to settle, then rethrows its rejection —
|
|
32
|
+
// a genuine hang surfaces as a waitUntil timeout rather than hanging forever.
|
|
33
|
+
function _within(promise, ms, label) {
|
|
34
|
+
var state = { done: false, value: undefined, error: undefined };
|
|
35
|
+
promise.then(
|
|
36
|
+
function (v) { state.done = true; state.value = v; },
|
|
37
|
+
function (e) { state.done = true; state.error = e; }
|
|
38
|
+
);
|
|
39
|
+
return helpers.waitUntil(function () { return state.done; }, { timeoutMs: ms, label: label })
|
|
40
|
+
.then(function () { if (state.error) throw state.error; return state.value; });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function testQueryTimesOutOnHangingTransport() {
|
|
44
|
+
var transport = _hangingTransport();
|
|
45
|
+
var r = b.network.dns.resolver.create({ transport: transport, timeoutMs: 80 });
|
|
46
|
+
|
|
47
|
+
var threw = null;
|
|
48
|
+
try {
|
|
49
|
+
await _within(r.queryA("example.com"), 4000,
|
|
50
|
+
"queryA against a hanging transport");
|
|
51
|
+
} catch (e) {
|
|
52
|
+
threw = e;
|
|
53
|
+
}
|
|
54
|
+
check("queryA rejects on a hanging transport (does not hang)", threw !== null);
|
|
55
|
+
var msg = threw ? String(threw.message) : "";
|
|
56
|
+
check("rejection is the resolver deadline, not the test backstop",
|
|
57
|
+
/timed out/i.test(msg) && !/TEST-DEADLINE/.test(msg));
|
|
58
|
+
check("transport.lookup was actually invoked", transport.lookupCalls === 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function testFollowCnamesHopTimesOut() {
|
|
62
|
+
// followCnames loops query() per hop; the first hop's transport stall
|
|
63
|
+
// must bound the whole walk, not hang it.
|
|
64
|
+
var transport = _hangingTransport();
|
|
65
|
+
var r = b.network.dns.resolver.create({ transport: transport, timeoutMs: 80 });
|
|
66
|
+
|
|
67
|
+
var threw = null;
|
|
68
|
+
try {
|
|
69
|
+
await _within(r.followCnames("alias.example.com", "A"), 4000,
|
|
70
|
+
"followCnames against a hanging transport");
|
|
71
|
+
} catch (e) {
|
|
72
|
+
threw = e;
|
|
73
|
+
}
|
|
74
|
+
check("followCnames rejects when a hop's transport stalls", threw !== null);
|
|
75
|
+
var msg2 = threw ? String(threw.message) : "";
|
|
76
|
+
check("followCnames rejection is the resolver deadline",
|
|
77
|
+
/timed out/i.test(msg2) && !/TEST-DEADLINE/.test(msg2));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function testBadTimeoutRejectedAtCreate() {
|
|
81
|
+
var cases = [0, -1, NaN, Infinity];
|
|
82
|
+
for (var i = 0; i < cases.length; i += 1) {
|
|
83
|
+
var threw = null;
|
|
84
|
+
try {
|
|
85
|
+
b.network.dns.resolver.create({ timeoutMs: cases[i] });
|
|
86
|
+
} catch (e) {
|
|
87
|
+
threw = e;
|
|
88
|
+
}
|
|
89
|
+
check("create rejects timeoutMs=" + cases[i],
|
|
90
|
+
threw !== null && threw.code === "resolver/bad-input");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function testDefaultTimeoutAccepted() {
|
|
95
|
+
// No timeoutMs supplied — create() must still build (uses the default)
|
|
96
|
+
// and a fast fake transport resolves normally.
|
|
97
|
+
var r = b.network.dns.resolver.create({
|
|
98
|
+
transport: { lookup: function () { return Promise.reject(new Error("boom")); } },
|
|
99
|
+
});
|
|
100
|
+
var threw = null;
|
|
101
|
+
try {
|
|
102
|
+
await r.queryA("example.com");
|
|
103
|
+
} catch (e) {
|
|
104
|
+
threw = e;
|
|
105
|
+
}
|
|
106
|
+
// A transport error (not a timeout) surfaces as upstream-failed — proves
|
|
107
|
+
// the default-timeout path doesn't swallow real fast failures.
|
|
108
|
+
check("default timeout path surfaces fast transport errors",
|
|
109
|
+
threw !== null && threw.code === "resolver/upstream-failed");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function run() {
|
|
113
|
+
await testQueryTimesOutOnHangingTransport();
|
|
114
|
+
await testFollowCnamesHopTimesOut();
|
|
115
|
+
await testBadTimeoutRejectedAtCreate();
|
|
116
|
+
await testDefaultTimeoutAccepted();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { run: run };
|
|
120
|
+
|
|
121
|
+
if (require.main === module) run().then(function () {
|
|
122
|
+
process.stdout.write("PASS: network-dns-resolver-timeout\n");
|
|
123
|
+
}).catch(function (e) {
|
|
124
|
+
process.stderr.write("FAIL: " + (e && e.stack || e) + "\n");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.* NTS-KE handshake byte-cap.
|
|
4
|
+
*
|
|
5
|
+
* The NTS-KE (RFC 8915) key-establishment handshake reader accumulates
|
|
6
|
+
* received bytes until a REC_END record terminates the exchange. A
|
|
7
|
+
* wall-clock timer bounds the handshake by time, but NOT by memory: a
|
|
8
|
+
* server that streams non-END records fast enough OOMs the process
|
|
9
|
+
* before the timer fires. The reader must cap the accumulated buffer at
|
|
10
|
+
* a sane ceiling (64 KiB) and fail closed with an nts/* typed error.
|
|
11
|
+
*
|
|
12
|
+
* This drives the real consumer path — network-nts.performKeHandshake
|
|
13
|
+
* against a live loopback TLS server negotiating ALPN "ntske/1" that
|
|
14
|
+
* streams > 64 KiB of non-END records.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
var helpers = require("../helpers");
|
|
18
|
+
var check = helpers.check;
|
|
19
|
+
var nts = require("../../lib/network-nts");
|
|
20
|
+
var nodeTls = require("node:tls");
|
|
21
|
+
|
|
22
|
+
var REC_NEW_COOKIE = 5;
|
|
23
|
+
|
|
24
|
+
// Encode an NTS-KE record: u16 type (top bit = critical) || u16 length
|
|
25
|
+
// || body. We emit non-critical NEW_COOKIE records that never include a
|
|
26
|
+
// REC_END (type 0), so a conformant reader keeps accumulating.
|
|
27
|
+
function _encodeRecord(type, body) {
|
|
28
|
+
var hdr = Buffer.alloc(4);
|
|
29
|
+
hdr.writeUInt16BE(type & 0x7fff, 0);
|
|
30
|
+
hdr.writeUInt16BE(body.length, 2);
|
|
31
|
+
return Buffer.concat([hdr, body]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function testHandshakeByteCapFailsClosed() {
|
|
35
|
+
return new Promise(function (resolve, reject) {
|
|
36
|
+
(async function () {
|
|
37
|
+
var server = null;
|
|
38
|
+
try {
|
|
39
|
+
var ca = await helpers.b.mtlsEngine.generateCa({ name: "nts-test-ca" });
|
|
40
|
+
var leaf = await helpers.b.mtlsEngine.signClientCert({
|
|
41
|
+
cn: "localhost",
|
|
42
|
+
caCertPem: ca.caCertPem,
|
|
43
|
+
caKeyPem: ca.caKeyPem,
|
|
44
|
+
usage: "server",
|
|
45
|
+
sans: ["DNS:localhost", "IP:127.0.0.1"],
|
|
46
|
+
validityDays: 1,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// One ~1 KiB non-END record; streamed enough times to exceed the
|
|
50
|
+
// 64 KiB ceiling without ever sending REC_END.
|
|
51
|
+
var oneRecord = _encodeRecord(REC_NEW_COOKIE, Buffer.alloc(1020, 0x41));
|
|
52
|
+
var TOTAL_BYTES = 128 * 1024; // 2x the 64 KiB cap
|
|
53
|
+
|
|
54
|
+
server = nodeTls.createServer({
|
|
55
|
+
key: leaf.key,
|
|
56
|
+
cert: leaf.cert,
|
|
57
|
+
minVersion: "TLSv1.3",
|
|
58
|
+
maxVersion: "TLSv1.3",
|
|
59
|
+
ALPNProtocols: ["ntske/1"],
|
|
60
|
+
}, function (sock) {
|
|
61
|
+
sock.on("error", function () { /* client tears down mid-stream */ });
|
|
62
|
+
var sent = 0;
|
|
63
|
+
function pump() {
|
|
64
|
+
while (sent < TOTAL_BYTES) {
|
|
65
|
+
sent += oneRecord.length;
|
|
66
|
+
if (!sock.write(oneRecord)) {
|
|
67
|
+
sock.once("drain", pump);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
pump();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
server.listen(0, "127.0.0.1", function () {
|
|
76
|
+
var port = server.address().port;
|
|
77
|
+
var startedAt = Date.now();
|
|
78
|
+
nts.performKeHandshake({
|
|
79
|
+
host: "127.0.0.1",
|
|
80
|
+
port: port,
|
|
81
|
+
servername: "localhost",
|
|
82
|
+
ca: ca.caCertPem,
|
|
83
|
+
// Generous timeout: a passing run must reject on the byte cap
|
|
84
|
+
// FAST (well under this), proving the cap — not the timer —
|
|
85
|
+
// bounded the read.
|
|
86
|
+
timeoutMs: 30000,
|
|
87
|
+
}).then(function () {
|
|
88
|
+
server.close();
|
|
89
|
+
reject(new Error("handshake resolved despite > 64 KiB of non-END records"));
|
|
90
|
+
}).catch(function (e) {
|
|
91
|
+
var elapsed = Date.now() - startedAt;
|
|
92
|
+
try { server.close(); } catch (_e) { /* best-effort */ }
|
|
93
|
+
try {
|
|
94
|
+
check("handshake rejects on oversized stream",
|
|
95
|
+
e && e.code === "nts/ke-too-large");
|
|
96
|
+
check("rejection is NtsError typed",
|
|
97
|
+
e instanceof nts.NtsError);
|
|
98
|
+
check("rejection is bounded, not the wall-clock timeout",
|
|
99
|
+
e.code !== "nts/ke-timeout");
|
|
100
|
+
check("bounded fast (well under the 30s timer)",
|
|
101
|
+
elapsed < 15000);
|
|
102
|
+
resolve();
|
|
103
|
+
} catch (assertErr) {
|
|
104
|
+
reject(assertErr);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
server.on("error", function (e) { reject(e); });
|
|
110
|
+
} catch (e) {
|
|
111
|
+
if (server) { try { server.close(); } catch (_e) { /* best-effort */ } }
|
|
112
|
+
reject(e);
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function run() {
|
|
119
|
+
await testHandshakeByteCapFailsClosed();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { run: run };
|
|
123
|
+
|
|
124
|
+
if (require.main === module) {
|
|
125
|
+
run().then(function () { console.log("OK"); })
|
|
126
|
+
.catch(function (e) { console.error(e.stack || e); process.exit(1); });
|
|
127
|
+
}
|