socketduct 0.1.24 → 0.1.26
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/AGENTS.md +2 -2
- package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
- package/package.json +2 -2
- package/scripts/test.js +9 -14
- package/src/client/multiplexed-transport-set.js +20 -2
- package/src/logging/compressed-log-writer.js +1 -1
- package/src/logging/zstd-frame-codec.js +15 -1
- package/src/reverse/client.js +44 -1
- package/src/reverse/hub.js +10 -1
- package/src/reverse/runtime.js +11 -2
- package/test/e2e/gateway-process.test.js +93 -92
- package/test/e2e/logger-shutdown-process.test.js +40 -37
- package/test/e2e/reverse-client-recovery-process.test.js +107 -103
- package/test/e2e/reverse-gateway-process.test.js +328 -318
- package/test/integration/initial-target-recovery.test.js +333 -334
- package/test/integration/logging-cli.test.js +121 -119
- package/test/integration/logging-compressed-recovery.test.js +96 -95
- package/test/integration/multiplexed-compatibility.test.js +178 -177
- package/test/integration/multiplexed-corrections.test.js +360 -359
- package/test/integration/multiplexed-failures.test.js +253 -251
- package/test/integration/multiplexed-transport-set.test.js +334 -322
- package/test/integration/multiplexed-transport.test.js +135 -134
- package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
- package/test/integration/reverse-gateway.test.js +2309 -2218
- package/test/integration/reverse-resume-continuity.test.js +219 -218
- package/test/integration/reverse-route-class.test.js +363 -278
- package/test/integration/socketduct-http-agent.test.js +314 -312
- package/test/integration/socketduct-https-agent.test.js +516 -485
- package/test/integration/stream-compression.test.js +862 -824
- package/test/integration/tcp-forwarding.test.js +2550 -2447
- package/test/integration/tls-forwarding.test.js +593 -592
- package/test/unit/auth/auth.test.js +43 -44
- package/test/unit/auth/static-token-store.test.js +9 -8
- package/test/unit/client/connect.test.js +41 -40
- package/test/unit/client/deferred-virtual-socket.test.js +95 -94
- package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
- package/test/unit/client/multiplexed-transport-set.test.js +845 -732
- package/test/unit/client/socketduct-socket.test.js +768 -771
- package/test/unit/client/virtual-socket.test.js +198 -168
- package/test/unit/config/docker-gateway-files.test.js +224 -224
- package/test/unit/config/environment.test.js +472 -492
- package/test/unit/index.test.js +53 -52
- package/test/unit/logging/compressed-log-worker.test.js +23 -22
- package/test/unit/logging/compressed-log-writer.test.js +155 -154
- package/test/unit/logging/log-retention.test.js +52 -47
- package/test/unit/logging/log-segment-index.test.js +84 -81
- package/test/unit/logging/zstd-frame-codec.test.js +28 -19
- package/test/unit/protocol/compression.test.js +254 -298
- package/test/unit/protocol/frame.test.js +280 -276
- package/test/unit/protocol/v3-compression.test.js +21 -23
- package/test/unit/protocol/v3-frame.test.js +42 -41
- package/test/unit/release-configuration.test.js +15 -12
- package/test/unit/reverse/disconnect-attribution.test.js +65 -64
- package/test/unit/reverse-terminal-outcomes.test.js +53 -50
- package/test/unit/server/control-handler.test.js +232 -231
- package/test/unit/server/primary.test.js +51 -49
- package/test/unit/server/relay-runtime.test.js +280 -294
- package/test/unit/sessions/session.test.js +48 -62
- package/test/unit/spool/spool-file.test.js +401 -385
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {describe, expect, it} from "@velocious/testing"
|
|
2
2
|
import {mkdtemp, readdir, rm} from "node:fs/promises"
|
|
3
3
|
import {tmpdir} from "node:os"
|
|
4
4
|
import {join} from "node:path"
|
|
5
5
|
import {createServer} from "node:net"
|
|
6
6
|
import {Duplex} from "node:stream"
|
|
7
7
|
import {EventEmitter} from "node:events"
|
|
8
|
-
import test from "node:test"
|
|
9
8
|
|
|
10
9
|
import {InMemoryUserStore, login, TokenStore} from "../../../src/auth/user-store.js"
|
|
11
10
|
import {connectSocketduct, SocketductSocket} from "../../../src/client/socketduct-socket.js"
|
|
@@ -180,7 +179,7 @@ class FakeTimers {
|
|
|
180
179
|
* @returns {void}
|
|
181
180
|
*/
|
|
182
181
|
run(timer) {
|
|
183
|
-
|
|
182
|
+
expect(timer.active).toBe(true) // expected timer to be active
|
|
184
183
|
timer.active = false
|
|
185
184
|
timer.callback()
|
|
186
185
|
}
|
|
@@ -188,8 +187,8 @@ class FakeTimers {
|
|
|
188
187
|
/** @returns {void} - Runs the active relay connect timer. */
|
|
189
188
|
runConnectTimeout() {
|
|
190
189
|
const timer = this.timers.find((candidate) => candidate.active && candidate.delay === 10_000)
|
|
190
|
+
if (!timer) throw new Error("expected active relay connect timer")
|
|
191
191
|
|
|
192
|
-
assert.ok(timer, "expected the relay connect timeout to be armed")
|
|
193
192
|
timer.active = false
|
|
194
193
|
timer.callback()
|
|
195
194
|
}
|
|
@@ -226,41 +225,41 @@ async function createFakeRelaySocketClient({tls = false} = {}) {
|
|
|
226
225
|
return {fakeRelaySocket, rootDir, socket, timers}
|
|
227
226
|
}
|
|
228
227
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
228
|
+
describe("SocketductSocket", () => {
|
|
229
|
+
it("SocketductSocket is a Duplex-compatible socket facade", async () => {
|
|
230
|
+
const {rootDir, spool} = await createTestSpool()
|
|
231
|
+
const socket = new SocketductSocket({
|
|
232
|
+
relayHost: "127.0.0.1",
|
|
233
|
+
relayPort: 3100,
|
|
234
|
+
token: "token",
|
|
235
|
+
sessionName: "unit socket",
|
|
236
|
+
target: {host: "docker-api", port: 2375},
|
|
237
|
+
spool
|
|
238
|
+
})
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
240
|
+
expect(socket instanceof Duplex).toBe(true)
|
|
241
|
+
expect(socket.connecting).toBe(false)
|
|
242
|
+
expect(socket.pending).toBe(true)
|
|
243
|
+
expect(socket.remoteAddress).toBe("docker-api")
|
|
244
|
+
expect(socket.remotePort).toBe(2375)
|
|
245
|
+
expect(socket.bytesRead).toBe(0)
|
|
246
|
+
expect(socket.bytesWritten).toBe(0)
|
|
247
|
+
expect(typeof socket.connect).toBe("function")
|
|
248
|
+
expect(socket.setNoDelay()).toBe(socket)
|
|
249
|
+
expect(socket.setKeepAlive()).toBe(socket)
|
|
250
|
+
expect(socket.setTimeout(0)).toBe(socket)
|
|
251
|
+
expect(socket.ref()).toBe(socket)
|
|
252
|
+
expect(socket.unref()).toBe(socket)
|
|
253
|
+
expect(socket.address()).toEqual({address: "0.0.0.0", family: "IPv4", port: 0})
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
})
|
|
255
|
+
socket.destroy()
|
|
256
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
257
|
+
})
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
() => new SocketductSocket({
|
|
259
|
+
it("SocketductSocket and relay reject invalid compression chunk limits", async () => {
|
|
260
|
+
const {rootDir, spool} = await createTestSpool()
|
|
261
|
+
try {
|
|
262
|
+
expect(() => new SocketductSocket({
|
|
264
263
|
relayHost: "127.0.0.1",
|
|
265
264
|
relayPort: 3100,
|
|
266
265
|
token: "token",
|
|
@@ -268,336 +267,215 @@ test("SocketductSocket and relay reject invalid compression chunk limits", async
|
|
|
268
267
|
target: {host: "docker-api", port: 2375},
|
|
269
268
|
spool,
|
|
270
269
|
compressionLimits: {maxDecompressedBytes: 0}
|
|
271
|
-
})
|
|
272
|
-
/compressionLimits\.maxDecompressedBytes must be a positive safe integer/
|
|
273
|
-
)
|
|
270
|
+
})).toThrow(/compressionLimits\.maxDecompressedBytes must be a positive safe integer/)
|
|
274
271
|
|
|
275
|
-
|
|
276
|
-
() => startTcpRelay({
|
|
272
|
+
await expect(startTcpRelay({
|
|
277
273
|
authorizeTarget: () => false,
|
|
278
274
|
compressionLimits: {maxDecompressedBytes: 1.5},
|
|
279
275
|
tokens: new TokenStore({now: () => 1000, tokenTtlMs: 60_000}),
|
|
280
276
|
sessions: new SessionStore({now: () => 1000})
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
await rm(rootDir, {recursive: true, force: true})
|
|
286
|
-
}
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
test("SocketductSocket connect returns the socket", async () => {
|
|
290
|
-
const {rootDir, spool} = await createTestSpool()
|
|
291
|
-
const socket = new SocketductSocket({
|
|
292
|
-
relayHost: "127.0.0.1",
|
|
293
|
-
relayPort: 9,
|
|
294
|
-
token: "token",
|
|
295
|
-
sessionName: "unit socket",
|
|
296
|
-
target: {host: "docker-api", port: 2375},
|
|
297
|
-
spool,
|
|
298
|
-
reconnectDelayMs: 60_000
|
|
277
|
+
})).rejects.toThrow(/compressionLimits\.maxDecompressedBytes must be a positive safe integer/)
|
|
278
|
+
} finally {
|
|
279
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
280
|
+
}
|
|
299
281
|
})
|
|
300
282
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
socket.connect()
|
|
314
|
-
fakeRelaySocket.emit("connect")
|
|
315
|
-
const connected = new Promise((resolve) => socket.once("connect", resolve))
|
|
316
|
-
fakeRelaySocket.emit("data", encodeFrame({type: "session.created", identifier: "idle-timeout", framing: 2}))
|
|
317
|
-
await connected
|
|
318
|
-
|
|
319
|
-
socket.setTimeout(50)
|
|
320
|
-
const initialTimer = timers.activeTimer(50)
|
|
321
|
-
assert.ok(initialTimer, "setting an idle timeout must arm the injected scheduler")
|
|
322
|
-
|
|
323
|
-
await new Promise((resolve, reject) => {
|
|
324
|
-
socket.write("ping", (error) => error ? reject(error) : resolve(undefined))
|
|
283
|
+
it("SocketductSocket connect returns the socket", async () => {
|
|
284
|
+
const {rootDir, spool} = await createTestSpool()
|
|
285
|
+
const socket = new SocketductSocket({
|
|
286
|
+
relayHost: "127.0.0.1",
|
|
287
|
+
relayPort: 9,
|
|
288
|
+
token: "token",
|
|
289
|
+
sessionName: "unit socket",
|
|
290
|
+
target: {host: "docker-api", port: 2375},
|
|
291
|
+
spool,
|
|
292
|
+
reconnectDelayMs: 60_000
|
|
325
293
|
})
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
assert.equal(initialTimer.active, false)
|
|
330
|
-
assert.equal(timeoutEvents, 0)
|
|
331
|
-
|
|
332
|
-
const received = readOnce(socket)
|
|
333
|
-
fakeRelaySocket.emit("data", encodeCompactData({sequence: 4, payload: Buffer.from("pong")}))
|
|
334
|
-
assert.equal((await received).toString("utf8"), "pong")
|
|
335
|
-
const afterReadTimer = timers.activeTimer(50)
|
|
336
|
-
assert.ok(afterReadTimer, "read activity must leave one idle timer armed")
|
|
337
|
-
assert.notEqual(afterReadTimer, afterWriteTimer)
|
|
338
|
-
assert.equal(afterWriteTimer.active, false)
|
|
339
|
-
assert.equal(timeoutEvents, 0)
|
|
340
|
-
|
|
341
|
-
initialTimer.callback()
|
|
342
|
-
afterWriteTimer.callback()
|
|
343
|
-
assert.equal(timeoutEvents, 0, "cleared idle timer callbacks already queued before activity must be stale")
|
|
344
|
-
|
|
345
|
-
timers.run(afterReadTimer)
|
|
346
|
-
assert.equal(timeoutEvents, 1, "the current idle timer must fire without later activity")
|
|
347
|
-
socket.setTimeout(0)
|
|
348
|
-
assert.equal(timers.activeTimer(50), undefined)
|
|
349
|
-
} finally {
|
|
350
|
-
socket.setTimeout(0)
|
|
351
|
-
const closed = new Promise((resolve) => socket.once("close", resolve))
|
|
294
|
+
|
|
295
|
+
expect(socket.connect()).toBe(socket)
|
|
296
|
+
expect(socket.connecting).toBe(true)
|
|
352
297
|
socket.destroy()
|
|
353
|
-
await settleRelayFrames()
|
|
354
|
-
fakeRelaySocket.emit("data", encodeFrame({type: "session.close", direction: "target-to-client", acknowledged: true}))
|
|
355
|
-
await closed
|
|
356
298
|
await rm(rootDir, {recursive: true, force: true})
|
|
357
|
-
}
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
test("SocketductSocket destroys a timed-out plain relay socket once and ignores late events", async () => {
|
|
361
|
-
const {fakeRelaySocket, rootDir, socket, timers} = await createFakeRelaySocketClient()
|
|
362
|
-
let connectEvents = 0
|
|
363
|
-
let readyEvents = 0
|
|
299
|
+
})
|
|
364
300
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
301
|
+
it("SocketductSocket idle timeout resets on write and read activity before firing when idle", async () => {
|
|
302
|
+
const {fakeRelaySocket, rootDir, socket, timers} = await createFakeRelaySocketClient()
|
|
303
|
+
let timeoutEvents = 0
|
|
304
|
+
socket.on("timeout", () => { timeoutEvents += 1 })
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
socket.connect()
|
|
308
|
+
fakeRelaySocket.emit("connect")
|
|
309
|
+
const connected = new Promise((resolve) => socket.once("connect", resolve))
|
|
310
|
+
fakeRelaySocket.emit("data", encodeFrame({type: "session.created", identifier: "idle-timeout", framing: 2}))
|
|
311
|
+
await connected
|
|
312
|
+
|
|
313
|
+
socket.setTimeout(50)
|
|
314
|
+
const initialTimer = timers.activeTimer(50)
|
|
315
|
+
expect(initialTimer).toBeDefined() // setting an idle timeout must arm the injected scheduler
|
|
316
|
+
if (!initialTimer) throw new Error("expected initial idle timer to be armed")
|
|
317
|
+
|
|
318
|
+
await new Promise((resolve, reject) => {
|
|
319
|
+
socket.write("ping", (error) => error ? reject(error) : resolve(undefined))
|
|
320
|
+
})
|
|
321
|
+
const afterWriteTimer = timers.activeTimer(50)
|
|
322
|
+
expect(afterWriteTimer).toBeDefined() // write activity must leave one idle timer armed
|
|
323
|
+
if (!afterWriteTimer) throw new Error("expected idle timer after write")
|
|
324
|
+
expect(afterWriteTimer).not.toBe(initialTimer)
|
|
325
|
+
expect(initialTimer.active).toBe(false)
|
|
326
|
+
expect(timeoutEvents).toBe(0)
|
|
327
|
+
|
|
328
|
+
const received = readOnce(socket)
|
|
329
|
+
fakeRelaySocket.emit("data", encodeCompactData({sequence: 4, payload: Buffer.from("pong")}))
|
|
330
|
+
expect((await received).toString("utf8")).toBe("pong")
|
|
331
|
+
const afterReadTimer = timers.activeTimer(50)
|
|
332
|
+
expect(afterReadTimer).toBeDefined() // read activity must leave one idle timer armed
|
|
333
|
+
if (!afterReadTimer) throw new Error("expected idle timer after read")
|
|
334
|
+
expect(afterReadTimer).not.toBe(afterWriteTimer)
|
|
335
|
+
expect(afterWriteTimer.active).toBe(false)
|
|
336
|
+
expect(timeoutEvents).toBe(0)
|
|
337
|
+
|
|
338
|
+
initialTimer.callback()
|
|
339
|
+
afterWriteTimer.callback()
|
|
340
|
+
expect(timeoutEvents).toBe(0) // cleared idle timer callbacks already queued before activity must be stale
|
|
341
|
+
|
|
342
|
+
timers.run(afterReadTimer)
|
|
343
|
+
expect(timeoutEvents).toBe(1) // the current idle timer must fire without later activity
|
|
344
|
+
socket.setTimeout(0)
|
|
345
|
+
expect(timers.activeTimer(50)).toBe(undefined)
|
|
346
|
+
} finally {
|
|
347
|
+
socket.setTimeout(0)
|
|
348
|
+
const closed = new Promise((resolve) => socket.once("close", resolve))
|
|
349
|
+
socket.destroy()
|
|
350
|
+
await settleRelayFrames()
|
|
351
|
+
fakeRelaySocket.emit("data", encodeFrame({type: "session.close", direction: "target-to-client", acknowledged: true}))
|
|
352
|
+
await closed
|
|
353
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
354
|
+
}
|
|
355
|
+
})
|
|
369
356
|
|
|
370
|
-
|
|
371
|
-
|
|
357
|
+
it("SocketductSocket destroys a timed-out plain relay socket once and ignores late events", async () => {
|
|
358
|
+
const {fakeRelaySocket, rootDir, socket, timers} = await createFakeRelaySocketClient()
|
|
359
|
+
let connectEvents = 0
|
|
360
|
+
let readyEvents = 0
|
|
372
361
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
362
|
+
socket.on("connect", () => { connectEvents += 1 })
|
|
363
|
+
socket.on("ready", () => { readyEvents += 1 })
|
|
364
|
+
socket.connect()
|
|
365
|
+
timers.runConnectTimeout()
|
|
376
366
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
assert.equal(connectEvents, 0)
|
|
380
|
-
assert.equal(readyEvents, 0)
|
|
381
|
-
assert.equal(socket.connecting, true)
|
|
382
|
-
assert.equal(socket.pending, true)
|
|
383
|
-
assert.equal(timers.activeReconnectTimers(), 1)
|
|
367
|
+
expect(fakeRelaySocket.destroyCount).toBe(1)
|
|
368
|
+
expect(timers.activeReconnectTimers()).toBe(1)
|
|
384
369
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
370
|
+
fakeRelaySocket.emit("connect")
|
|
371
|
+
fakeRelaySocket.emit("error", new Error("late connect error"))
|
|
372
|
+
fakeRelaySocket.emit("close")
|
|
388
373
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
374
|
+
expect(fakeRelaySocket.destroyCount).toBe(1)
|
|
375
|
+
expect(fakeRelaySocket.writes.length).toBe(0)
|
|
376
|
+
expect(connectEvents).toBe(0)
|
|
377
|
+
expect(readyEvents).toBe(0)
|
|
378
|
+
expect(socket.connecting).toBe(true)
|
|
379
|
+
expect(socket.pending).toBe(true)
|
|
380
|
+
expect(timers.activeReconnectTimers()).toBe(1)
|
|
393
381
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
timers.runConnectTimeout()
|
|
382
|
+
socket.destroy()
|
|
383
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
384
|
+
})
|
|
398
385
|
|
|
399
|
-
|
|
400
|
-
|
|
386
|
+
it("SocketductSocket destroys a timed-out TLS relay socket once and ignores late secureConnect events", async () => {
|
|
387
|
+
const {fakeRelaySocket, rootDir, socket, timers} = await createFakeRelaySocketClient({tls: true})
|
|
388
|
+
let connectEvents = 0
|
|
389
|
+
let readyEvents = 0
|
|
401
390
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
391
|
+
socket.on("connect", () => { connectEvents += 1 })
|
|
392
|
+
socket.on("ready", () => { readyEvents += 1 })
|
|
393
|
+
socket.connect()
|
|
394
|
+
timers.runConnectTimeout()
|
|
405
395
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
assert.equal(connectEvents, 0)
|
|
409
|
-
assert.equal(readyEvents, 0)
|
|
410
|
-
assert.equal(socket.connecting, true)
|
|
411
|
-
assert.equal(socket.pending, true)
|
|
412
|
-
assert.equal(timers.activeReconnectTimers(), 1)
|
|
396
|
+
expect(fakeRelaySocket.destroyCount).toBe(1)
|
|
397
|
+
expect(timers.activeReconnectTimers()).toBe(1)
|
|
413
398
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
399
|
+
fakeRelaySocket.emit("secureConnect")
|
|
400
|
+
fakeRelaySocket.emit("error", new Error("late TLS error"))
|
|
401
|
+
fakeRelaySocket.emit("close")
|
|
417
402
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
peer.write(encodeFrame({type: "session.created", identifier: "legacy-session"}))
|
|
426
|
-
}
|
|
427
|
-
})
|
|
428
|
-
})
|
|
429
|
-
const relayPort = await listen(relay)
|
|
430
|
-
const socket = new SocketductSocket({
|
|
431
|
-
relayHost: "127.0.0.1",
|
|
432
|
-
relayPort,
|
|
433
|
-
token: "token",
|
|
434
|
-
sessionName: "legacy handshake",
|
|
435
|
-
target: {host: "docker-api", port: 2375},
|
|
436
|
-
spool,
|
|
437
|
-
reconnectDelayMs: 60_000
|
|
438
|
-
})
|
|
403
|
+
expect(fakeRelaySocket.destroyCount).toBe(1)
|
|
404
|
+
expect(fakeRelaySocket.writes.length).toBe(0)
|
|
405
|
+
expect(connectEvents).toBe(0)
|
|
406
|
+
expect(readyEvents).toBe(0)
|
|
407
|
+
expect(socket.connecting).toBe(true)
|
|
408
|
+
expect(socket.pending).toBe(true)
|
|
409
|
+
expect(timers.activeReconnectTimers()).toBe(1)
|
|
439
410
|
|
|
440
|
-
try {
|
|
441
|
-
let connected = false
|
|
442
|
-
socket.on("connect", () => { connected = true })
|
|
443
|
-
const error = new Promise((resolve) => socket.once("error", resolve))
|
|
444
|
-
socket.connect()
|
|
445
|
-
const handshakeError = await error
|
|
446
|
-
assert.match(String(handshakeError), /required framing version 2/)
|
|
447
|
-
assert.equal(connected, false)
|
|
448
|
-
assert.deepEqual(await readdir(rootDir), [])
|
|
449
|
-
} finally {
|
|
450
411
|
socket.destroy()
|
|
451
|
-
await new Promise((resolve, reject) => relay.close((closeError) => closeError ? reject(closeError) : resolve(undefined)))
|
|
452
412
|
await rm(rootDir, {recursive: true, force: true})
|
|
453
|
-
}
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
test("SocketductSocket relays target traffic directly", async () => {
|
|
457
|
-
const {rootDir, spool} = await createTestSpool()
|
|
458
|
-
const echoServer = createServer((socket) => {
|
|
459
|
-
socket.on("data", (chunk) => socket.write(Buffer.from(`echo:${chunk.toString("utf8")}`)))
|
|
460
|
-
})
|
|
461
|
-
const targetPort = await listen(echoServer)
|
|
462
|
-
|
|
463
|
-
const users = new InMemoryUserStore()
|
|
464
|
-
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
465
|
-
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
466
|
-
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
467
|
-
const relay = await startTcpRelay({
|
|
468
|
-
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
469
|
-
tokens,
|
|
470
|
-
sessions: new SessionStore({now: () => 1000})
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
const socket = new SocketductSocket({
|
|
474
|
-
relayHost: "127.0.0.1",
|
|
475
|
-
relayPort: relay.port,
|
|
476
|
-
token: token.value,
|
|
477
|
-
sessionName: "direct socket integration",
|
|
478
|
-
target: {host: "127.0.0.1", port: targetPort},
|
|
479
|
-
spool,
|
|
480
|
-
reconnectDelayMs: 10
|
|
481
413
|
})
|
|
482
414
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
415
|
+
it("SocketductSocket rejects a legacy session handshake without framing v2", async () => {
|
|
416
|
+
const {rootDir, spool} = await createTestSpool()
|
|
417
|
+
const relay = createServer((peer) => {
|
|
418
|
+
const decoder = new FrameDecoder()
|
|
419
|
+
peer.on("data", (chunk) => {
|
|
420
|
+
const frames = decoder.push(chunk)
|
|
421
|
+
if (frames.some((frame) => frame.header.type === "session.create")) {
|
|
422
|
+
peer.write(encodeFrame({type: "session.created", identifier: "legacy-session"}))
|
|
423
|
+
}
|
|
424
|
+
})
|
|
489
425
|
})
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
echoServer.close((error) => {
|
|
500
|
-
if (error) {
|
|
501
|
-
reject(error)
|
|
502
|
-
return
|
|
503
|
-
}
|
|
504
|
-
resolve(undefined)
|
|
426
|
+
const relayPort = await listen(relay)
|
|
427
|
+
const socket = new SocketductSocket({
|
|
428
|
+
relayHost: "127.0.0.1",
|
|
429
|
+
relayPort,
|
|
430
|
+
token: "token",
|
|
431
|
+
sessionName: "legacy handshake",
|
|
432
|
+
target: {host: "docker-api", port: 2375},
|
|
433
|
+
spool,
|
|
434
|
+
reconnectDelayMs: 60_000
|
|
505
435
|
})
|
|
506
|
-
})
|
|
507
|
-
await rm(rootDir, {recursive: true, force: true})
|
|
508
|
-
})
|
|
509
436
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
sessions: new SessionStore({now: () => 1000})
|
|
525
|
-
})
|
|
526
|
-
let relayConnections = 0
|
|
527
|
-
relay.server.on("connection", () => {
|
|
528
|
-
relayConnections += 1
|
|
437
|
+
try {
|
|
438
|
+
let connected = false
|
|
439
|
+
socket.on("connect", () => { connected = true })
|
|
440
|
+
const error = new Promise((resolve) => socket.once("error", resolve))
|
|
441
|
+
socket.connect()
|
|
442
|
+
const handshakeError = await error
|
|
443
|
+
expect(String(handshakeError)).toMatch(/required framing version 2/)
|
|
444
|
+
expect(connected).toBe(false)
|
|
445
|
+
expect(await readdir(rootDir)).toEqual([])
|
|
446
|
+
} finally {
|
|
447
|
+
socket.destroy()
|
|
448
|
+
await new Promise((resolve, reject) => relay.close((closeError) => closeError ? reject(closeError) : resolve(undefined)))
|
|
449
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
450
|
+
}
|
|
529
451
|
})
|
|
530
452
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
sessionName: "intentional end",
|
|
536
|
-
target: {host: "127.0.0.1", port: targetPort},
|
|
537
|
-
spool,
|
|
538
|
-
reconnectDelayMs: 10
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
socket.connect()
|
|
542
|
-
await new Promise((resolve, reject) => {
|
|
543
|
-
socket.once("error", reject)
|
|
544
|
-
socket.once("connect", () => {
|
|
545
|
-
socket.off("error", reject)
|
|
546
|
-
resolve(undefined)
|
|
453
|
+
it("SocketductSocket relays target traffic directly", async () => {
|
|
454
|
+
const {rootDir, spool} = await createTestSpool()
|
|
455
|
+
const echoServer = createServer((socket) => {
|
|
456
|
+
socket.on("data", (chunk) => socket.write(Buffer.from(`echo:${chunk.toString("utf8")}`)))
|
|
547
457
|
})
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
echoServer.close((error) => {
|
|
559
|
-
if (error) {
|
|
560
|
-
reject(error)
|
|
561
|
-
return
|
|
562
|
-
}
|
|
563
|
-
resolve(undefined)
|
|
458
|
+
const targetPort = await listen(echoServer)
|
|
459
|
+
|
|
460
|
+
const users = new InMemoryUserStore()
|
|
461
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
462
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
463
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
464
|
+
const relay = await startTcpRelay({
|
|
465
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
466
|
+
tokens,
|
|
467
|
+
sessions: new SessionStore({now: () => 1000})
|
|
564
468
|
})
|
|
565
|
-
})
|
|
566
|
-
await rm(rootDir, {recursive: true, force: true})
|
|
567
|
-
})
|
|
568
469
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
579
|
-
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
580
|
-
const relay = await startTcpRelay({
|
|
581
|
-
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
582
|
-
tokens,
|
|
583
|
-
sessions: new SessionStore({now: () => 1000}),
|
|
584
|
-
resumeWindowMs: 1_000
|
|
585
|
-
})
|
|
586
|
-
|
|
587
|
-
const socket = new SocketductSocket({
|
|
588
|
-
relayHost: "127.0.0.1",
|
|
589
|
-
relayPort: relay.port,
|
|
590
|
-
token: token.value,
|
|
591
|
-
sessionName: "socket reconnect",
|
|
592
|
-
target: {host: "127.0.0.1", port: targetPort},
|
|
593
|
-
spool,
|
|
594
|
-
reconnectDelayMs: 10
|
|
595
|
-
})
|
|
596
|
-
try {
|
|
597
|
-
let connectEvents = 0
|
|
598
|
-
let readyEvents = 0
|
|
599
|
-
socket.on("connect", () => { connectEvents += 1 })
|
|
600
|
-
socket.on("ready", () => { readyEvents += 1 })
|
|
470
|
+
const socket = new SocketductSocket({
|
|
471
|
+
relayHost: "127.0.0.1",
|
|
472
|
+
relayPort: relay.port,
|
|
473
|
+
token: token.value,
|
|
474
|
+
sessionName: "direct socket integration",
|
|
475
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
476
|
+
spool,
|
|
477
|
+
reconnectDelayMs: 10
|
|
478
|
+
})
|
|
601
479
|
|
|
602
480
|
socket.connect()
|
|
603
481
|
await new Promise((resolve, reject) => {
|
|
@@ -607,23 +485,70 @@ test("SocketductSocket reconnect replays writes without re-emitting connect", as
|
|
|
607
485
|
resolve(undefined)
|
|
608
486
|
})
|
|
609
487
|
})
|
|
610
|
-
socket.write("
|
|
611
|
-
|
|
488
|
+
socket.write("hello")
|
|
489
|
+
expect((await readOnce(socket)).toString("utf8")).toBe("echo:hello")
|
|
490
|
+
expect(socket.bytesWritten).toBe(5)
|
|
491
|
+
expect(socket.bytesRead).toBe(10)
|
|
492
|
+
|
|
493
|
+
socket.destroy()
|
|
494
|
+
await relay.close()
|
|
495
|
+
await new Promise((resolve, reject) => {
|
|
496
|
+
echoServer.close((error) => {
|
|
497
|
+
if (error) {
|
|
498
|
+
reject(error)
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
resolve(undefined)
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it("SocketductSocket end does not reopen the relay tunnel", async () => {
|
|
508
|
+
const {rootDir, spool} = await createTestSpool()
|
|
509
|
+
const echoServer = createServer((socket) => {
|
|
510
|
+
socket.on("data", (chunk) => socket.write(Buffer.from(`ended:${chunk.toString("utf8")}`)))
|
|
511
|
+
})
|
|
512
|
+
const targetPort = await listen(echoServer)
|
|
513
|
+
|
|
514
|
+
const users = new InMemoryUserStore()
|
|
515
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
516
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
517
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
518
|
+
const relay = await startTcpRelay({
|
|
519
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
520
|
+
tokens,
|
|
521
|
+
sessions: new SessionStore({now: () => 1000})
|
|
522
|
+
})
|
|
523
|
+
let relayConnections = 0
|
|
524
|
+
relay.server.on("connection", () => {
|
|
525
|
+
relayConnections += 1
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
const socket = new SocketductSocket({
|
|
529
|
+
relayHost: "127.0.0.1",
|
|
530
|
+
relayPort: relay.port,
|
|
531
|
+
token: token.value,
|
|
532
|
+
sessionName: "intentional end",
|
|
533
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
534
|
+
spool,
|
|
535
|
+
reconnectDelayMs: 10
|
|
536
|
+
})
|
|
612
537
|
|
|
613
|
-
|
|
538
|
+
socket.connect()
|
|
539
|
+
await new Promise((resolve, reject) => {
|
|
614
540
|
socket.once("error", reject)
|
|
615
|
-
socket.once("
|
|
541
|
+
socket.once("connect", () => {
|
|
616
542
|
socket.off("error", reject)
|
|
617
543
|
resolve(undefined)
|
|
618
544
|
})
|
|
619
545
|
})
|
|
620
|
-
|
|
621
|
-
socket.
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
} finally {
|
|
546
|
+
socket.write("bye")
|
|
547
|
+
expect((await readOnce(socket)).toString("utf8")).toBe("ended:bye")
|
|
548
|
+
socket.end()
|
|
549
|
+
await new Promise((resolve) => { setTimeout(resolve, 50) })
|
|
550
|
+
expect(relayConnections).toBe(1)
|
|
551
|
+
|
|
627
552
|
socket.destroy()
|
|
628
553
|
await relay.close()
|
|
629
554
|
await new Promise((resolve, reject) => {
|
|
@@ -636,489 +561,561 @@ test("SocketductSocket reconnect replays writes without re-emitting connect", as
|
|
|
636
561
|
})
|
|
637
562
|
})
|
|
638
563
|
await rm(rootDir, {recursive: true, force: true})
|
|
639
|
-
}
|
|
640
|
-
})
|
|
641
|
-
|
|
642
|
-
test("SocketductSocket defers an intentional close until an in-flight reconnect is authenticated and resumed", async () => {
|
|
643
|
-
const {rootDir, spool} = await createTestSpool()
|
|
644
|
-
const firstRelay = new FakeRelaySocket()
|
|
645
|
-
const secondRelay = new FakeRelaySocket()
|
|
646
|
-
const relaySockets = [firstRelay, secondRelay]
|
|
647
|
-
const timers = new FakeTimers()
|
|
648
|
-
const socket = new SocketductSocket({
|
|
649
|
-
relayHost: "relay.invalid",
|
|
650
|
-
relayPort: 3100,
|
|
651
|
-
token: "token",
|
|
652
|
-
sessionName: "close during reconnect",
|
|
653
|
-
target: {host: "docker-api", port: 2375},
|
|
654
|
-
spool,
|
|
655
|
-
reconnectDelayMs: 60_000
|
|
656
|
-
}, {
|
|
657
|
-
clearTimeout: (timer) => timers.clearTimeout(/** @type {{active: boolean}} */ (timer)),
|
|
658
|
-
createRelaySocket: () => /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (relaySockets.shift())),
|
|
659
|
-
setTimeout: (callback, delay) => timers.setTimeout(callback, delay)
|
|
660
564
|
})
|
|
661
565
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
await
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
await settleRelayFrames()
|
|
680
|
-
assert.deepEqual(writtenHeaders(secondRelay).map((header) => header.type), ["session.resume"])
|
|
681
|
-
|
|
682
|
-
secondRelay.emit("data", encodeFrame({type: "session.resumed", identifier: "close-reconnect", framing: 2}))
|
|
683
|
-
await settleRelayFrames()
|
|
684
|
-
assert.deepEqual(writtenHeaders(secondRelay).map((header) => header.type), ["session.resume", "session.close"])
|
|
685
|
-
secondRelay.emit("data", encodeFrame({type: "session.close", direction: "target-to-client", acknowledged: true}))
|
|
686
|
-
await closeEvent
|
|
687
|
-
assert.equal(relaySockets.length, 0)
|
|
688
|
-
assert.equal(timers.timers.filter((timer) => timer.active).length, 0)
|
|
689
|
-
assert.equal(secondRelay.destroyCount, 1)
|
|
690
|
-
assert.equal(secondRelay.listenerCount("connect"), 0)
|
|
691
|
-
assert.equal(secondRelay.listenerCount("data"), 0)
|
|
692
|
-
assert.equal(secondRelay.listenerCount("close"), 0)
|
|
693
|
-
assert.deepEqual(await readdir(rootDir), [])
|
|
694
|
-
} finally {
|
|
695
|
-
await destroyAndWait(socket)
|
|
696
|
-
await rm(rootDir, {recursive: true, force: true})
|
|
697
|
-
}
|
|
698
|
-
})
|
|
566
|
+
it("SocketductSocket reconnect replays writes without re-emitting connect", async () => {
|
|
567
|
+
const {rootDir, spool} = await createTestSpool()
|
|
568
|
+
const echoServer = createServer((socket) => {
|
|
569
|
+
socket.on("data", (chunk) => socket.write(Buffer.from(`again:${chunk.toString("utf8")}`)))
|
|
570
|
+
})
|
|
571
|
+
const targetPort = await listen(echoServer)
|
|
572
|
+
|
|
573
|
+
const users = new InMemoryUserStore()
|
|
574
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
575
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
576
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
577
|
+
const relay = await startTcpRelay({
|
|
578
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
579
|
+
tokens,
|
|
580
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
581
|
+
resumeWindowMs: 1_000
|
|
582
|
+
})
|
|
699
583
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
584
|
+
const socket = new SocketductSocket({
|
|
585
|
+
relayHost: "127.0.0.1",
|
|
586
|
+
relayPort: relay.port,
|
|
587
|
+
token: token.value,
|
|
588
|
+
sessionName: "socket reconnect",
|
|
589
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
590
|
+
spool,
|
|
591
|
+
reconnectDelayMs: 10
|
|
592
|
+
})
|
|
593
|
+
try {
|
|
594
|
+
let connectEvents = 0
|
|
595
|
+
let readyEvents = 0
|
|
596
|
+
socket.on("connect", () => { connectEvents += 1 })
|
|
597
|
+
socket.on("ready", () => { readyEvents += 1 })
|
|
598
|
+
|
|
599
|
+
socket.connect()
|
|
600
|
+
await new Promise((resolve, reject) => {
|
|
601
|
+
socket.once("error", reject)
|
|
602
|
+
socket.once("connect", () => {
|
|
603
|
+
socket.off("error", reject)
|
|
604
|
+
resolve(undefined)
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
socket.write("first")
|
|
608
|
+
expect((await readOnce(socket)).toString("utf8")).toBe("again:first")
|
|
609
|
+
|
|
610
|
+
const resumed = new Promise((resolve, reject) => {
|
|
611
|
+
socket.once("error", reject)
|
|
612
|
+
socket.once("ready", () => {
|
|
613
|
+
socket.off("error", reject)
|
|
614
|
+
resolve(undefined)
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
relay.closeActiveTunnel()
|
|
618
|
+
socket.write("second")
|
|
619
|
+
await resumed
|
|
620
|
+
expect((await readOnce(socket)).toString("utf8")).toBe("again:second")
|
|
621
|
+
expect(connectEvents).toBe(1)
|
|
622
|
+
expect(readyEvents >= 2).toBe(true)
|
|
623
|
+
} finally {
|
|
624
|
+
socket.destroy()
|
|
625
|
+
await relay.close()
|
|
626
|
+
await new Promise((resolve, reject) => {
|
|
627
|
+
echoServer.close((error) => {
|
|
628
|
+
if (error) {
|
|
629
|
+
reject(error)
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
resolve(undefined)
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
636
|
+
}
|
|
718
637
|
})
|
|
719
638
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
firstRelay
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
assert.deepEqual(writtenHeaders(secondRelay).map((header) => header.type), ["session.resume", "session.close"])
|
|
740
|
-
secondRelay.emit("data", encodeFrame({type: "session.close", direction: "target-to-client", acknowledged: true}))
|
|
741
|
-
await closeEvent
|
|
742
|
-
assert.equal(timers.timers.filter((timer) => timer.active).length, 0)
|
|
743
|
-
assert.equal(secondRelay.destroyCount, 1)
|
|
744
|
-
assert.equal(secondRelay.listenerCount("connect"), 0)
|
|
745
|
-
assert.equal(secondRelay.listenerCount("data"), 0)
|
|
746
|
-
assert.equal(secondRelay.listenerCount("close"), 0)
|
|
747
|
-
assert.deepEqual(await readdir(rootDir), [])
|
|
748
|
-
} finally {
|
|
749
|
-
await destroyAndWait(socket)
|
|
750
|
-
await rm(rootDir, {recursive: true, force: true})
|
|
751
|
-
}
|
|
752
|
-
})
|
|
639
|
+
it("SocketductSocket defers an intentional close until an in-flight reconnect is authenticated and resumed", async () => {
|
|
640
|
+
const {rootDir, spool} = await createTestSpool()
|
|
641
|
+
const firstRelay = new FakeRelaySocket()
|
|
642
|
+
const secondRelay = new FakeRelaySocket()
|
|
643
|
+
const relaySockets = [firstRelay, secondRelay]
|
|
644
|
+
const timers = new FakeTimers()
|
|
645
|
+
const socket = new SocketductSocket({
|
|
646
|
+
relayHost: "relay.invalid",
|
|
647
|
+
relayPort: 3100,
|
|
648
|
+
token: "token",
|
|
649
|
+
sessionName: "close during reconnect",
|
|
650
|
+
target: {host: "docker-api", port: 2375},
|
|
651
|
+
spool,
|
|
652
|
+
reconnectDelayMs: 60_000
|
|
653
|
+
}, {
|
|
654
|
+
clearTimeout: (timer) => timers.clearTimeout(/** @type {{active: boolean}} */ (timer)),
|
|
655
|
+
createRelaySocket: () => /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (relaySockets.shift())),
|
|
656
|
+
setTimeout: (callback, delay) => timers.setTimeout(callback, delay)
|
|
657
|
+
})
|
|
753
658
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
659
|
+
try {
|
|
660
|
+
socket.connect()
|
|
661
|
+
firstRelay.emit("connect")
|
|
662
|
+
const connected = new Promise((resolve) => socket.once("connect", resolve))
|
|
663
|
+
firstRelay.emit("data", encodeFrame({type: "session.created", identifier: "close-reconnect", framing: 2}))
|
|
664
|
+
await connected
|
|
665
|
+
|
|
666
|
+
firstRelay.destroyed = true
|
|
667
|
+
firstRelay.emit("close")
|
|
668
|
+
const reconnectTimer = timers.timers.find((timer) => timer.active && timer.delay === 60_000)
|
|
669
|
+
expect(reconnectTimer).toBeDefined()
|
|
670
|
+
if (!reconnectTimer) throw new Error("expected active reconnect timer")
|
|
671
|
+
reconnectTimer.active = false
|
|
672
|
+
reconnectTimer.callback()
|
|
673
|
+
|
|
674
|
+
const closeEvent = new Promise((resolve) => socket.once("close", resolve))
|
|
675
|
+
socket.destroy()
|
|
676
|
+
secondRelay.emit("connect")
|
|
677
|
+
await settleRelayFrames()
|
|
678
|
+
expect(writtenHeaders(secondRelay).map((header) => header.type)).toEqual(["session.resume"])
|
|
679
|
+
|
|
680
|
+
secondRelay.emit("data", encodeFrame({type: "session.resumed", identifier: "close-reconnect", framing: 2}))
|
|
681
|
+
await settleRelayFrames()
|
|
682
|
+
expect(writtenHeaders(secondRelay).map((header) => header.type)).toEqual(["session.resume", "session.close"])
|
|
683
|
+
secondRelay.emit("data", encodeFrame({type: "session.close", direction: "target-to-client", acknowledged: true}))
|
|
684
|
+
await closeEvent
|
|
685
|
+
expect(relaySockets.length).toBe(0)
|
|
686
|
+
expect(timers.timers.filter((timer) => timer.active).length).toBe(0)
|
|
687
|
+
expect(secondRelay.destroyCount).toBe(1)
|
|
688
|
+
expect(secondRelay.listenerCount("connect")).toBe(0)
|
|
689
|
+
expect(secondRelay.listenerCount("data")).toBe(0)
|
|
690
|
+
expect(secondRelay.listenerCount("close")).toBe(0)
|
|
691
|
+
expect(await readdir(rootDir)).toEqual([])
|
|
692
|
+
} finally {
|
|
693
|
+
await destroyAndWait(socket)
|
|
694
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
695
|
+
}
|
|
776
696
|
})
|
|
777
697
|
|
|
778
|
-
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
698
|
+
it("SocketductSocket reconnects immediately to close an announced session between attempts", async () => {
|
|
699
|
+
const {rootDir, spool} = await createTestSpool()
|
|
700
|
+
const firstRelay = new FakeRelaySocket()
|
|
701
|
+
const secondRelay = new FakeRelaySocket()
|
|
702
|
+
const relaySockets = [firstRelay, secondRelay]
|
|
703
|
+
const timers = new FakeTimers()
|
|
704
|
+
const socket = new SocketductSocket({
|
|
705
|
+
relayHost: "relay.invalid",
|
|
706
|
+
relayPort: 3100,
|
|
707
|
+
token: "token",
|
|
708
|
+
sessionName: "close between reconnects",
|
|
709
|
+
target: {host: "docker-api", port: 2375},
|
|
710
|
+
spool,
|
|
711
|
+
reconnectDelayMs: 60_000
|
|
712
|
+
}, {
|
|
713
|
+
clearTimeout: (timer) => timers.clearTimeout(/** @type {{active: boolean}} */ (timer)),
|
|
714
|
+
createRelaySocket: () => /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (relaySockets.shift())),
|
|
715
|
+
setTimeout: (callback, delay) => timers.setTimeout(callback, delay)
|
|
790
716
|
})
|
|
791
|
-
assert.deepEqual(await readdir(rootDir), [])
|
|
792
|
-
} finally {
|
|
793
|
-
await destroyAndWait(socket)
|
|
794
|
-
await relay.close()
|
|
795
|
-
await new Promise((resolve, reject) => targetServer.close((error) => error ? reject(error) : resolve(undefined)))
|
|
796
|
-
await rm(rootDir, {recursive: true, force: true})
|
|
797
|
-
}
|
|
798
|
-
})
|
|
799
717
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
718
|
+
try {
|
|
719
|
+
socket.connect()
|
|
720
|
+
firstRelay.emit("connect")
|
|
721
|
+
const connected = new Promise((resolve) => socket.once("connect", resolve))
|
|
722
|
+
firstRelay.emit("data", encodeFrame({type: "session.created", identifier: "close-between", framing: 2}))
|
|
723
|
+
await connected
|
|
724
|
+
firstRelay.destroyed = true
|
|
725
|
+
firstRelay.emit("close")
|
|
726
|
+
expect(timers.activeReconnectTimers()).toBe(1)
|
|
727
|
+
|
|
728
|
+
const closeEvent = new Promise((resolve) => socket.once("close", resolve))
|
|
729
|
+
socket.destroy()
|
|
730
|
+
expect(timers.activeReconnectTimers()).toBe(0)
|
|
731
|
+
expect(relaySockets.length).toBe(0) // logical close should start its own authenticated reconnect immediately
|
|
732
|
+
secondRelay.emit("connect")
|
|
733
|
+
await settleRelayFrames()
|
|
734
|
+
expect(writtenHeaders(secondRelay).map((header) => header.type)).toEqual(["session.resume"])
|
|
735
|
+
secondRelay.emit("data", encodeFrame({type: "session.resumed", identifier: "close-between", framing: 2}))
|
|
736
|
+
await settleRelayFrames()
|
|
737
|
+
expect(writtenHeaders(secondRelay).map((header) => header.type)).toEqual(["session.resume", "session.close"])
|
|
738
|
+
secondRelay.emit("data", encodeFrame({type: "session.close", direction: "target-to-client", acknowledged: true}))
|
|
739
|
+
await closeEvent
|
|
740
|
+
expect(timers.timers.filter((timer) => timer.active).length).toBe(0)
|
|
741
|
+
expect(secondRelay.destroyCount).toBe(1)
|
|
742
|
+
expect(secondRelay.listenerCount("connect")).toBe(0)
|
|
743
|
+
expect(secondRelay.listenerCount("data")).toBe(0)
|
|
744
|
+
expect(secondRelay.listenerCount("close")).toBe(0)
|
|
745
|
+
expect(await readdir(rootDir)).toEqual([])
|
|
746
|
+
} finally {
|
|
747
|
+
await destroyAndWait(socket)
|
|
748
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
749
|
+
}
|
|
813
750
|
})
|
|
814
|
-
/** @type {Error[]} */
|
|
815
|
-
const errors = []
|
|
816
|
-
/** @type {Buffer[]} */
|
|
817
|
-
const chunks = []
|
|
818
|
-
socket.on("error", (error) => errors.push(error))
|
|
819
|
-
socket.on("data", (chunk) => chunks.push(chunk))
|
|
820
|
-
|
|
821
|
-
try {
|
|
822
|
-
socket.connect()
|
|
823
|
-
relay.emit("connect")
|
|
824
|
-
const connected = new Promise((resolve) => socket.once("connect", resolve))
|
|
825
|
-
relay.emit("data", encodeFrame({type: "session.created", identifier: "queued-after-eof", framing: 2}))
|
|
826
|
-
await connected
|
|
827
|
-
relay.emit("data", Buffer.concat([
|
|
828
|
-
encodeFrame({type: "session.close", direction: "target-to-client"}),
|
|
829
|
-
encodeCompactData({sequence: 4, payload: Buffer.from("late")})
|
|
830
|
-
]))
|
|
831
|
-
await settleRelayFrames()
|
|
832
|
-
|
|
833
|
-
assert.deepEqual(chunks, [])
|
|
834
|
-
assert.deepEqual(errors, [])
|
|
835
|
-
assert.equal(socket.bytesRead, 0)
|
|
836
|
-
assert.equal(socket.destroyed, false)
|
|
837
|
-
assert.equal(socket.readable, false)
|
|
838
|
-
const closed = new Promise((resolve) => socket.once("close", resolve))
|
|
839
|
-
socket.destroy()
|
|
840
|
-
await settleRelayFrames()
|
|
841
|
-
assert.equal(writtenHeaders(relay).at(-1)?.type, "session.close")
|
|
842
|
-
relay.emit("data", encodeFrame({type: "session.close", direction: "target-to-client", acknowledged: true}))
|
|
843
|
-
await withDeadline(closed, "timed out waiting for logical close acknowledgement")
|
|
844
|
-
} finally {
|
|
845
|
-
await destroyAndWait(socket)
|
|
846
|
-
await rm(rootDir, {recursive: true, force: true})
|
|
847
|
-
}
|
|
848
|
-
})
|
|
849
751
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
752
|
+
it("SocketductSocket destroy while detached promptly removes the resumable relay session", async () => {
|
|
753
|
+
const {rootDir, spool} = await createTestSpool()
|
|
754
|
+
const targetServer = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
755
|
+
const targetPort = await listen(targetServer)
|
|
756
|
+
const users = new InMemoryUserStore()
|
|
757
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
758
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
759
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
760
|
+
const relay = await startTcpRelay({
|
|
761
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
762
|
+
resumeWindowMs: 10_000,
|
|
763
|
+
sessions: new SessionStore({now: () => 1000}),
|
|
764
|
+
tokens
|
|
765
|
+
})
|
|
766
|
+
const socket = new SocketductSocket({
|
|
767
|
+
relayHost: "127.0.0.1",
|
|
768
|
+
relayPort: relay.port,
|
|
769
|
+
token: token.value,
|
|
770
|
+
sessionName: "destroy while detached",
|
|
771
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
772
|
+
spool,
|
|
773
|
+
reconnectDelayMs: 60_000
|
|
774
|
+
})
|
|
866
775
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
776
|
+
try {
|
|
777
|
+
const connected = new Promise((resolve) => socket.once("connect", resolve))
|
|
778
|
+
socket.connect()
|
|
779
|
+
await connected
|
|
780
|
+
await relay.closeActiveTunnel()
|
|
781
|
+
await waitUntil(() => relay.status().detachedSessions === 1)
|
|
782
|
+
|
|
783
|
+
await destroyAndWait(socket)
|
|
784
|
+
await waitUntil(() => relay.status().sessions === 0)
|
|
785
|
+
expect(relay.status()).toEqual({
|
|
786
|
+
activeTunnels: 0, activeTargets: 0, sessions: 0, attachedSessions: 0,
|
|
787
|
+
detachedSessions: 0, bufferedTargetBytes: 0, targetConnectionAttempts: 0, targetRetryTimers: 0
|
|
788
|
+
})
|
|
789
|
+
expect(await readdir(rootDir)).toEqual([])
|
|
790
|
+
} finally {
|
|
791
|
+
await destroyAndWait(socket)
|
|
792
|
+
await relay.close()
|
|
793
|
+
await new Promise((resolve, reject) => targetServer.close((error) => error ? reject(error) : resolve(undefined)))
|
|
794
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
795
|
+
}
|
|
873
796
|
})
|
|
874
797
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
798
|
+
it("SocketductSocket ignores queued target data after target EOF", async () => {
|
|
799
|
+
const {rootDir, spool} = await createTestSpool()
|
|
800
|
+
const relay = new FakeRelaySocket()
|
|
801
|
+
const socket = new SocketductSocket({
|
|
802
|
+
relayHost: "relay.invalid",
|
|
803
|
+
relayPort: 3100,
|
|
804
|
+
token: "token",
|
|
805
|
+
sessionName: "queued data after eof",
|
|
806
|
+
target: {host: "docker-api", port: 2375},
|
|
807
|
+
spool,
|
|
808
|
+
reconnectDelayMs: 60_000
|
|
809
|
+
}, {
|
|
810
|
+
createRelaySocket: () => /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (relay))
|
|
880
811
|
})
|
|
812
|
+
/** @type {Error[]} */
|
|
813
|
+
const errors = []
|
|
814
|
+
/** @type {Buffer[]} */
|
|
815
|
+
const chunks = []
|
|
816
|
+
socket.on("error", (error) => errors.push(error))
|
|
817
|
+
socket.on("data", (chunk) => chunks.push(chunk))
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
socket.connect()
|
|
821
|
+
relay.emit("connect")
|
|
822
|
+
const connected = new Promise((resolve) => socket.once("connect", resolve))
|
|
823
|
+
relay.emit("data", encodeFrame({type: "session.created", identifier: "queued-after-eof", framing: 2}))
|
|
824
|
+
await connected
|
|
825
|
+
relay.emit("data", Buffer.concat([
|
|
826
|
+
encodeFrame({type: "session.close", direction: "target-to-client"}),
|
|
827
|
+
encodeCompactData({sequence: 4, payload: Buffer.from("late")})
|
|
828
|
+
]))
|
|
829
|
+
await settleRelayFrames()
|
|
830
|
+
|
|
831
|
+
expect(chunks).toEqual([])
|
|
832
|
+
expect(errors).toEqual([])
|
|
833
|
+
expect(socket.bytesRead).toBe(0)
|
|
834
|
+
expect(socket.destroyed).toBe(false)
|
|
835
|
+
expect(socket.readable).toBe(false)
|
|
836
|
+
const closed = new Promise((resolve) => socket.once("close", resolve))
|
|
837
|
+
socket.destroy()
|
|
838
|
+
await settleRelayFrames()
|
|
839
|
+
expect(writtenHeaders(relay).at(-1)?.type).toBe("session.close")
|
|
840
|
+
relay.emit("data", encodeFrame({type: "session.close", direction: "target-to-client", acknowledged: true}))
|
|
841
|
+
await withDeadline(closed, "timed out waiting for logical close acknowledgement")
|
|
842
|
+
} finally {
|
|
843
|
+
await destroyAndWait(socket)
|
|
844
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
845
|
+
}
|
|
881
846
|
})
|
|
882
|
-
socket.write("hello")
|
|
883
|
-
assert.equal((await readOnce(socket)).toString("utf8"), "factory:hello")
|
|
884
847
|
|
|
885
|
-
socket
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
848
|
+
it("connectSocketduct creates and connects the public socket option shape", async () => {
|
|
849
|
+
const {rootDir} = await createTestSpool()
|
|
850
|
+
const echoServer = createServer((socket) => {
|
|
851
|
+
socket.on("data", (chunk) => socket.write(Buffer.from(`factory:${chunk.toString("utf8")}`)))
|
|
852
|
+
})
|
|
853
|
+
const targetPort = await listen(echoServer)
|
|
854
|
+
|
|
855
|
+
const users = new InMemoryUserStore()
|
|
856
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
857
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
858
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
859
|
+
const relay = await startTcpRelay({
|
|
860
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
861
|
+
tokens,
|
|
862
|
+
sessions: new SessionStore({now: () => 1000})
|
|
894
863
|
})
|
|
895
|
-
})
|
|
896
|
-
await rm(rootDir, {recursive: true, force: true})
|
|
897
|
-
})
|
|
898
864
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const users = new InMemoryUserStore()
|
|
907
|
-
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
908
|
-
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
909
|
-
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
910
|
-
const relay = await startTcpRelay({
|
|
911
|
-
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
912
|
-
tokens,
|
|
913
|
-
sessions: new SessionStore({now: () => 1000})
|
|
914
|
-
})
|
|
865
|
+
const socket = connectSocketduct({
|
|
866
|
+
relay: {host: "127.0.0.1", port: relay.port, token: token.value},
|
|
867
|
+
sessionName: "public socket options",
|
|
868
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
869
|
+
spoolDirectory: rootDir,
|
|
870
|
+
reconnect: {initialDelayMs: 10}
|
|
871
|
+
})
|
|
915
872
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
873
|
+
await new Promise((resolve, reject) => {
|
|
874
|
+
socket.once("error", reject)
|
|
875
|
+
socket.once("connect", () => {
|
|
876
|
+
socket.off("error", reject)
|
|
877
|
+
resolve(undefined)
|
|
878
|
+
})
|
|
879
|
+
})
|
|
880
|
+
socket.write("hello")
|
|
881
|
+
expect((await readOnce(socket)).toString("utf8")).toBe("factory:hello")
|
|
925
882
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
883
|
+
socket.destroy()
|
|
884
|
+
await relay.close()
|
|
885
|
+
await new Promise((resolve, reject) => {
|
|
886
|
+
echoServer.close((error) => {
|
|
887
|
+
if (error) {
|
|
888
|
+
reject(error)
|
|
889
|
+
return
|
|
890
|
+
}
|
|
891
|
+
resolve(undefined)
|
|
892
|
+
})
|
|
932
893
|
})
|
|
894
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
933
895
|
})
|
|
934
|
-
socket.write("hello")
|
|
935
|
-
assert.equal((await readOnce(socket)).toString("utf8"), "cleanup:hello")
|
|
936
896
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
socket.
|
|
941
|
-
|
|
897
|
+
it("SocketductSocket cleans up spool directory on destroy", async () => {
|
|
898
|
+
const {rootDir, spool} = await createTestSpool()
|
|
899
|
+
const echoServer = createServer((socket) => {
|
|
900
|
+
socket.on("data", (chunk) => socket.write(Buffer.from(`cleanup:${chunk.toString("utf8")}`)))
|
|
901
|
+
})
|
|
902
|
+
const targetPort = await listen(echoServer)
|
|
903
|
+
|
|
904
|
+
const users = new InMemoryUserStore()
|
|
905
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
906
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
907
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
908
|
+
const relay = await startTcpRelay({
|
|
909
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
910
|
+
tokens,
|
|
911
|
+
sessions: new SessionStore({now: () => 1000})
|
|
942
912
|
})
|
|
943
|
-
})
|
|
944
|
-
socket.destroy()
|
|
945
|
-
await closePromise
|
|
946
913
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
914
|
+
const socket = new SocketductSocket({
|
|
915
|
+
relayHost: "127.0.0.1",
|
|
916
|
+
relayPort: relay.port,
|
|
917
|
+
token: token.value,
|
|
918
|
+
sessionName: "spool cleanup on destroy",
|
|
919
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
920
|
+
spool,
|
|
921
|
+
reconnectDelayMs: 10
|
|
922
|
+
})
|
|
950
923
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
resolve(undefined)
|
|
924
|
+
socket.connect()
|
|
925
|
+
await new Promise((resolve, reject) => {
|
|
926
|
+
socket.once("error", reject)
|
|
927
|
+
socket.once("connect", () => {
|
|
928
|
+
socket.off("error", reject)
|
|
929
|
+
resolve(undefined)
|
|
930
|
+
})
|
|
959
931
|
})
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
})
|
|
932
|
+
socket.write("hello")
|
|
933
|
+
expect((await readOnce(socket)).toString("utf8")).toBe("cleanup:hello")
|
|
963
934
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
974
|
-
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
975
|
-
const relay = await startTcpRelay({
|
|
976
|
-
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
977
|
-
tokens,
|
|
978
|
-
sessions: new SessionStore({now: () => 1000})
|
|
979
|
-
})
|
|
935
|
+
const closePromise = new Promise((resolve, hookReject) => {
|
|
936
|
+
socket.on("error", hookReject)
|
|
937
|
+
socket.on("close", () => {
|
|
938
|
+
socket.off("error", hookReject)
|
|
939
|
+
resolve(undefined)
|
|
940
|
+
})
|
|
941
|
+
})
|
|
942
|
+
socket.destroy()
|
|
943
|
+
await closePromise
|
|
980
944
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
relayPort: relay.port,
|
|
984
|
-
token: token.value,
|
|
985
|
-
sessionName: "spool cleanup on end",
|
|
986
|
-
target: {host: "127.0.0.1", port: targetPort},
|
|
987
|
-
spool,
|
|
988
|
-
reconnectDelayMs: 10
|
|
989
|
-
})
|
|
945
|
+
const entries = await readdir(rootDir)
|
|
946
|
+
expect(entries).toEqual([])
|
|
990
947
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
948
|
+
await relay.close()
|
|
949
|
+
await new Promise((resolve, reject) => {
|
|
950
|
+
echoServer.close((error) => {
|
|
951
|
+
if (error) {
|
|
952
|
+
reject(error)
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
resolve(undefined)
|
|
956
|
+
})
|
|
997
957
|
})
|
|
958
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
998
959
|
})
|
|
999
|
-
socket.write("endtest")
|
|
1000
960
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
socket.
|
|
1005
|
-
|
|
961
|
+
it("SocketductSocket end cleans up spool directory before finish event", async () => {
|
|
962
|
+
const {rootDir, spool} = await createTestSpool()
|
|
963
|
+
const echoServer = createServer((socket) => {
|
|
964
|
+
socket.on("data", (chunk) => socket.write(Buffer.from(`endclean:${chunk.toString("utf8")}`)))
|
|
965
|
+
})
|
|
966
|
+
const targetPort = await listen(echoServer)
|
|
967
|
+
|
|
968
|
+
const users = new InMemoryUserStore()
|
|
969
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
970
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
971
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
972
|
+
const relay = await startTcpRelay({
|
|
973
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
974
|
+
tokens,
|
|
975
|
+
sessions: new SessionStore({now: () => 1000})
|
|
1006
976
|
})
|
|
1007
|
-
})
|
|
1008
|
-
socket.end()
|
|
1009
|
-
await finishPromise
|
|
1010
977
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
978
|
+
const socket = new SocketductSocket({
|
|
979
|
+
relayHost: "127.0.0.1",
|
|
980
|
+
relayPort: relay.port,
|
|
981
|
+
token: token.value,
|
|
982
|
+
sessionName: "spool cleanup on end",
|
|
983
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
984
|
+
spool,
|
|
985
|
+
reconnectDelayMs: 10
|
|
986
|
+
})
|
|
1014
987
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1023
|
-
resolve(undefined)
|
|
988
|
+
socket.connect()
|
|
989
|
+
await new Promise((resolve, reject) => {
|
|
990
|
+
socket.once("error", reject)
|
|
991
|
+
socket.once("connect", () => {
|
|
992
|
+
socket.off("error", reject)
|
|
993
|
+
resolve(undefined)
|
|
994
|
+
})
|
|
1024
995
|
})
|
|
1025
|
-
|
|
1026
|
-
await rm(rootDir, {recursive: true, force: true})
|
|
1027
|
-
})
|
|
996
|
+
socket.write("endtest")
|
|
1028
997
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
1039
|
-
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
1040
|
-
const relay = await startTcpRelay({
|
|
1041
|
-
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
1042
|
-
tokens,
|
|
1043
|
-
sessions: new SessionStore({now: () => 1000})
|
|
1044
|
-
})
|
|
998
|
+
const finishPromise = new Promise((resolve, hookReject) => {
|
|
999
|
+
socket.on("error", hookReject)
|
|
1000
|
+
socket.on("finish", () => {
|
|
1001
|
+
socket.off("error", hookReject)
|
|
1002
|
+
resolve(undefined)
|
|
1003
|
+
})
|
|
1004
|
+
})
|
|
1005
|
+
socket.end()
|
|
1006
|
+
await finishPromise
|
|
1045
1007
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
sessionName: "public maxBytes",
|
|
1049
|
-
target: {host: "127.0.0.1", port: targetPort},
|
|
1050
|
-
spoolDirectory: rootDir2,
|
|
1051
|
-
maxBytes: 50,
|
|
1052
|
-
reconnect: {initialDelayMs: 10}
|
|
1053
|
-
})
|
|
1008
|
+
const entries2 = await readdir(rootDir)
|
|
1009
|
+
expect(entries2).toEqual([])
|
|
1054
1010
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1011
|
+
socket.destroy()
|
|
1012
|
+
await relay.close()
|
|
1013
|
+
await new Promise((resolve, reject) => {
|
|
1014
|
+
echoServer.close((error) => {
|
|
1015
|
+
if (error) {
|
|
1016
|
+
reject(error)
|
|
1017
|
+
return
|
|
1018
|
+
}
|
|
1019
|
+
resolve(undefined)
|
|
1020
|
+
})
|
|
1060
1021
|
})
|
|
1022
|
+
await rm(rootDir, {recursive: true, force: true})
|
|
1061
1023
|
})
|
|
1062
1024
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
resolve(undefined)
|
|
1025
|
+
it("connectSocketduct forwards maxBytes to spool storage", async () => {
|
|
1026
|
+
const rootDir2 = await mkdtemp(join(tmpdir(), "socketduct-socket-test-"))
|
|
1027
|
+
const echoServer = createServer((socket) => {
|
|
1028
|
+
socket.on("data", (chunk) => socket.write(Buffer.from(`maxbytes:${chunk.toString("utf8")}`)))
|
|
1029
|
+
})
|
|
1030
|
+
const targetPort = await listen(echoServer)
|
|
1031
|
+
|
|
1032
|
+
const users = new InMemoryUserStore()
|
|
1033
|
+
const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
|
|
1034
|
+
await users.createUser({username: "kasper", password: "correct horse battery staple"})
|
|
1035
|
+
const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
|
|
1036
|
+
const relay = await startTcpRelay({
|
|
1037
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
|
|
1038
|
+
tokens,
|
|
1039
|
+
sessions: new SessionStore({now: () => 1000})
|
|
1080
1040
|
})
|
|
1081
|
-
})
|
|
1082
|
-
await rm(rootDir2, {recursive: true, force: true})
|
|
1083
|
-
})
|
|
1084
1041
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1042
|
+
const socket = connectSocketduct({
|
|
1043
|
+
relay: {host: "127.0.0.1", port: relay.port, token: token.value},
|
|
1044
|
+
sessionName: "public maxBytes",
|
|
1045
|
+
target: {host: "127.0.0.1", port: targetPort},
|
|
1046
|
+
spoolDirectory: rootDir2,
|
|
1047
|
+
maxBytes: 50,
|
|
1048
|
+
reconnect: {initialDelayMs: 10}
|
|
1049
|
+
})
|
|
1087
1050
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1051
|
+
await new Promise((resolve, reject) => {
|
|
1052
|
+
socket.once("error", reject)
|
|
1053
|
+
socket.once("connect", () => {
|
|
1054
|
+
socket.off("error", reject)
|
|
1055
|
+
resolve(undefined)
|
|
1056
|
+
})
|
|
1094
1057
|
})
|
|
1095
1058
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1059
|
+
socket.write(Buffer.alloc(60))
|
|
1060
|
+
const errorPromise = new Promise((resolve) => {
|
|
1061
|
+
socket.once("error", (err) => resolve(err))
|
|
1062
|
+
})
|
|
1063
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve(null), 500))
|
|
1064
|
+
const result = await Promise.race([errorPromise, timeout])
|
|
1065
|
+
expect(result).toBeInstanceOf(Error) // Expected spool quota error to surface as an error event
|
|
1098
1066
|
|
|
1099
1067
|
socket.destroy()
|
|
1100
|
-
|
|
1101
|
-
await
|
|
1102
|
-
|
|
1103
|
-
|
|
1068
|
+
await relay.close()
|
|
1069
|
+
await new Promise((resolve, reject) => {
|
|
1070
|
+
echoServer.close((error) => {
|
|
1071
|
+
if (error) {
|
|
1072
|
+
reject(error)
|
|
1073
|
+
return
|
|
1074
|
+
}
|
|
1075
|
+
resolve(undefined)
|
|
1076
|
+
})
|
|
1077
|
+
})
|
|
1078
|
+
await rm(rootDir2, {recursive: true, force: true})
|
|
1079
|
+
})
|
|
1104
1080
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1081
|
+
it("createSocketductConnectionFactory forwards maxAgeMs to factory options", async () => {
|
|
1082
|
+
const {createSocketductConnectionFactory} = await import("../../../src/client/connect.js")
|
|
1107
1083
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
})
|
|
1084
|
+
const spoolDir = await mkdtemp(join(tmpdir(), "socketduct-factory-test-"))
|
|
1085
|
+
try {
|
|
1086
|
+
const factory = createSocketductConnectionFactory({
|
|
1087
|
+
relay: {host: "127.0.0.1", port: 9999, token: "test-token"},
|
|
1088
|
+
spoolDirectory: spoolDir,
|
|
1089
|
+
maxAgeMs: 1
|
|
1090
|
+
})
|
|
1116
1091
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1092
|
+
const socket = factory({host: "127.0.0.1", port: 2375})
|
|
1093
|
+
expect(socket).toBeInstanceOf(SocketductSocket)
|
|
1119
1094
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1095
|
+
socket.destroy()
|
|
1096
|
+
} finally {
|
|
1097
|
+
await rm(spoolDir, {recursive: true, force: true})
|
|
1098
|
+
}
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
it("SocketductHttpAgent forwards maxBytes to createConnection sockets", async () => {
|
|
1102
|
+
const {SocketductHttpAgent} = await import("../../../src/client/http-agent.js")
|
|
1103
|
+
|
|
1104
|
+
const spoolDir = await mkdtemp(join(tmpdir(), "socketduct-agent-test-"))
|
|
1105
|
+
try {
|
|
1106
|
+
const agent = new SocketductHttpAgent({
|
|
1107
|
+
relay: {host: "127.0.0.1", port: 9999, token: "test-token"},
|
|
1108
|
+
target: {host: "127.0.0.1", port: 2375},
|
|
1109
|
+
spoolDirectory: spoolDir,
|
|
1110
|
+
maxBytes: 200
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
const socket = agent.createConnection({})
|
|
1114
|
+
expect(socket).toBeInstanceOf(SocketductSocket)
|
|
1115
|
+
|
|
1116
|
+
socket.destroy()
|
|
1117
|
+
} finally {
|
|
1118
|
+
await rm(spoolDir, {recursive: true, force: true})
|
|
1119
|
+
}
|
|
1120
|
+
})
|
|
1124
1121
|
})
|