socketduct 0.1.25 → 0.1.27
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/changelog.d/20261025070000-reclaim-unreadable-retention-lock.md +14 -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/log-retention.js +36 -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 +71 -48
- 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,7 +1,6 @@
|
|
|
1
1
|
/* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param-type, jsdoc/require-returns */
|
|
2
|
-
import
|
|
2
|
+
import {describe, expect, it, withTestCleanup} from "@velocious/testing"
|
|
3
3
|
import {createServer} from "node:net"
|
|
4
|
-
import test from "node:test"
|
|
5
4
|
|
|
6
5
|
import {StaticTokenStore} from "../../src/auth/static-token-store.js"
|
|
7
6
|
import {MultiplexedSocketductConnection} from "../../src/client/multiplexed-connection.js"
|
|
@@ -17,7 +16,7 @@ async function listen(server) {
|
|
|
17
16
|
})
|
|
18
17
|
})
|
|
19
18
|
const address = server.address()
|
|
20
|
-
|
|
19
|
+
if (!address || typeof address === "string") throw new Error("expected AddressInfo")
|
|
21
20
|
return address.port
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -50,7 +49,7 @@ async function readExactly(socket, expected) {
|
|
|
50
49
|
if (value.length < expected.length) return
|
|
51
50
|
cleanup()
|
|
52
51
|
try {
|
|
53
|
-
|
|
52
|
+
expect(value).toBe(expected)
|
|
54
53
|
resolve(undefined)
|
|
55
54
|
} catch (error) {
|
|
56
55
|
reject(error)
|
|
@@ -65,144 +64,146 @@ async function readExactly(socket, expected) {
|
|
|
65
64
|
})
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const relay = await startMultiplexedTcpRelay({
|
|
78
|
-
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
79
|
-
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
|
|
80
|
-
})
|
|
81
|
-
t.after(() => relay.close())
|
|
67
|
+
describe("multiplexed transport", () => {
|
|
68
|
+
it("two virtual sockets use one authenticated physical transport", async () => withTestCleanup(async (after) => {
|
|
69
|
+
let targetAccepts = 0
|
|
70
|
+
const target = createServer((socket) => {
|
|
71
|
+
targetAccepts += 1
|
|
72
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
73
|
+
})
|
|
74
|
+
const targetPort = await listen(target)
|
|
75
|
+
after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
const relay = await startMultiplexedTcpRelay({
|
|
78
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
79
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
|
|
80
|
+
})
|
|
81
|
+
after(() => relay.close())
|
|
87
82
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
const connection = new MultiplexedSocketductConnection({
|
|
84
|
+
relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}
|
|
85
|
+
})
|
|
86
|
+
after(() => connection.close())
|
|
91
87
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
const first = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "first"})
|
|
89
|
+
const second = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "second"})
|
|
90
|
+
await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
assert.equal(connection.status().virtualStreams, 2)
|
|
92
|
+
first.write("one")
|
|
93
|
+
second.write("two")
|
|
94
|
+
await Promise.all([readExactly(first, "one"), readExactly(second, "two")])
|
|
100
95
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
expect(relay.status().physicalAccepts).toBe(1)
|
|
97
|
+
expect(relay.status().authenticatedConnections).toBe(1)
|
|
98
|
+
expect(targetAccepts).toBe(2)
|
|
99
|
+
expect(connection.status().virtualStreams).toBe(2)
|
|
105
100
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
111
|
-
const targetPort = await listen(target)
|
|
112
|
-
t.after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
113
|
-
const relay = await startMultiplexedTcpRelay({
|
|
114
|
-
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
115
|
-
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
|
|
116
|
-
})
|
|
117
|
-
t.after(() => relay.close())
|
|
118
|
-
const connection = new MultiplexedSocketductConnection({
|
|
119
|
-
relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
|
|
120
|
-
reconnectDelayMs: 10,
|
|
121
|
-
logger
|
|
122
|
-
})
|
|
123
|
-
t.after(() => connection.close())
|
|
124
|
-
const first = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-a"})
|
|
125
|
-
const second = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-b"})
|
|
126
|
-
await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
|
|
127
|
-
first.write("before-a")
|
|
128
|
-
second.write("before-b")
|
|
129
|
-
await Promise.all([readExactly(first, "before-a"), readExactly(second, "before-b")])
|
|
130
|
-
|
|
131
|
-
const ready = Promise.all([onceEvent(first, "ready"), onceEvent(second, "ready")])
|
|
132
|
-
await relay.closeActiveTransport()
|
|
133
|
-
first.write("after-a")
|
|
134
|
-
second.write("after-b")
|
|
135
|
-
await ready
|
|
136
|
-
await Promise.all([readExactly(first, "after-a"), readExactly(second, "after-b")])
|
|
137
|
-
|
|
138
|
-
assert.equal(relay.status().physicalAccepts, 2)
|
|
139
|
-
assert.equal(connection.status().reconnectAttempts, 1)
|
|
140
|
-
assert.equal(connection.status().reconnectSuccesses, 1)
|
|
141
|
-
assert.equal(relay.status().resumeSuccesses, 2)
|
|
142
|
-
const closed = Promise.all([onceEvent(first, "close"), onceEvent(second, "close")])
|
|
143
|
-
first.destroy()
|
|
144
|
-
second.destroy()
|
|
145
|
-
await closed
|
|
146
|
-
for (const event of [
|
|
147
|
-
"transport.connecting",
|
|
148
|
-
"transport.connected",
|
|
149
|
-
"transport.disconnected",
|
|
150
|
-
"transport.reconnect_scheduled",
|
|
151
|
-
"transport.reconnect_succeeded",
|
|
152
|
-
"virtual_stream.opening",
|
|
153
|
-
"virtual_stream.opened",
|
|
154
|
-
"virtual_stream.closed"
|
|
155
|
-
]) assert(lifecycle.some((entry) => entry.event === event), `missing ${event}`)
|
|
156
|
-
assert.doesNotMatch(JSON.stringify(lifecycle), /valid-token|resume-a|resume-b|127\.0\.0\.1/)
|
|
157
|
-
})
|
|
101
|
+
first.destroy()
|
|
102
|
+
expect(second.destroyed).toBe(false)
|
|
103
|
+
second.destroy()
|
|
104
|
+
}))
|
|
158
105
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
socket.on("data", (chunk) =>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
106
|
+
it("one transport drop has one reconnect owner and resumes sibling streams without gaps", async () => withTestCleanup(async (after) => {
|
|
107
|
+
/** @type {Array<{event: string} & Record<string, unknown>>} */
|
|
108
|
+
const lifecycle = []
|
|
109
|
+
const logger = {log: (/** @type {string} */ event, /** @type {Record<string, unknown>} */ fields = {}) => lifecycle.push({event, ...fields})}
|
|
110
|
+
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
111
|
+
const targetPort = await listen(target)
|
|
112
|
+
after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
113
|
+
const relay = await startMultiplexedTcpRelay({
|
|
114
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
115
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort
|
|
167
116
|
})
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
117
|
+
after(() => relay.close())
|
|
118
|
+
const connection = new MultiplexedSocketductConnection({
|
|
119
|
+
relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
|
|
120
|
+
reconnectDelayMs: 10,
|
|
121
|
+
logger
|
|
122
|
+
})
|
|
123
|
+
after(() => connection.close())
|
|
124
|
+
const first = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-a"})
|
|
125
|
+
const second = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "resume-b"})
|
|
126
|
+
await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
|
|
127
|
+
first.write("before-a")
|
|
128
|
+
second.write("before-b")
|
|
129
|
+
await Promise.all([readExactly(first, "before-a"), readExactly(second, "before-b")])
|
|
130
|
+
|
|
131
|
+
const ready = Promise.all([onceEvent(first, "ready"), onceEvent(second, "ready")])
|
|
132
|
+
await relay.closeActiveTransport()
|
|
133
|
+
first.write("after-a")
|
|
134
|
+
second.write("after-b")
|
|
135
|
+
await ready
|
|
136
|
+
await Promise.all([readExactly(first, "after-a"), readExactly(second, "after-b")])
|
|
137
|
+
|
|
138
|
+
expect(relay.status().physicalAccepts).toBe(2)
|
|
139
|
+
expect(connection.status().reconnectAttempts).toBe(1)
|
|
140
|
+
expect(connection.status().reconnectSuccesses).toBe(1)
|
|
141
|
+
expect(relay.status().resumeSuccesses).toBe(2)
|
|
142
|
+
const closed = Promise.all([onceEvent(first, "close"), onceEvent(second, "close")])
|
|
143
|
+
first.destroy()
|
|
144
|
+
second.destroy()
|
|
145
|
+
await closed
|
|
146
|
+
for (const event of [
|
|
147
|
+
"transport.connecting",
|
|
148
|
+
"transport.connected",
|
|
149
|
+
"transport.disconnected",
|
|
150
|
+
"transport.reconnect_scheduled",
|
|
151
|
+
"transport.reconnect_succeeded",
|
|
152
|
+
"virtual_stream.opening",
|
|
153
|
+
"virtual_stream.opened",
|
|
154
|
+
"virtual_stream.closed"
|
|
155
|
+
]) expect(lifecycle.some((entry) => entry.event === event)).toBeTruthy() // missing ${event}
|
|
156
|
+
expect(JSON.stringify(lifecycle)).not.toMatch(/valid-token|resume-a|resume-b|127\.0\.0\.1/)
|
|
157
|
+
}))
|
|
182
158
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
159
|
+
it("100 concurrent build-like streams create zero extra physical accepts", async () => withTestCleanup(async (after) => {
|
|
160
|
+
let targetAccepts = 0
|
|
161
|
+
const target = createServer((socket) => {
|
|
162
|
+
targetAccepts += 1
|
|
163
|
+
socket.on("data", (chunk) => {
|
|
164
|
+
const split = Math.max(1, Math.floor(chunk.length / 2))
|
|
165
|
+
socket.write(chunk.subarray(0, split))
|
|
166
|
+
socket.write(chunk.subarray(split))
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
const targetPort = await listen(target)
|
|
170
|
+
after(() => new Promise((resolve) => target.close(() => resolve(undefined))))
|
|
171
|
+
const relay = await startMultiplexedTcpRelay({
|
|
172
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
173
|
+
authorizeTarget: ({target: requested}) => requested.host === "127.0.0.1" && requested.port === targetPort,
|
|
174
|
+
maxStreams: 128
|
|
175
|
+
})
|
|
176
|
+
after(() => relay.close())
|
|
177
|
+
const connection = new MultiplexedSocketductConnection({
|
|
178
|
+
relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
|
|
179
|
+
maxStreams: 128
|
|
180
|
+
})
|
|
181
|
+
after(() => connection.close())
|
|
182
|
+
|
|
183
|
+
const sockets = Array.from({length: 100}, (_, index) => connection.createConnection({
|
|
184
|
+
host: "127.0.0.1",
|
|
185
|
+
port: targetPort,
|
|
186
|
+
sessionName: `build-flow-${index + 1}`
|
|
187
|
+
}))
|
|
188
|
+
await Promise.all(sockets.map((socket) => onceEvent(socket, "connect")))
|
|
189
|
+
const reads = sockets.map((socket, index) => readExactly(socket, `flow-${index + 1}:${"x".repeat(1024)}`))
|
|
190
|
+
sockets.forEach((socket, index) => socket.write(`flow-${index + 1}:${"x".repeat(1024)}`))
|
|
191
|
+
await Promise.all(reads)
|
|
192
|
+
|
|
193
|
+
expect(relay.status().physicalAccepts).toBe(1)
|
|
194
|
+
expect(relay.status().physicalConnections).toBe(1)
|
|
195
|
+
expect(targetAccepts).toBe(100)
|
|
196
|
+
expect(connection.status().virtualStreams).toBe(100)
|
|
197
|
+
|
|
198
|
+
const resumed = Promise.all(sockets.map((socket) => onceEvent(socket, "ready")))
|
|
199
|
+
await relay.closeActiveTransport()
|
|
200
|
+
await resumed
|
|
201
|
+
const resumedReads = sockets.map((socket, index) => readExactly(socket, `resumed-${index + 1}`))
|
|
202
|
+
sockets.forEach((socket, index) => socket.write(`resumed-${index + 1}`))
|
|
203
|
+
await Promise.all(resumedReads)
|
|
204
|
+
expect(relay.status().physicalAccepts).toBe(2)
|
|
205
|
+
expect(relay.status().resumeSuccesses).toBe(100)
|
|
206
|
+
expect(connection.status().reconnectAttempts).toBe(1)
|
|
207
|
+
sockets.forEach((socket) => socket.destroy())
|
|
187
208
|
}))
|
|
188
|
-
await Promise.all(sockets.map((socket) => onceEvent(socket, "connect")))
|
|
189
|
-
const reads = sockets.map((socket, index) => readExactly(socket, `flow-${index + 1}:${"x".repeat(1024)}`))
|
|
190
|
-
sockets.forEach((socket, index) => socket.write(`flow-${index + 1}:${"x".repeat(1024)}`))
|
|
191
|
-
await Promise.all(reads)
|
|
192
|
-
|
|
193
|
-
assert.equal(relay.status().physicalAccepts, 1)
|
|
194
|
-
assert.equal(relay.status().physicalConnections, 1)
|
|
195
|
-
assert.equal(targetAccepts, 100)
|
|
196
|
-
assert.equal(connection.status().virtualStreams, 100)
|
|
197
|
-
|
|
198
|
-
const resumed = Promise.all(sockets.map((socket) => onceEvent(socket, "ready")))
|
|
199
|
-
await relay.closeActiveTransport()
|
|
200
|
-
await resumed
|
|
201
|
-
const resumedReads = sockets.map((socket, index) => readExactly(socket, `resumed-${index + 1}`))
|
|
202
|
-
sockets.forEach((socket, index) => socket.write(`resumed-${index + 1}`))
|
|
203
|
-
await Promise.all(resumedReads)
|
|
204
|
-
assert.equal(relay.status().physicalAccepts, 2)
|
|
205
|
-
assert.equal(relay.status().resumeSuccesses, 100)
|
|
206
|
-
assert.equal(connection.status().reconnectAttempts, 1)
|
|
207
|
-
sockets.forEach((socket) => socket.destroy())
|
|
208
209
|
})
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {describe, expect, it, withTestCleanup} from "@velocious/testing"
|
|
2
2
|
import {mkdtemp, rm} from "node:fs/promises"
|
|
3
3
|
import {createServer} from "node:net"
|
|
4
4
|
import {tmpdir} from "node:os"
|
|
5
5
|
import {join} from "node:path"
|
|
6
|
-
import test from "node:test"
|
|
7
6
|
|
|
8
7
|
import {StaticTokenStore} from "../../src/auth/static-token-store.js"
|
|
9
8
|
import {withTimeout, waitUntil} from "awaitery"
|
|
@@ -11,99 +10,101 @@ import {OutboundReverseGateway, ReverseGatewayConnection, startReverseGatewayHub
|
|
|
11
10
|
import {generateTestCertificates} from "../helpers/tls-certificates.js"
|
|
12
11
|
import {listen, onceEvent} from "../helpers/test-async.js"
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
describe("reverse gateway stream id reset", () => {
|
|
14
|
+
it("gateway resets its reverse stream high-water on fresh registration after a hub restart", async () => withTestCleanup(async (after) => {
|
|
15
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-streamid-"))
|
|
16
|
+
after(() => rm(root, {recursive: true, force: true}))
|
|
17
|
+
const certificates = generateTestCertificates(root)
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
let targetAccepts = 0
|
|
20
|
+
const targetSockets = new Set()
|
|
21
|
+
const target = createServer((socket) => {
|
|
22
|
+
targetAccepts += 1
|
|
23
|
+
targetSockets.add(socket)
|
|
24
|
+
socket.once("close", () => targetSockets.delete(socket))
|
|
25
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
26
|
+
})
|
|
27
|
+
const targetPort = await listen(target)
|
|
28
|
+
after(() => {
|
|
29
|
+
for (const socket of targetSockets) socket.destroy()
|
|
30
|
+
return new Promise((resolve) => target.close(() => resolve(undefined)))
|
|
31
|
+
})
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
const tokens = new StaticTokenStore([
|
|
34
|
+
{username: "home-o2", token: "gateway-token"},
|
|
35
|
+
{username: "tensorbuzz", token: "client-token"}
|
|
36
|
+
])
|
|
37
|
+
const hubOptions = {
|
|
38
|
+
tokens,
|
|
39
|
+
tls: {certFile: certificates.serverCertFile, keyFile: certificates.serverKeyFile},
|
|
40
|
+
knownGatewayIds: ["o2"],
|
|
41
|
+
authorizeGateway: (/** @type {{principal: {username: string}, gatewayId: string}} */ {principal, gatewayId}) => principal.username === "home-o2" && gatewayId === "o2",
|
|
42
|
+
authorizeClientGateway: (/** @type {{principal: {username: string}, gatewayId: string}} */ {principal, gatewayId}) => principal.username === "tensorbuzz" && gatewayId === "o2",
|
|
43
|
+
authorizeOpen: (/** @type {{principal: {username: string}, gatewayId: string, target: {host: string, port: number}}} */ {principal, gatewayId, target: requested}) => principal.username === "tensorbuzz" && gatewayId === "o2" && requested.host === "127.0.0.1" && requested.port === targetPort
|
|
44
|
+
}
|
|
45
|
+
let hub = await startReverseGatewayHub(hubOptions)
|
|
46
|
+
after(() => hub.close())
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
const gateway = new OutboundReverseGateway({
|
|
49
|
+
gatewayId: "o2",
|
|
50
|
+
connectionId: "streamid_gateway",
|
|
51
|
+
hub: {host: "127.0.0.1", port: hub.port, token: "gateway-token"},
|
|
52
|
+
tls: {caFile: certificates.caCertFile, servername: "localhost"},
|
|
53
|
+
authorizeTarget: (requested) => requested.host === "127.0.0.1" && requested.port === targetPort,
|
|
54
|
+
reconnectDelayMs: 10,
|
|
55
|
+
maxReconnectDelayMs: 20
|
|
56
|
+
})
|
|
57
|
+
after(() => gateway.close())
|
|
58
|
+
await withTimeout(gateway.start(), "gateway registration")
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
const client = new ReverseGatewayConnection({
|
|
61
|
+
gatewayId: "o2",
|
|
62
|
+
connectionId: "streamid_client",
|
|
63
|
+
hub: {host: "127.0.0.1", port: hub.port, token: "client-token"},
|
|
64
|
+
tls: {caFile: certificates.caCertFile, servername: "localhost"},
|
|
65
|
+
reconnectDelayMs: 10,
|
|
66
|
+
maxReconnectDelayMs: 20
|
|
67
|
+
})
|
|
68
|
+
after(() => client.close())
|
|
69
|
+
await withTimeout(client.connect(), "client authentication")
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
71
|
+
// Open the first stream: this raises the gateway's process-lifetime
|
|
72
|
+
// #lastStreamId high-water mark above the fresh registration's start.
|
|
73
|
+
const first = client.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "before-restart"})
|
|
74
|
+
await withTimeout(onceEvent(first, "connect"), "first reverse stream opens")
|
|
75
|
+
first.write("first")
|
|
76
|
+
await new Promise((resolve, reject) => {
|
|
77
|
+
first.once("data", (/** @type {Buffer} */ chunk) => { expect(chunk.toString()).toBe("first"); resolve(undefined) })
|
|
78
|
+
first.once("error", reject)
|
|
79
|
+
})
|
|
80
|
+
const firstClosed = onceEvent(first, "close")
|
|
81
|
+
first.end()
|
|
82
|
+
await withTimeout(firstClosed, "first stream close")
|
|
83
|
+
// Fully retire the first stream on every peer before the hub restarts so
|
|
84
|
+
// the only effect of the restart is the fresh (non-resumed) registration.
|
|
85
|
+
await waitUntil(() => client.status().virtualStreams === 0 && gateway.status().virtualStreams === 0 && hub.status().virtualStreams === 0, "first stream retired")
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
// Restart the hub so the gateway reconnects with a fresh (non-resumed)
|
|
88
|
+
// registration whose stream ids restart from the low end.
|
|
89
|
+
const port = hub.port
|
|
90
|
+
await withTimeout(hub.close(), "hub close")
|
|
91
|
+
hub = await startReverseGatewayHub({...hubOptions, port})
|
|
92
|
+
after(() => hub.close())
|
|
93
|
+
await waitUntil(() => gateway.status().physicalState === "ready" && client.status().physicalState === "ready" && gateway.status().physicalConnects >= 2 && client.status().physicalConnects >= 2, "peers after hub restart")
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
95
|
+
// Without the high-water reset, this OPEN is rejected as
|
|
96
|
+
// "Invalid reverse gateway stream ID" and the gateway wedges.
|
|
97
|
+
const second = client.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "after-restart"})
|
|
98
|
+
await withTimeout(onceEvent(second, "connect"), "second reverse stream opens after hub restart")
|
|
99
|
+
second.write("second")
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
second.once("data", (/** @type {Buffer} */ chunk) => { expect(chunk.toString()).toBe("second"); resolve(undefined) })
|
|
102
|
+
second.once("error", reject)
|
|
103
|
+
})
|
|
104
|
+
const secondClosed = onceEvent(second, "close")
|
|
105
|
+
second.end()
|
|
106
|
+
await withTimeout(secondClosed, "second stream close")
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
expect(targetAccepts).toBeGreaterThanOrEqual(2) // expected at least two target connections, got ${targetAccepts}
|
|
109
|
+
}))
|
|
109
110
|
})
|