AtlatestRepositorysigil-websocket

sigil-websocket / tree / test / integrationtest-native-websocket-binary.mjs

1import crypto from "node:crypto";
2import net from "node:net";
3import path from "node:path";
4import { fileURLToPath } from "node:url";
5import { spawn } from "node:child_process";
6
7const here = path.dirname(fileURLToPath(import.meta.url));
8const repo = path.resolve(here, "../..");
9
10function serverFrame(payload) {
11 if (payload.length >= 126) throw new Error("fixture payload is unexpectedly large");
12 return Buffer.concat([Buffer.from([0x82, payload.length]), payload]);
15function decodeClientFrame(buffer) {
16 if (buffer.length < 6) return null;
17 const opcode = buffer[0] & 0x0f;
18 const masked = (buffer[1] & 0x80) !== 0;
19 const shortLength = buffer[1] & 0x7f;
20 let length;
21 let headerLength;
22 if (shortLength < 126) {
23 length = shortLength;
24 headerLength = 2;
25 } else if (shortLength === 126) {
26 if (buffer.length < 8) return null;
27 length = buffer.readUInt16BE(2);
28 headerLength = 4;
29 } else {
30 if (buffer.length < 14) return null;
31 const wide = buffer.readBigUInt64BE(2);
32 if (wide > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("fixture frame is too large");
33 length = Number(wide);
34 headerLength = 10;
35 }
36 if (!masked || buffer.length < headerLength + 4 + length) return null;
37 const mask = buffer.subarray(headerLength, headerLength + 4);
38 const payload = Buffer.alloc(length);
39 const start = headerLength + 4;
40 for (let i = 0; i < length; i += 1) payload[i] = buffer[start + i] ^ mask[i % 4];
41 return { opcode, payload, consumed: start + length };
44const server = net.createServer();
45let resolvePort;
46const listening = new Promise((resolve) => { resolvePort = resolve; });
47const receivedClientFrames = [];
49server.on("connection", (socket) => {
50 let buffer = Buffer.alloc(0);
51 let upgraded = false;
52 socket.on("data", (chunk) => {
53 buffer = Buffer.concat([buffer, chunk]);
54 if (!upgraded) {
55 const end = buffer.indexOf("\r\n\r\n");
56 if (end < 0) return;
57 const request = buffer.subarray(0, end + 4).toString("ascii");
58 const key = request.match(/Sec-WebSocket-Key:\s*([^\r\n]+)/i)?.[1];
59 if (!key) throw new Error("client handshake omitted Sec-WebSocket-Key");
60 const accept = crypto.createHash("sha1")
61 .update(`${key.trim()}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
62 .digest("base64");
63 const headers = Buffer.from(
64 `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`,
65 "ascii",
66 );
67 // Load-bearing case: the first binary frame shares the handshake write.
68 socket.write(Buffer.concat([headers, serverFrame(Buffer.from([0xff, 0x00, 0x80, 0x42]))]));
69 buffer = buffer.subarray(end + 4);
70 upgraded = true;
71 }
72 let decoded;
73 while ((decoded = decodeClientFrame(buffer))) {
74 receivedClientFrames.push(decoded);
75 buffer = buffer.subarray(decoded.consumed);
76 // Hold the ordinary response until BOTH client binary frames are whole.
77 // The client cannot close after its small-frame acknowledgement while the
78 // kernel still has the large frame queued, making completeness causal.
79 if (decoded.opcode === 2 &&
80 receivedClientFrames.filter((frame) => frame.opcode === 2).length === 2) {
81 setTimeout(() => socket.write(serverFrame(Buffer.from([0x10, 0xfe, 0x20, 0x80]))), 10);
82 }
83 }
84 });
85});
87server.listen(0, "127.0.0.1", () => resolvePort(server.address().port));
88const port = await listening;
90const dependencyRoots = [
91 "sigil-stdlib", "sigil-socket", "sigil-tls", "sigil-crypto",
92].map((name) => path.join(repo, ".sigil", "deps", name, "src"));
93const args = [
94 "-L", path.join(repo, "build/dev/lib"),
95 ...dependencyRoots.flatMap((root) => ["-L", root]),
96 path.join(here, "native-binary-client.sgl"),
97];
98const child = spawn("sigil", args, {
99 cwd: repo,
100 env: { ...process.env, SIGIL_WS_TEST_PORT: String(port) },
101 stdio: ["ignore", "pipe", "pipe"],
102});
103let stdout = "";
104let stderr = "";
105child.stdout.on("data", (chunk) => { stdout += chunk; });
106child.stderr.on("data", (chunk) => { stderr += chunk; });
108const timeout = setTimeout(() => child.kill("SIGKILL"), 5000);
110const status = await new Promise((resolve) => child.on("close", resolve));
111clearTimeout(timeout);
112const binary = receivedClientFrames.filter((frame) => frame.opcode === 2);
113const received = binary[0];
114const receivedLarge = binary[1];
115server.close();
117if (!received) throw new Error(`native client sent no decodable binary frame\nstdout:\n${stdout}\nstderr:\n${stderr}`);
118if (received.opcode !== 2) throw new Error(`expected binary opcode, got ${received.opcode}`);
119if (!received.payload.equals(Buffer.from([0x00, 0x7f, 0x80, 0xff, 0x41]))) {
120 throw new Error(`native client binary write was not byte-exact: ${received.payload.toString("hex")}`);
122const largeLength = (2 * 1024 * 1024) + 37;
123if (!receivedLarge || receivedLarge.payload.length !== largeLength) {
124 throw new Error(`native client large binary frame was truncated: ${receivedLarge?.payload.length ?? 0}/${largeLength}\nstatus: ${status}\nstdout:\n${stdout}\nstderr:\n${stderr}`);
126for (let i = 0; i < receivedLarge.payload.length; i += 1) {
127 if (receivedLarge.payload[i] !== 0x5a) {
128 throw new Error(`native client large binary frame differs at byte ${i}`);
129 }
131if (status !== 0 || !stdout.includes("native-websocket-binary-ok")) {
132 throw new Error(`native client failed (${status})\nstdout:\n${stdout}\nstderr:\n${stderr}`);
134console.log("native-websocket-binary-integration-ok");