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.
- 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/logging/compressed-log-writer.js +1 -1
- package/src/logging/zstd-frame-codec.js +15 -1
- 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 +358 -341
- 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 +830 -748
- 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 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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
377
|
+
const response = await request({agent, host: "docker", port: 80, path: "/logs", method: "GET"})
|
|
377
378
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
})
|