socketduct 0.1.15
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/.dockerignore +11 -0
- package/.env.example +131 -0
- package/AGENTS.md +168 -0
- package/Dockerfile +30 -0
- package/README.md +134 -0
- package/changelog.d/20260716083144-v2-ack-wire-format.md +3 -0
- package/changelog.d/20260716092044-v2-only-stream-framing.md +1 -0
- package/changelog.d/20260727113000-relay-connect-timeout-destroy.md +1 -0
- package/changelog.d/20260813093000-private-relay-hardening.md +1 -0
- package/changelog.d/20260824183412-initial-target-recovery-https.md +1 -0
- package/changelog.d/20260824230000-tls-relay-session-close.md +1 -0
- package/changelog.d/20260825120000-reconnect-close-target-timeout.md +1 -0
- package/changelog.d/20260829120000-protocol-v3-multiplexing.md +1 -0
- package/changelog.d/20260829160000-v3-review-corrections.md +1 -0
- package/changelog.d/20260830000000-fix-socket-idle-timeout.md +1 -0
- package/changelog.d/20260830090000-multi-gateway-transport-set.md +1 -0
- package/changelog.d/20260830093000-relay-close-settlement.md +1 -0
- package/changelog.d/20260830120000-immutable-gateway-image.md +1 -0
- package/changelog.d/20260902210000-native-reverse-gateway.md +1 -0
- package/changelog.d/20260902230000-relay-resume-terminal-order.md +1 -0
- package/changelog.d/20260902230708-multiplexed-open-ownership.md +1 -0
- package/changelog.d/20260902231541-multiplexed-terminal-close.md +1 -0
- package/changelog.d/20260902231542-reverse-close-position.md +1 -0
- package/changelog.d/20260902231543-reverse-terminal-connect.md +1 -0
- package/changelog.d/20260902233000-reverse-gateway-bounds.md +1 -0
- package/changelog.d/20260903001000-reverse-data-ordering.md +1 -0
- package/changelog.d/20260903003000-reverse-hub-restart-window.md +1 -0
- package/changelog.d/20260903010000-reverse-terminal-lifecycle.md +1 -0
- package/changelog.d/20260904142743-transport-set-readiness-wait.md +1 -0
- package/changelog.d/20260904152855-deferred-open-snapshots-binary-views.md +1 -0
- package/changelog.d/20260904163041-tensorbuzz-container-workflow.md +1 -0
- package/changelog.d/20260904174955-transport-set-terminal-causes.md +1 -0
- package/changelog.d/20260905081000-contain-reverse-auth-write-errors.md +1 -0
- package/changelog.d/20260905100520-reverse-hub-close-race.md +1 -0
- package/changelog.d/20260905120000-persistent-jsonl-logging.md +1 -0
- package/changelog.d/20260908071009-jsonl-partial-append-recovery.md +1 -0
- package/changelog.d/20260908074916-private-log-directory-setup.md +1 -0
- package/changelog.d/20260909093057-reverse-large-write-backpressure.md +1 -0
- package/changelog.d/20260909135830-actionable-error-diagnostics.md +1 -0
- package/changelog.d/20260909150000-transport-set-error-redaction.md +1 -0
- package/docker-compose.dev.yml +19 -0
- package/docker-compose.reverse-gateway.yml +36 -0
- package/docker-compose.yml +73 -0
- package/docs/architecture.md +264 -0
- package/docs/docker-topology.md +164 -0
- package/docs/docker.md +281 -0
- package/docs/logging.md +185 -0
- package/docs/plans/initial-implementation.md +205 -0
- package/docs/plans/socket-docker-integration.md +221 -0
- package/docs/protocol.md +558 -0
- package/docs/releasing.md +18 -0
- package/docs/reverse-gateway.md +186 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +144 -0
- package/docs/running.md +215 -0
- package/docs/security.md +258 -0
- package/docs/session-lifecycle.md +127 -0
- package/docs/socket-api.md +448 -0
- package/eslint.config.js +36 -0
- package/package.json +44 -0
- package/scripts/deploy-gateway.js +194 -0
- package/scripts/test.js +28 -0
- package/src/auth/passwords.js +38 -0
- package/src/auth/static-token-store.js +41 -0
- package/src/auth/user-store.js +139 -0
- package/src/client/connect.js +76 -0
- package/src/client/deferred-virtual-socket.js +312 -0
- package/src/client/http-agent.js +101 -0
- package/src/client/https-agent.js +366 -0
- package/src/client/multiplexed-connection.js +1504 -0
- package/src/client/multiplexed-transport-set.js +917 -0
- package/src/client/multiplexed.js +3 -0
- package/src/client/socketduct-socket.js +1174 -0
- package/src/client/virtual-socket.js +256 -0
- package/src/config/environment.js +400 -0
- package/src/edge/tcp-edge.js +82 -0
- package/src/index.js +101 -0
- package/src/logging/structured-logger.js +745 -0
- package/src/protocol/compression.js +325 -0
- package/src/protocol/frame.js +341 -0
- package/src/protocol/v3-compression.js +44 -0
- package/src/protocol/v3-frame.js +209 -0
- package/src/proxy/multiplexed-relay.js +798 -0
- package/src/proxy/tcp-relay.js +1191 -0
- package/src/relay.js +3 -0
- package/src/reverse/client.js +721 -0
- package/src/reverse/common.js +88 -0
- package/src/reverse/gateway.js +663 -0
- package/src/reverse/hub.js +563 -0
- package/src/reverse/index.js +3 -0
- package/src/reverse/runtime.js +78 -0
- package/src/server/control-handler.js +170 -0
- package/src/server/primary.js +64 -0
- package/src/server/relay-runtime.js +124 -0
- package/src/server/worker.js +39 -0
- package/src/sessions/session.js +212 -0
- package/src/spool/spool-file.js +451 -0
- package/tensorbuzz.yml +36 -0
- package/test/e2e/gateway-process.test.js +262 -0
- package/test/e2e/reverse-gateway-process.test.js +285 -0
- package/test/fixtures/logging/enospc-matrix.mjs +238 -0
- package/test/fixtures/logging/enospc-recovery.mjs +146 -0
- package/test/fixtures/logging/process-writer.mjs +20 -0
- package/test/helpers/tls-certificates.js +115 -0
- package/test/integration/initial-target-recovery.test.js +518 -0
- package/test/integration/logging-recovery.test.js +163 -0
- package/test/integration/multiplexed-compatibility.test.js +254 -0
- package/test/integration/multiplexed-corrections.test.js +551 -0
- package/test/integration/multiplexed-failures.test.js +339 -0
- package/test/integration/multiplexed-transport-set.test.js +456 -0
- package/test/integration/multiplexed-transport.test.js +208 -0
- package/test/integration/reverse-gateway.test.js +1293 -0
- package/test/integration/socketduct-http-agent.test.js +435 -0
- package/test/integration/socketduct-https-agent.test.js +669 -0
- package/test/integration/stream-compression.test.js +1267 -0
- package/test/integration/tcp-forwarding.test.js +2878 -0
- package/test/integration/tls-forwarding.test.js +748 -0
- package/test/unit/auth/auth.test.js +64 -0
- package/test/unit/auth/static-token-store.test.js +14 -0
- package/test/unit/client/connect.test.js +58 -0
- package/test/unit/client/multiplexed-spool-budget.test.js +309 -0
- package/test/unit/client/multiplexed-transport-set.test.js +906 -0
- package/test/unit/client/socketduct-socket.test.js +1124 -0
- package/test/unit/client/virtual-socket.test.js +98 -0
- package/test/unit/config/docker-gateway-files.test.js +274 -0
- package/test/unit/config/environment.test.js +507 -0
- package/test/unit/index.test.js +78 -0
- package/test/unit/logging/structured-logger.test.js +674 -0
- package/test/unit/protocol/compression.test.js +421 -0
- package/test/unit/protocol/frame.test.js +336 -0
- package/test/unit/protocol/v3-compression.test.js +31 -0
- package/test/unit/protocol/v3-frame.test.js +57 -0
- package/test/unit/release-configuration.test.js +21 -0
- package/test/unit/server/control-handler.test.js +291 -0
- package/test/unit/server/primary.test.js +59 -0
- package/test/unit/server/relay-runtime.test.js +434 -0
- package/test/unit/sessions/session.test.js +84 -0
- package/test/unit/spool/spool-file.test.js +295 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-returns */
|
|
2
|
+
import assert from "node:assert/strict"
|
|
3
|
+
import {readFileSync} from "node:fs"
|
|
4
|
+
import {mkdtemp, rm} from "node:fs/promises"
|
|
5
|
+
import http from "node:http"
|
|
6
|
+
import {createServer} from "node:net"
|
|
7
|
+
import {tmpdir} from "node:os"
|
|
8
|
+
import {join} from "node:path"
|
|
9
|
+
import {connect as tlsConnect, createServer as createTlsServer} from "node:tls"
|
|
10
|
+
import test from "node:test"
|
|
11
|
+
|
|
12
|
+
import {StaticTokenStore} from "../../src/auth/static-token-store.js"
|
|
13
|
+
import {MultiplexedSocketductConnection} from "../../src/client/multiplexed-connection.js"
|
|
14
|
+
import {startMultiplexedTcpRelay} from "../../src/proxy/multiplexed-relay.js"
|
|
15
|
+
import {generateTestCertificates} from "../helpers/tls-certificates.js"
|
|
16
|
+
|
|
17
|
+
/** @type {WeakMap<import('node:net').Server, Set<import('node:net').Socket>>} */
|
|
18
|
+
const serverSockets = new WeakMap()
|
|
19
|
+
|
|
20
|
+
/** @param {import('node:net').Server} server - Server. @returns {Promise<number>} Bound port. */
|
|
21
|
+
async function listen(server) {
|
|
22
|
+
const sockets = new Set()
|
|
23
|
+
serverSockets.set(server, sockets)
|
|
24
|
+
server.on("connection", (socket) => {
|
|
25
|
+
sockets.add(socket)
|
|
26
|
+
socket.once("close", () => sockets.delete(socket))
|
|
27
|
+
})
|
|
28
|
+
await new Promise((resolve, reject) => {
|
|
29
|
+
server.once("error", reject)
|
|
30
|
+
server.listen(0, "127.0.0.1", () => {
|
|
31
|
+
server.off("error", reject)
|
|
32
|
+
resolve(undefined)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
const address = server.address()
|
|
36
|
+
assert(address !== null && typeof address !== "string")
|
|
37
|
+
return address.port
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {import('node:net').Server} server - Server. @returns {Promise<void>} */
|
|
41
|
+
async function closeServer(server) {
|
|
42
|
+
for (const socket of serverSockets.get(server) ?? []) socket.destroy()
|
|
43
|
+
if (!server.listening) return
|
|
44
|
+
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve(undefined)))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @param {import('node:http').RequestOptions} options - Request. @returns {Promise<{statusCode: number | undefined, body: string}>} */
|
|
48
|
+
async function request(options) {
|
|
49
|
+
return await new Promise((resolve, reject) => {
|
|
50
|
+
const timer = setTimeout(() => reject(new Error("timed out waiting for HTTP response")), 2_000)
|
|
51
|
+
const req = http.request(options, (res) => {
|
|
52
|
+
/** @type {Buffer[]} */
|
|
53
|
+
const chunks = []
|
|
54
|
+
res.on("data", (chunk) => chunks.push(chunk))
|
|
55
|
+
res.on("error", (error) => { clearTimeout(timer); reject(error) })
|
|
56
|
+
res.on("end", () => {
|
|
57
|
+
clearTimeout(timer)
|
|
58
|
+
resolve({statusCode: res.statusCode, body: Buffer.concat(chunks).toString()})
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
req.on("error", (error) => { clearTimeout(timer); reject(error) })
|
|
62
|
+
req.end()
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @param {number} targetPort - Allowed target. @returns {Promise<{relay: Awaited<ReturnType<typeof startMultiplexedTcpRelay>>, connection: MultiplexedSocketductConnection}>} */
|
|
67
|
+
async function fixture(targetPort) {
|
|
68
|
+
const relay = await startMultiplexedTcpRelay({
|
|
69
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
70
|
+
authorizeTarget: ({target}) => target.host === "127.0.0.1" && target.port === targetPort
|
|
71
|
+
})
|
|
72
|
+
const connection = new MultiplexedSocketductConnection({
|
|
73
|
+
relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}
|
|
74
|
+
})
|
|
75
|
+
return {relay, connection}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("stock Node HTTP uses only createConnection, reuses keep-alive, and handles close-delimited responses", async (t) => {
|
|
79
|
+
let targetAccepts = 0
|
|
80
|
+
const target = http.createServer((req, res) => {
|
|
81
|
+
if (req.url === "/close") {
|
|
82
|
+
res.shouldKeepAlive = false
|
|
83
|
+
res.writeHead(200, {connection: "close"})
|
|
84
|
+
res.end("close-delimited")
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
res.end(`ok:${req.url}`)
|
|
88
|
+
})
|
|
89
|
+
target.on("connection", () => { targetAccepts += 1 })
|
|
90
|
+
const targetPort = await listen(target)
|
|
91
|
+
t.after(() => closeServer(target))
|
|
92
|
+
const {relay, connection} = await fixture(targetPort)
|
|
93
|
+
t.after(() => relay.close())
|
|
94
|
+
t.after(() => connection.close())
|
|
95
|
+
const agent = new http.Agent({keepAlive: true, maxSockets: 1})
|
|
96
|
+
agent.createConnection = connection.createConnection
|
|
97
|
+
t.after(() => agent.destroy())
|
|
98
|
+
|
|
99
|
+
assert.deepEqual(await request({agent, host: "127.0.0.1", port: targetPort, path: "/one"}), {statusCode: 200, body: "ok:/one"})
|
|
100
|
+
assert.deepEqual(await request({agent, host: "127.0.0.1", port: targetPort, path: "/two"}), {statusCode: 200, body: "ok:/two"})
|
|
101
|
+
assert.equal(targetAccepts, 1)
|
|
102
|
+
assert.deepEqual(await request({agent, host: "127.0.0.1", port: targetPort, path: "/close"}), {statusCode: 200, body: "close-delimited"})
|
|
103
|
+
assert.equal(relay.status().physicalAccepts, 1)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("request abort resets one HTTP stream without closing a sibling", async (t) => {
|
|
107
|
+
const target = http.createServer((req, res) => {
|
|
108
|
+
if (req.url === "/slow") {
|
|
109
|
+
setTimeout(() => res.end("too-late"), 500).unref()
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
res.end("sibling-ok")
|
|
113
|
+
})
|
|
114
|
+
const targetPort = await listen(target)
|
|
115
|
+
t.after(() => closeServer(target))
|
|
116
|
+
const {relay, connection} = await fixture(targetPort)
|
|
117
|
+
t.after(() => relay.close())
|
|
118
|
+
t.after(() => connection.close())
|
|
119
|
+
const agent = new http.Agent({keepAlive: false, maxSockets: 2})
|
|
120
|
+
agent.createConnection = connection.createConnection
|
|
121
|
+
t.after(() => agent.destroy())
|
|
122
|
+
|
|
123
|
+
const aborted = new Promise((resolve) => {
|
|
124
|
+
const req = http.request({agent, host: "127.0.0.1", port: targetPort, path: "/slow"})
|
|
125
|
+
req.on("error", (error) => resolve(error))
|
|
126
|
+
req.end()
|
|
127
|
+
req.once("socket", () => req.destroy(new Error("cancelled")))
|
|
128
|
+
})
|
|
129
|
+
const sibling = await request({agent, host: "127.0.0.1", port: targetPort, path: "/fast"})
|
|
130
|
+
assert.match((await aborted).message, /cancelled/)
|
|
131
|
+
assert.equal(sibling.body, "sibling-ok")
|
|
132
|
+
assert.equal(relay.status().physicalConnections, 1)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test("HTTP upgrade/hijack and long-lived streaming work over virtual sockets", async (t) => {
|
|
136
|
+
const target = http.createServer((req, res) => {
|
|
137
|
+
res.writeHead(200)
|
|
138
|
+
res.write("part-1|")
|
|
139
|
+
setTimeout(() => res.end("part-2"), 20).unref()
|
|
140
|
+
})
|
|
141
|
+
target.on("upgrade", (_req, socket) => {
|
|
142
|
+
socket.write("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: test\r\n\r\nhello")
|
|
143
|
+
socket.on("data", (chunk) => socket.write(chunk))
|
|
144
|
+
})
|
|
145
|
+
const targetPort = await listen(target)
|
|
146
|
+
t.after(() => closeServer(target))
|
|
147
|
+
const {relay, connection} = await fixture(targetPort)
|
|
148
|
+
t.after(() => relay.close())
|
|
149
|
+
t.after(() => connection.close())
|
|
150
|
+
const agent = new http.Agent({keepAlive: false})
|
|
151
|
+
agent.createConnection = connection.createConnection
|
|
152
|
+
t.after(() => agent.destroy())
|
|
153
|
+
|
|
154
|
+
assert.equal((await request({agent, host: "127.0.0.1", port: targetPort, path: "/stream"})).body, "part-1|part-2")
|
|
155
|
+
const upgraded = await new Promise((resolve, reject) => {
|
|
156
|
+
const timer = setTimeout(() => reject(new Error("timed out waiting for HTTP upgrade echo")), 2_000)
|
|
157
|
+
const req = http.request({agent, host: "127.0.0.1", port: targetPort, headers: {connection: "Upgrade", upgrade: "test"}})
|
|
158
|
+
req.on("upgrade", (_res, socket, head) => {
|
|
159
|
+
let value = head.toString()
|
|
160
|
+
const finish = () => {
|
|
161
|
+
if (!value.includes("hello") || !value.includes("ping")) return
|
|
162
|
+
socket.destroy()
|
|
163
|
+
clearTimeout(timer)
|
|
164
|
+
resolve(value)
|
|
165
|
+
}
|
|
166
|
+
socket.on("data", (chunk) => { value += chunk.toString(); finish() })
|
|
167
|
+
socket.write("ping")
|
|
168
|
+
finish()
|
|
169
|
+
})
|
|
170
|
+
req.on("error", (error) => { clearTimeout(timer); reject(error) })
|
|
171
|
+
req.end()
|
|
172
|
+
})
|
|
173
|
+
assert.match(upgraded, /hello/)
|
|
174
|
+
assert.match(upgraded, /ping/)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test("node:tls can wrap a VirtualSocket for transitional inner TLS", async (t) => {
|
|
178
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-v3-inner-tls-"))
|
|
179
|
+
t.after(() => rm(root, {recursive: true, force: true}))
|
|
180
|
+
const certs = generateTestCertificates(join(root, "certs"))
|
|
181
|
+
const target = createTlsServer({
|
|
182
|
+
cert: readFileSync(certs.serverCertFile),
|
|
183
|
+
key: readFileSync(certs.serverKeyFile)
|
|
184
|
+
}, (socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
185
|
+
const targetPort = await listen(target)
|
|
186
|
+
t.after(() => closeServer(target))
|
|
187
|
+
const {relay, connection} = await fixture(targetPort)
|
|
188
|
+
t.after(() => relay.close())
|
|
189
|
+
t.after(() => connection.close())
|
|
190
|
+
|
|
191
|
+
const raw = connection.createConnection({host: "127.0.0.1", port: targetPort, sessionName: "inner-tls"})
|
|
192
|
+
const secure = tlsConnect({socket: raw, ca: readFileSync(certs.caCertFile), servername: "localhost"})
|
|
193
|
+
await new Promise((resolve, reject) => {
|
|
194
|
+
secure.once("secureConnect", resolve)
|
|
195
|
+
secure.once("error", reject)
|
|
196
|
+
})
|
|
197
|
+
const echoed = new Promise((resolve, reject) => {
|
|
198
|
+
secure.once("data", (chunk) => resolve(chunk.toString()))
|
|
199
|
+
secure.once("error", reject)
|
|
200
|
+
})
|
|
201
|
+
secure.write("secure-echo")
|
|
202
|
+
assert.equal(await echoed, "secure-echo")
|
|
203
|
+
secure.destroy()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test("v3 authenticates once over a verified outer TLS transport", async (t) => {
|
|
207
|
+
const root = await mkdtemp(join(tmpdir(), "socketduct-v3-outer-tls-"))
|
|
208
|
+
t.after(() => rm(root, {recursive: true, force: true}))
|
|
209
|
+
const certs = generateTestCertificates(join(root, "certs"))
|
|
210
|
+
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
211
|
+
const targetPort = await listen(target)
|
|
212
|
+
t.after(() => closeServer(target))
|
|
213
|
+
const relay = await startMultiplexedTcpRelay({
|
|
214
|
+
tokens: new StaticTokenStore([{username: "edge", token: "valid-token"}]),
|
|
215
|
+
authorizeTarget: ({target: requested}) => requested.port === targetPort,
|
|
216
|
+
tls: {certFile: certs.serverCertFile, keyFile: certs.serverKeyFile}
|
|
217
|
+
})
|
|
218
|
+
t.after(() => relay.close())
|
|
219
|
+
const connection = new MultiplexedSocketductConnection({
|
|
220
|
+
relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
|
|
221
|
+
tls: {caFile: certs.caCertFile, servername: "localhost"}
|
|
222
|
+
})
|
|
223
|
+
t.after(() => connection.close())
|
|
224
|
+
const first = connection.createConnection({host: "127.0.0.1", port: targetPort})
|
|
225
|
+
const second = connection.createConnection({host: "127.0.0.1", port: targetPort})
|
|
226
|
+
await Promise.all([
|
|
227
|
+
new Promise((resolve, reject) => { first.once("connect", resolve); first.once("error", reject) }),
|
|
228
|
+
new Promise((resolve, reject) => { second.once("connect", resolve); second.once("error", reject) })
|
|
229
|
+
])
|
|
230
|
+
const echoed = new Promise((resolve, reject) => { second.once("data", (chunk) => resolve(chunk.toString())); second.once("error", reject) })
|
|
231
|
+
second.write("outer-tls")
|
|
232
|
+
assert.equal(await echoed, "outer-tls")
|
|
233
|
+
assert.equal(relay.status().physicalAccepts, 1)
|
|
234
|
+
assert.equal(relay.status().authenticatedConnections, 1)
|
|
235
|
+
first.destroy()
|
|
236
|
+
second.destroy()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test("raw TCP pre-connect writes are buffered until stream.opened", async (t) => {
|
|
240
|
+
const target = createServer((socket) => socket.on("data", (chunk) => socket.write(chunk)))
|
|
241
|
+
const targetPort = await listen(target)
|
|
242
|
+
t.after(() => closeServer(target))
|
|
243
|
+
const {relay, connection} = await fixture(targetPort)
|
|
244
|
+
t.after(() => relay.close())
|
|
245
|
+
t.after(() => connection.close())
|
|
246
|
+
const socket = connection.createConnection({host: "127.0.0.1", port: targetPort})
|
|
247
|
+
const echoed = new Promise((resolve, reject) => {
|
|
248
|
+
socket.once("data", (chunk) => resolve(chunk.toString()))
|
|
249
|
+
socket.once("error", reject)
|
|
250
|
+
})
|
|
251
|
+
socket.write("early")
|
|
252
|
+
assert.equal(await echoed, "early")
|
|
253
|
+
socket.destroy()
|
|
254
|
+
})
|