AtlatestRepositorysigil-websocket
sigil-websocket / tree / test / integrationtest-native-websocket-binary.mjs
1
import crypto from "node:crypto";2
import net from "node:net";3
import path from "node:path";4
import { fileURLToPath } from "node:url";5
import { spawn } from "node:child_process";7
const here = path.dirname(fileURLToPath(import.meta.url));8
const repo = path.resolve(here, "../..");10
function 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]);13
}15
function 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 };42
}44
const server = net.createServer();45
let resolvePort;46
const listening = new Promise((resolve) => { resolvePort = resolve; });47
const receivedClientFrames = [];49
server.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 the78
// 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
});87
server.listen(0, "127.0.0.1", () => resolvePort(server.address().port));88
const port = await listening;90
const dependencyRoots = [91
"sigil-stdlib", "sigil-socket", "sigil-tls", "sigil-crypto",92
].map((name) => path.join(repo, ".sigil", "deps", name, "src"));93
const args = [94
"-L", path.join(repo, "build/dev/lib"),95
...dependencyRoots.flatMap((root) => ["-L", root]),96
path.join(here, "native-binary-client.sgl"),97
];98
const child = spawn("sigil", args, {99
cwd: repo,100
env: { ...process.env, SIGIL_WS_TEST_PORT: String(port) },101
stdio: ["ignore", "pipe", "pipe"],102
});103
let stdout = "";104
let stderr = "";105
child.stdout.on("data", (chunk) => { stdout += chunk; });106
child.stderr.on("data", (chunk) => { stderr += chunk; });108
const timeout = setTimeout(() => child.kill("SIGKILL"), 5000);110
const status = await new Promise((resolve) => child.on("close", resolve));111
clearTimeout(timeout);112
const binary = receivedClientFrames.filter((frame) => frame.opcode === 2);113
const received = binary[0];114
const receivedLarge = binary[1];115
server.close();117
if (!received) throw new Error(`native client sent no decodable binary frame\nstdout:\n${stdout}\nstderr:\n${stderr}`);118
if (received.opcode !== 2) throw new Error(`expected binary opcode, got ${received.opcode}`);119
if (!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")}`);121
}122
const largeLength = (2 * 1024 * 1024) + 37;123
if (!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}`);125
}126
for (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
}130
}131
if (status !== 0 || !stdout.includes("native-websocket-binary-ok")) {132
throw new Error(`native client failed (${status})\nstdout:\n${stdout}\nstderr:\n${stderr}`);133
}134
console.log("native-websocket-binary-integration-ok");