socketduct 0.1.25 → 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.
Files changed (56) hide show
  1. package/AGENTS.md +2 -2
  2. package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
  3. package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
  4. package/package.json +2 -2
  5. package/scripts/test.js +9 -14
  6. package/src/logging/compressed-log-writer.js +1 -1
  7. package/src/logging/zstd-frame-codec.js +15 -1
  8. package/test/e2e/gateway-process.test.js +93 -92
  9. package/test/e2e/logger-shutdown-process.test.js +40 -37
  10. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  11. package/test/e2e/reverse-gateway-process.test.js +328 -318
  12. package/test/integration/initial-target-recovery.test.js +333 -334
  13. package/test/integration/logging-cli.test.js +121 -119
  14. package/test/integration/logging-compressed-recovery.test.js +96 -95
  15. package/test/integration/multiplexed-compatibility.test.js +178 -177
  16. package/test/integration/multiplexed-corrections.test.js +360 -359
  17. package/test/integration/multiplexed-failures.test.js +253 -251
  18. package/test/integration/multiplexed-transport-set.test.js +334 -322
  19. package/test/integration/multiplexed-transport.test.js +135 -134
  20. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  21. package/test/integration/reverse-gateway.test.js +2309 -2218
  22. package/test/integration/reverse-resume-continuity.test.js +219 -218
  23. package/test/integration/reverse-route-class.test.js +358 -341
  24. package/test/integration/socketduct-http-agent.test.js +314 -312
  25. package/test/integration/socketduct-https-agent.test.js +516 -485
  26. package/test/integration/stream-compression.test.js +862 -824
  27. package/test/integration/tcp-forwarding.test.js +2550 -2447
  28. package/test/integration/tls-forwarding.test.js +593 -592
  29. package/test/unit/auth/auth.test.js +43 -44
  30. package/test/unit/auth/static-token-store.test.js +9 -8
  31. package/test/unit/client/connect.test.js +41 -40
  32. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  33. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  34. package/test/unit/client/multiplexed-transport-set.test.js +830 -748
  35. package/test/unit/client/socketduct-socket.test.js +768 -771
  36. package/test/unit/client/virtual-socket.test.js +198 -168
  37. package/test/unit/config/docker-gateway-files.test.js +224 -224
  38. package/test/unit/config/environment.test.js +472 -492
  39. package/test/unit/index.test.js +53 -52
  40. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  41. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  42. package/test/unit/logging/log-retention.test.js +52 -47
  43. package/test/unit/logging/log-segment-index.test.js +84 -81
  44. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  45. package/test/unit/protocol/compression.test.js +254 -298
  46. package/test/unit/protocol/frame.test.js +280 -276
  47. package/test/unit/protocol/v3-compression.test.js +21 -23
  48. package/test/unit/protocol/v3-frame.test.js +42 -41
  49. package/test/unit/release-configuration.test.js +15 -12
  50. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  51. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  52. package/test/unit/server/control-handler.test.js +232 -231
  53. package/test/unit/server/primary.test.js +51 -49
  54. package/test/unit/server/relay-runtime.test.js +280 -294
  55. package/test/unit/sessions/session.test.js +48 -62
  56. package/test/unit/spool/spool-file.test.js +401 -385
@@ -1,11 +1,10 @@
1
- import assert from "node:assert/strict"
1
+ import {describe, expect, it} from "@velocious/testing"
2
2
  import {mkdtemp, readdir, rm} from "node:fs/promises"
3
3
  import http from "node:http"
4
4
  import net from "node:net"
5
5
  import {tmpdir} from "node:os"
6
6
  import {join} from "node:path"
7
7
  import {setTimeout as delay} from "node:timers/promises"
8
- import test from "node:test"
9
8
 
10
9
  import {InMemoryUserStore, login, TokenStore} from "../../src/auth/user-store.js"
11
10
  import {SocketductHttpAgent} from "../../src/client/http-agent.js"
@@ -92,344 +91,347 @@ async function request(options) {
92
91
  })
93
92
  }
94
93
 
95
- test("SocketductHttpAgent sends HTTP requests over a Socketduct socket", async () => {
96
- const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-test-"))
97
- const targetServer = http.createServer((req, res) => {
98
- if (req.url === "/_ping") {
99
- res.writeHead(200, {"content-type": "text/plain"})
100
- res.end("OK")
101
- return
102
- }
103
- res.writeHead(404)
104
- res.end("missing")
105
- })
106
- const targetPort = await listen(targetServer)
107
-
108
- const users = new InMemoryUserStore()
109
- const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
110
- await users.createUser({username: "kasper", password: "correct horse battery staple"})
111
- const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
112
- const relay = await startTcpRelay({
113
- authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
114
- tokens,
115
- sessions: new SessionStore({now: () => 1000})
116
- })
117
- const agent = new SocketductHttpAgent({
118
- relay: {host: "127.0.0.1", port: relay.port, token: token.value},
119
- target: {host: "127.0.0.1", port: targetPort},
120
- spoolDirectory,
121
- sessionNamePrefix: "http-agent"
94
+ describe("socketduct http agent", () => {
95
+ it("SocketductHttpAgent sends HTTP requests over a Socketduct socket", async () => {
96
+ const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-test-"))
97
+ const targetServer = http.createServer((req, res) => {
98
+ if (req.url === "/_ping") {
99
+ res.writeHead(200, {"content-type": "text/plain"})
100
+ res.end("OK")
101
+ return
102
+ }
103
+ res.writeHead(404)
104
+ res.end("missing")
105
+ })
106
+ const targetPort = await listen(targetServer)
107
+
108
+ const users = new InMemoryUserStore()
109
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
110
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
111
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
112
+ const relay = await startTcpRelay({
113
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
114
+ tokens,
115
+ sessions: new SessionStore({now: () => 1000})
116
+ })
117
+ const agent = new SocketductHttpAgent({
118
+ relay: {host: "127.0.0.1", port: relay.port, token: token.value},
119
+ target: {host: "127.0.0.1", port: targetPort},
120
+ spoolDirectory,
121
+ sessionNamePrefix: "http-agent"
122
+ })
123
+
124
+ const response = await request({agent, host: "docker", port: 80, path: "/_ping", method: "GET"})
125
+
126
+ expect(response.statusCode).toBe(200)
127
+ expect(response.body).toBe("OK")
128
+ agent.destroy()
129
+ await relay.close()
130
+ await closeServer(targetServer)
131
+ await rm(spoolDirectory, {recursive: true, force: true})
122
132
  })
123
133
 
124
- const response = await request({agent, host: "docker", port: 80, path: "/_ping", method: "GET"})
134
+ it("SocketductHttpAgent reuses keep-alive sockets", async () => {
135
+ const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-keepalive-test-"))
136
+ let targetConnections = 0
137
+ const targetServer = http.createServer((req, res) => {
138
+ res.writeHead(200, {"content-type": "text/plain"})
139
+ res.end(`pong:${req.url}`)
140
+ })
141
+ targetServer.on("connection", () => {
142
+ targetConnections += 1
143
+ })
144
+ const targetPort = await listen(targetServer)
145
+
146
+ const users = new InMemoryUserStore()
147
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
148
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
149
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
150
+ const relay = await startTcpRelay({
151
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
152
+ tokens,
153
+ sessions: new SessionStore({now: () => 1000})
154
+ })
155
+ const agent = new SocketductHttpAgent({
156
+ relay: {host: "127.0.0.1", port: relay.port, token: token.value},
157
+ target: {host: "127.0.0.1", port: targetPort},
158
+ spoolDirectory,
159
+ sessionNamePrefix: "http-agent-keepalive",
160
+ keepAlive: true,
161
+ maxSockets: 1
162
+ })
125
163
 
126
- assert.equal(response.statusCode, 200)
127
- assert.equal(response.body, "OK")
128
- agent.destroy()
129
- await relay.close()
130
- await closeServer(targetServer)
131
- await rm(spoolDirectory, {recursive: true, force: true})
132
- })
164
+ const first = await request({agent, host: "docker", port: 80, path: "/one", method: "GET"})
165
+ const second = await request({agent, host: "docker", port: 80, path: "/two", method: "GET"})
133
166
 
134
- test("SocketductHttpAgent reuses keep-alive sockets", async () => {
135
- const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-keepalive-test-"))
136
- let targetConnections = 0
137
- const targetServer = http.createServer((req, res) => {
138
- res.writeHead(200, {"content-type": "text/plain"})
139
- res.end(`pong:${req.url}`)
140
- })
141
- targetServer.on("connection", () => {
142
- targetConnections += 1
143
- })
144
- const targetPort = await listen(targetServer)
145
-
146
- const users = new InMemoryUserStore()
147
- const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
148
- await users.createUser({username: "kasper", password: "correct horse battery staple"})
149
- const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
150
- const relay = await startTcpRelay({
151
- authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
152
- tokens,
153
- sessions: new SessionStore({now: () => 1000})
154
- })
155
- const agent = new SocketductHttpAgent({
156
- relay: {host: "127.0.0.1", port: relay.port, token: token.value},
157
- target: {host: "127.0.0.1", port: targetPort},
158
- spoolDirectory,
159
- sessionNamePrefix: "http-agent-keepalive",
160
- keepAlive: true,
161
- maxSockets: 1
167
+ expect(first.body).toBe("pong:/one")
168
+ expect(second.body).toBe("pong:/two")
169
+ expect(targetConnections).toBe(1)
170
+ agent.destroy()
171
+ await waitUntil(() => {
172
+ const status = relay.status()
173
+ return status.sessions === 0 && status.activeTunnels === 0 && status.activeTargets === 0
174
+ })
175
+ expect(relay.status()).toEqual(EMPTY_RELAY_STATUS)
176
+ await relay.close()
177
+ await closeServer(targetServer)
178
+ await rm(spoolDirectory, {recursive: true, force: true})
162
179
  })
163
180
 
164
- const first = await request({agent, host: "docker", port: 80, path: "/one", method: "GET"})
165
- const second = await request({agent, host: "docker", port: 80, path: "/two", method: "GET"})
181
+ it("SocketductHttpAgent closes a reused session after target EOF through a TLS relay", async () => {
182
+ const root = await mkdtemp(join(tmpdir(), "socketduct-http-agent-tls-relay-close-"))
183
+ const certs = generateTestCertificates(join(root, "certs"))
184
+ const spoolDirectory = join(root, "spool")
185
+ /** @type {Set<import('node:net').Socket>} */
186
+ const targetSockets = new Set()
187
+ const targetServer = http.createServer((req, res) => {
188
+ if (req.url === "/close") res.setHeader("connection", "close")
189
+ res.end(req.url === "/close" ? "tls-relay-close" : "tls-relay-keepalive")
190
+ })
191
+ targetServer.on("connection", (socket) => {
192
+ targetSockets.add(socket)
193
+ socket.once("close", () => targetSockets.delete(socket))
194
+ })
195
+ const targetPort = await listen(targetServer)
196
+
197
+ const users = new InMemoryUserStore()
198
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
199
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
200
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
201
+ const relay = await startTcpRelay({
202
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
203
+ tokens,
204
+ sessions: new SessionStore({now: () => 1000}),
205
+ tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
206
+ })
207
+ /** @type {(() => void) | undefined} */
208
+ let resolveSocketClosed
209
+ /** @type {import('../../src/client/socketduct-socket.js').SocketductSocket | undefined} */
210
+ let observedSocket
211
+ const socketClosed = new Promise((resolve) => { resolveSocketClosed = () => resolve(undefined) })
212
+ class ObservedSocketductHttpAgent extends SocketductHttpAgent {
213
+ /**
214
+ * @override
215
+ * @param {import('node:http').ClientRequestArgs} options - HTTP connection options.
216
+ * @param {(error: Error | null, socket: import('../../src/client/socketduct-socket.js').SocketductSocket) => void} [callback] - Agent callback.
217
+ * @returns {import('../../src/client/socketduct-socket.js').SocketductSocket} Socketduct socket.
218
+ */
219
+ createConnection(options, callback) {
220
+ const socket = super.createConnection(options, callback)
221
+ observedSocket = socket
222
+ socket.once("close", () => resolveSocketClosed?.())
223
+ return socket
224
+ }
225
+ }
226
+ const agent = new ObservedSocketductHttpAgent({
227
+ relay: {host: "127.0.0.1", port: relay.port, token: token.value},
228
+ target: {host: "127.0.0.1", port: targetPort},
229
+ spoolDirectory,
230
+ sessionNamePrefix: "http-agent-tls-relay-close",
231
+ keepAlive: true,
232
+ maxSockets: 1,
233
+ tls: {caFile: certs.caCertFile, servername: "localhost", rejectUnauthorized: true}
234
+ })
166
235
 
167
- assert.equal(first.body, "pong:/one")
168
- assert.equal(second.body, "pong:/two")
169
- assert.equal(targetConnections, 1)
170
- agent.destroy()
171
- await waitUntil(() => {
172
- const status = relay.status()
173
- return status.sessions === 0 && status.activeTunnels === 0 && status.activeTargets === 0
174
- })
175
- assert.deepEqual(relay.status(), EMPTY_RELAY_STATUS)
176
- await relay.close()
177
- await closeServer(targetServer)
178
- await rm(spoolDirectory, {recursive: true, force: true})
179
- })
236
+ try {
237
+ expect(await request({agent, host: "docker", port: 80, path: "/keepalive", method: "GET"})).toEqual({statusCode: 200, body: "tls-relay-keepalive"})
238
+ expect(targetSockets.size).toBe(1)
239
+ expect(relay.status().attachedSessions).toBe(1)
240
+ expect(await request({agent, host: "docker", port: 80, path: "/close", method: "GET"})).toEqual({statusCode: 200, body: "tls-relay-close"})
241
+ await socketClosed
242
+
243
+ agent.destroy()
244
+ agent.destroy()
245
+ await waitUntil(async () => relay.status().sessions === 0 && targetSockets.size === 0 &&
246
+ (await readdir(spoolDirectory).catch((error) => {
247
+ if (error instanceof Error && /** @type {Error & {code?: string}} */ (error).code === "ENOENT") return []
248
+ throw error
249
+ })).length === 0).catch((error) => {
250
+ throw new Error(`TLS relay resources did not return to zero: ${JSON.stringify(relay.status())}; targets=${targetSockets.size}`, {cause: error})
251
+ })
180
252
 
181
- test("SocketductHttpAgent closes a reused session after target EOF through a TLS relay", async () => {
182
- const root = await mkdtemp(join(tmpdir(), "socketduct-http-agent-tls-relay-close-"))
183
- const certs = generateTestCertificates(join(root, "certs"))
184
- const spoolDirectory = join(root, "spool")
185
- /** @type {Set<import('node:net').Socket>} */
186
- const targetSockets = new Set()
187
- const targetServer = http.createServer((req, res) => {
188
- if (req.url === "/close") res.setHeader("connection", "close")
189
- res.end(req.url === "/close" ? "tls-relay-close" : "tls-relay-keepalive")
190
- })
191
- targetServer.on("connection", (socket) => {
192
- targetSockets.add(socket)
193
- socket.once("close", () => targetSockets.delete(socket))
194
- })
195
- const targetPort = await listen(targetServer)
196
-
197
- const users = new InMemoryUserStore()
198
- const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
199
- await users.createUser({username: "kasper", password: "correct horse battery staple"})
200
- const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
201
- const relay = await startTcpRelay({
202
- authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
203
- tokens,
204
- sessions: new SessionStore({now: () => 1000}),
205
- tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
206
- })
207
- /** @type {(() => void) | undefined} */
208
- let resolveSocketClosed
209
- /** @type {import('../../src/client/socketduct-socket.js').SocketductSocket | undefined} */
210
- let observedSocket
211
- const socketClosed = new Promise((resolve) => { resolveSocketClosed = () => resolve(undefined) })
212
- class ObservedSocketductHttpAgent extends SocketductHttpAgent {
213
- /**
214
- * @override
215
- * @param {import('node:http').ClientRequestArgs} options - HTTP connection options.
216
- * @param {(error: Error | null, socket: import('../../src/client/socketduct-socket.js').SocketductSocket) => void} [callback] - Agent callback.
217
- * @returns {import('../../src/client/socketduct-socket.js').SocketductSocket} Socketduct socket.
218
- */
219
- createConnection(options, callback) {
220
- const socket = super.createConnection(options, callback)
221
- observedSocket = socket
222
- socket.once("close", () => resolveSocketClosed?.())
223
- return socket
253
+ expect(relay.status()).toEqual(EMPTY_RELAY_STATUS)
254
+ expect(Object.keys(agent.sockets).length).toBe(0)
255
+ expect(Object.keys(agent.freeSockets).length).toBe(0)
256
+ expect(Object.keys(agent.requests).length).toBe(0)
257
+ expect(observedSocket?.listenerCount("connect")).toBe(0)
258
+ observedSocket = undefined
259
+ } finally {
260
+ agent.destroy()
261
+ await relay.close()
262
+ for (const socket of targetSockets) socket.destroy()
263
+ await closeServer(targetServer)
264
+ await rm(root, {recursive: true, force: true})
224
265
  }
225
- }
226
- const agent = new ObservedSocketductHttpAgent({
227
- relay: {host: "127.0.0.1", port: relay.port, token: token.value},
228
- target: {host: "127.0.0.1", port: targetPort},
229
- spoolDirectory,
230
- sessionNamePrefix: "http-agent-tls-relay-close",
231
- keepAlive: true,
232
- maxSockets: 1,
233
- tls: {caFile: certs.caCertFile, servername: "localhost", rejectUnauthorized: true}
234
266
  })
235
267
 
236
- try {
237
- assert.deepEqual(
238
- await request({agent, host: "docker", port: 80, path: "/keepalive", method: "GET"}),
239
- {statusCode: 200, body: "tls-relay-keepalive"}
240
- )
241
- assert.equal(targetSockets.size, 1)
242
- assert.equal(relay.status().attachedSessions, 1)
243
- assert.deepEqual(
244
- await request({agent, host: "docker", port: 80, path: "/close", method: "GET"}),
245
- {statusCode: 200, body: "tls-relay-close"}
246
- )
247
- await socketClosed
248
-
249
- agent.destroy()
250
- agent.destroy()
251
- await waitUntil(async () => relay.status().sessions === 0 && targetSockets.size === 0 &&
252
- (await readdir(spoolDirectory).catch((error) => {
253
- if (error instanceof Error && /** @type {Error & {code?: string}} */ (error).code === "ENOENT") return []
254
- throw error
255
- })).length === 0).catch((error) => {
256
- throw new Error(`TLS relay resources did not return to zero: ${JSON.stringify(relay.status())}; targets=${targetSockets.size}`, {cause: error})
268
+ it("SocketductHttpAgent reports unauthorized targets as HTTP request errors", async () => {
269
+ const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-deny-test-"))
270
+ const users = new InMemoryUserStore()
271
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
272
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
273
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
274
+ const relay = await startTcpRelay({
275
+ authorizeTarget: () => false,
276
+ tokens,
277
+ sessions: new SessionStore({now: () => 1000})
278
+ })
279
+ const agent = new SocketductHttpAgent({
280
+ relay: {host: "127.0.0.1", port: relay.port, token: token.value},
281
+ target: {host: "127.0.0.1", port: 2375},
282
+ spoolDirectory,
283
+ sessionNamePrefix: "http-agent-denied"
257
284
  })
258
285
 
259
- assert.deepEqual(relay.status(), EMPTY_RELAY_STATUS)
260
- assert.equal(Object.keys(agent.sockets).length, 0)
261
- assert.equal(Object.keys(agent.freeSockets).length, 0)
262
- assert.equal(Object.keys(agent.requests).length, 0)
263
- assert.equal(observedSocket?.listenerCount("connect"), 0)
264
- observedSocket = undefined
265
- } finally {
286
+ /** @type {unknown} */
287
+ let error
288
+ try {
289
+ await request({agent, host: "docker", port: 80, path: "/_ping", method: "GET"})
290
+ } catch (caught) {
291
+ error = caught
292
+ }
293
+ expect(error).toBeDefined()
294
+ const relayError = /** @type {SocketductRelayError} */ (error)
295
+ expect(relayError).toBeInstanceOf(SocketductRelayError)
296
+ expect(relayError.code).toBe("TARGET_UNAUTHORIZED")
297
+ expect(relayError.retryable).toBe(false)
266
298
  agent.destroy()
267
299
  await relay.close()
268
- for (const socket of targetSockets) socket.destroy()
269
- await closeServer(targetServer)
270
- await rm(root, {recursive: true, force: true})
271
- }
272
- })
273
-
274
- test("SocketductHttpAgent reports unauthorized targets as HTTP request errors", async () => {
275
- const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-deny-test-"))
276
- const users = new InMemoryUserStore()
277
- const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
278
- await users.createUser({username: "kasper", password: "correct horse battery staple"})
279
- const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
280
- const relay = await startTcpRelay({
281
- authorizeTarget: () => false,
282
- tokens,
283
- sessions: new SessionStore({now: () => 1000})
284
- })
285
- const agent = new SocketductHttpAgent({
286
- relay: {host: "127.0.0.1", port: relay.port, token: token.value},
287
- target: {host: "127.0.0.1", port: 2375},
288
- spoolDirectory,
289
- sessionNamePrefix: "http-agent-denied"
300
+ await rm(spoolDirectory, {recursive: true, force: true})
290
301
  })
291
302
 
292
- await assert.rejects(request({agent, host: "docker", port: 80, path: "/_ping", method: "GET"}), (error) => {
293
- assert.ok(error instanceof SocketductRelayError)
294
- assert.equal(error.code, "TARGET_UNAUTHORIZED")
295
- assert.equal(error.retryable, false)
296
- return true
297
- })
298
- agent.destroy()
299
- await relay.close()
300
- await rm(spoolDirectory, {recursive: true, force: true})
301
- })
302
-
303
- test("SocketductHttpAgent callback receives a typed pre-connect relay failure exactly once", async () => {
304
- const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-callback-error-test-"))
305
- const users = new InMemoryUserStore()
306
- const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
307
- await users.createUser({username: "kasper", password: "correct horse battery staple"})
308
- const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
309
- const relay = await startTcpRelay({
310
- authorizeTarget: () => false,
311
- tokens,
312
- sessions: new SessionStore({now: () => 1000})
313
- })
314
- const agent = new SocketductHttpAgent({
315
- relay: {host: "127.0.0.1", port: relay.port, token: token.value},
316
- target: {host: "127.0.0.1", port: 2375},
317
- spoolDirectory,
318
- sessionNamePrefix: "http-agent-callback-error"
319
- })
303
+ it("SocketductHttpAgent callback receives a typed pre-connect relay failure exactly once", async () => {
304
+ const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-callback-error-test-"))
305
+ const users = new InMemoryUserStore()
306
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
307
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
308
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
309
+ const relay = await startTcpRelay({
310
+ authorizeTarget: () => false,
311
+ tokens,
312
+ sessions: new SessionStore({now: () => 1000})
313
+ })
314
+ const agent = new SocketductHttpAgent({
315
+ relay: {host: "127.0.0.1", port: relay.port, token: token.value},
316
+ target: {host: "127.0.0.1", port: 2375},
317
+ spoolDirectory,
318
+ sessionNamePrefix: "http-agent-callback-error"
319
+ })
320
320
 
321
- let callbackCount = 0
322
- const result = await new Promise((resolve) => {
323
- const socket = agent.createConnection({}, (error, connectedSocket) => {
324
- callbackCount += 1
325
- resolve({connectedSocket, error})
321
+ let callbackCount = 0
322
+ const result = await new Promise((resolve) => {
323
+ const socket = agent.createConnection({}, (error, connectedSocket) => {
324
+ callbackCount += 1
325
+ resolve({connectedSocket, error})
326
+ })
327
+ socket.on("error", () => {})
326
328
  })
327
- socket.on("error", () => {})
328
- })
329
329
 
330
- assert.equal(callbackCount, 1)
331
- assert.ok(result.error instanceof SocketductRelayError)
332
- assert.equal(result.error.code, "TARGET_UNAUTHORIZED")
333
- assert.equal(result.error.retryable, false)
334
- assert.ok(result.connectedSocket)
335
- agent.destroy()
336
- await relay.close()
337
- await rm(spoolDirectory, {recursive: true, force: true})
338
- })
330
+ expect(callbackCount).toBe(1)
331
+ expect(result.error).toBeInstanceOf(SocketductRelayError)
332
+ const relayError = /** @type {SocketductRelayError} */ (result.error)
333
+ expect(relayError.code).toBe("TARGET_UNAUTHORIZED")
334
+ expect(relayError.retryable).toBe(false)
335
+ expect(result.connectedSocket).toBeTruthy()
336
+ agent.destroy()
337
+ await relay.close()
338
+ await rm(spoolDirectory, {recursive: true, force: true})
339
+ })
339
340
 
340
- test("SocketductHttpAgent handles delayed streaming response close without relay socket errors", async () => {
341
- const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-stream-close-test-"))
342
- const targetServer = http.createServer((req, res) => {
343
- if (req.url === "/logs") {
344
- res.writeHead(200, {"content-type": "application/octet-stream"})
345
- void (async () => {
346
- res.write("chunk-one")
347
- await delay(10)
348
- res.write("chunk-two")
349
- await delay(10)
350
- res.end()
351
- })()
352
- return
353
- }
341
+ it("SocketductHttpAgent handles delayed streaming response close without relay socket errors", async () => {
342
+ const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-stream-close-test-"))
343
+ const targetServer = http.createServer((req, res) => {
344
+ if (req.url === "/logs") {
345
+ res.writeHead(200, {"content-type": "application/octet-stream"})
346
+ void (async () => {
347
+ res.write("chunk-one")
348
+ await delay(10)
349
+ res.write("chunk-two")
350
+ await delay(10)
351
+ res.end()
352
+ })()
353
+ return
354
+ }
354
355
 
355
- res.writeHead(404)
356
- res.end("missing")
357
- })
358
- const targetPort = await listen(targetServer)
359
-
360
- const users = new InMemoryUserStore()
361
- const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
362
- await users.createUser({username: "kasper", password: "correct horse battery staple"})
363
- const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
364
- const relay = await startTcpRelay({
365
- authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
366
- tokens,
367
- sessions: new SessionStore({now: () => 1000})
368
- })
369
- const agent = new SocketductHttpAgent({
370
- relay: {host: "127.0.0.1", port: relay.port, token: token.value},
371
- target: {host: "127.0.0.1", port: targetPort},
372
- spoolDirectory,
373
- sessionNamePrefix: "http-agent-stream-close"
374
- })
356
+ res.writeHead(404)
357
+ res.end("missing")
358
+ })
359
+ const targetPort = await listen(targetServer)
360
+
361
+ const users = new InMemoryUserStore()
362
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
363
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
364
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
365
+ const relay = await startTcpRelay({
366
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
367
+ tokens,
368
+ sessions: new SessionStore({now: () => 1000})
369
+ })
370
+ const agent = new SocketductHttpAgent({
371
+ relay: {host: "127.0.0.1", port: relay.port, token: token.value},
372
+ target: {host: "127.0.0.1", port: targetPort},
373
+ spoolDirectory,
374
+ sessionNamePrefix: "http-agent-stream-close"
375
+ })
375
376
 
376
- const response = await request({agent, host: "docker", port: 80, path: "/logs", method: "GET"})
377
+ const response = await request({agent, host: "docker", port: 80, path: "/logs", method: "GET"})
377
378
 
378
- assert.equal(response.statusCode, 200)
379
- assert.equal(response.body, "chunk-onechunk-two")
380
- agent.destroy()
381
- await relay.close()
382
- await closeServer(targetServer)
383
- await rm(spoolDirectory, {recursive: true, force: true})
384
- })
379
+ expect(response.statusCode).toBe(200)
380
+ expect(response.body).toBe("chunk-onechunk-two")
381
+ agent.destroy()
382
+ await relay.close()
383
+ await closeServer(targetServer)
384
+ await rm(spoolDirectory, {recursive: true, force: true})
385
+ })
385
386
 
386
- test("SocketductHttpAgent ends responses that rely on target TCP close", async () => {
387
- const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-target-eof-test-"))
388
- const targetServer = net.createServer((socket) => {
389
- socket.once("data", () => {
390
- socket.write("HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\n\r\n")
391
- socket.write("close-delimited-body")
392
- socket.end()
387
+ it("SocketductHttpAgent ends responses that rely on target TCP close", async () => {
388
+ const spoolDirectory = await mkdtemp(join(tmpdir(), "socketduct-http-agent-target-eof-test-"))
389
+ const targetServer = net.createServer((socket) => {
390
+ socket.once("data", () => {
391
+ socket.write("HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\n\r\n")
392
+ socket.write("close-delimited-body")
393
+ socket.end()
394
+ })
395
+ })
396
+ const targetPort = await listen(targetServer)
397
+
398
+ const users = new InMemoryUserStore()
399
+ const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
400
+ await users.createUser({username: "kasper", password: "correct horse battery staple"})
401
+ const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
402
+ const relay = await startTcpRelay({
403
+ authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
404
+ tokens,
405
+ sessions: new SessionStore({now: () => 1000})
406
+ })
407
+ const agent = new SocketductHttpAgent({
408
+ relay: {host: "127.0.0.1", port: relay.port, token: token.value},
409
+ target: {host: "127.0.0.1", port: targetPort},
410
+ spoolDirectory,
411
+ sessionNamePrefix: "http-agent-target-eof"
393
412
  })
394
- })
395
- const targetPort = await listen(targetServer)
396
-
397
- const users = new InMemoryUserStore()
398
- const tokens = new TokenStore({now: () => 1000, tokenTtlMs: 60_000})
399
- await users.createUser({username: "kasper", password: "correct horse battery staple"})
400
- const token = await login(users, tokens, {username: "kasper", password: "correct horse battery staple"})
401
- const relay = await startTcpRelay({
402
- authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort,
403
- tokens,
404
- sessions: new SessionStore({now: () => 1000})
405
- })
406
- const agent = new SocketductHttpAgent({
407
- relay: {host: "127.0.0.1", port: relay.port, token: token.value},
408
- target: {host: "127.0.0.1", port: targetPort},
409
- spoolDirectory,
410
- sessionNamePrefix: "http-agent-target-eof"
411
- })
412
413
 
413
- const timeout = AbortSignal.timeout(1000)
414
- const response = await new Promise((resolve, reject) => {
415
- const req = http.request({agent, host: "docker", port: 80, path: "/raw", method: "POST", signal: timeout}, (res) => {
416
- /** @type {Buffer[]} */
417
- const chunks = []
418
- res.on("data", (chunk) => chunks.push(chunk))
419
- res.on("end", () => resolve({statusCode: res.statusCode, body: Buffer.concat(chunks).toString("utf8")}))
414
+ const timeout = AbortSignal.timeout(1000)
415
+ const response = await new Promise((resolve, reject) => {
416
+ const req = http.request({agent, host: "docker", port: 80, path: "/raw", method: "POST", signal: timeout}, (res) => {
417
+ /** @type {Buffer[]} */
418
+ const chunks = []
419
+ res.on("data", (chunk) => chunks.push(chunk))
420
+ res.on("end", () => resolve({statusCode: res.statusCode, body: Buffer.concat(chunks).toString("utf8")}))
421
+ })
422
+ req.on("error", reject)
423
+ req.end("{}")
420
424
  })
421
- req.on("error", reject)
422
- req.end("{}")
423
- })
424
425
 
425
- assert.deepEqual(response, {statusCode: 200, body: "close-delimited-body"})
426
- agent.destroy()
427
- await waitUntil(() => {
428
- const status = relay.status()
429
- return status.sessions === 0 && status.activeTunnels === 0 && status.activeTargets === 0
426
+ expect(response).toEqual({statusCode: 200, body: "close-delimited-body"})
427
+ agent.destroy()
428
+ await waitUntil(() => {
429
+ const status = relay.status()
430
+ return status.sessions === 0 && status.activeTunnels === 0 && status.activeTargets === 0
431
+ })
432
+ expect(relay.status()).toEqual(EMPTY_RELAY_STATUS)
433
+ await relay.close()
434
+ await closeServer(targetServer)
435
+ await rm(spoolDirectory, {recursive: true, force: true})
430
436
  })
431
- assert.deepEqual(relay.status(), EMPTY_RELAY_STATUS)
432
- await relay.close()
433
- await closeServer(targetServer)
434
- await rm(spoolDirectory, {recursive: true, force: true})
435
437
  })