148 lines
5.1 KiB
JavaScript
148 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createHash, randomBytes } from "node:crypto";
|
|
import { createConnection } from "node:net";
|
|
|
|
const websocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
|
|
function socketArgument(argv: string[]): string {
|
|
const index = argv.indexOf("--socket");
|
|
const value = index >= 0 ? argv[index + 1] : undefined;
|
|
if (!value) throw new Error("--socket requires a Unix socket path");
|
|
return value;
|
|
}
|
|
|
|
function clientFrame(payload: Buffer, opcode = 0x1): Buffer {
|
|
const length = payload.length;
|
|
const extended = length < 126 ? 0 : length <= 0xffff ? 2 : 8;
|
|
const frame = Buffer.alloc(2 + extended + 4 + length);
|
|
frame[0] = 0x80 | opcode;
|
|
frame[1] = 0x80 | (length < 126 ? length : extended === 2 ? 126 : 127);
|
|
let offset = 2;
|
|
if (extended === 2) {
|
|
frame.writeUInt16BE(length, offset);
|
|
offset += 2;
|
|
} else if (extended === 8) {
|
|
frame.writeBigUInt64BE(BigInt(length), offset);
|
|
offset += 8;
|
|
}
|
|
const mask = randomBytes(4);
|
|
mask.copy(frame, offset);
|
|
offset += 4;
|
|
for (let index = 0; index < length; index += 1)
|
|
frame[offset + index] = payload[index]! ^ mask[index % 4]!;
|
|
return frame;
|
|
}
|
|
|
|
try {
|
|
const socket = createConnection(socketArgument(process.argv.slice(2)));
|
|
const websocketKey = randomBytes(16).toString("base64");
|
|
const expectedAccept = createHash("sha1")
|
|
.update(`${websocketKey}${websocketGuid}`)
|
|
.digest("base64");
|
|
let connected = false;
|
|
let networkBuffer = Buffer.alloc(0);
|
|
let inputBuffer = "";
|
|
let fragmentedPayloads: Buffer[] = [];
|
|
|
|
function emitMessage(payload: Buffer): void {
|
|
process.stdout.write(payload);
|
|
process.stdout.write("\n");
|
|
}
|
|
|
|
function readFrames(): void {
|
|
while (networkBuffer.length >= 2) {
|
|
const first = networkBuffer[0]!;
|
|
const second = networkBuffer[1]!;
|
|
const masked = (second & 0x80) !== 0;
|
|
let length = second & 0x7f;
|
|
let offset = 2;
|
|
if (length === 126) {
|
|
if (networkBuffer.length < 4) return;
|
|
length = networkBuffer.readUInt16BE(2);
|
|
offset = 4;
|
|
} else if (length === 127) {
|
|
if (networkBuffer.length < 10) return;
|
|
const wideLength = networkBuffer.readBigUInt64BE(2);
|
|
if (wideLength > BigInt(Number.MAX_SAFE_INTEGER))
|
|
throw new Error("Codex app-server WebSocket frame is too large");
|
|
length = Number(wideLength);
|
|
offset = 10;
|
|
}
|
|
const maskLength = masked ? 4 : 0;
|
|
if (networkBuffer.length < offset + maskLength + length) return;
|
|
const mask = masked
|
|
? networkBuffer.subarray(offset, offset + 4)
|
|
: null;
|
|
offset += maskLength;
|
|
const payload = Buffer.from(
|
|
networkBuffer.subarray(offset, offset + length),
|
|
);
|
|
networkBuffer = networkBuffer.subarray(offset + length);
|
|
if (mask)
|
|
for (let index = 0; index < payload.length; index += 1)
|
|
payload[index] = payload[index]! ^ mask[index % 4]!;
|
|
|
|
const final = (first & 0x80) !== 0;
|
|
const opcode = first & 0x0f;
|
|
if (opcode === 0x8) {
|
|
socket.end(clientFrame(payload, 0x8));
|
|
return;
|
|
}
|
|
if (opcode === 0x9) {
|
|
socket.write(clientFrame(payload, 0xa));
|
|
continue;
|
|
}
|
|
if (opcode === 0x1 || opcode === 0x2 || opcode === 0x0) {
|
|
fragmentedPayloads.push(payload);
|
|
if (final) {
|
|
emitMessage(Buffer.concat(fragmentedPayloads));
|
|
fragmentedPayloads = [];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
socket.once("connect", () => {
|
|
socket.write(
|
|
`GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${websocketKey}\r\nSec-WebSocket-Version: 13\r\n\r\n`,
|
|
);
|
|
});
|
|
socket.on("data", (chunk: Buffer) => {
|
|
networkBuffer = Buffer.concat([networkBuffer, chunk]);
|
|
if (!connected) {
|
|
const boundary = networkBuffer.indexOf("\r\n\r\n");
|
|
if (boundary < 0) return;
|
|
const headers = networkBuffer.subarray(0, boundary).toString("utf8");
|
|
networkBuffer = networkBuffer.subarray(boundary + 4);
|
|
if (!headers.startsWith("HTTP/1.1 101 "))
|
|
throw new Error("Codex app-server rejected the WebSocket upgrade");
|
|
const accept = headers
|
|
.match(/^sec-websocket-accept:\s*(.+)$/im)?.[1]
|
|
?.trim();
|
|
if (accept !== expectedAccept)
|
|
throw new Error("Codex app-server returned an invalid WebSocket accept value");
|
|
connected = true;
|
|
process.stdin.setEncoding("utf8");
|
|
process.stdin.on("data", (chunk: string) => {
|
|
inputBuffer += chunk;
|
|
for (;;) {
|
|
const newline = inputBuffer.indexOf("\n");
|
|
if (newline < 0) break;
|
|
const line = inputBuffer.slice(0, newline).trim();
|
|
inputBuffer = inputBuffer.slice(newline + 1);
|
|
if (line) socket.write(clientFrame(Buffer.from(line)));
|
|
}
|
|
});
|
|
process.stdin.resume();
|
|
}
|
|
readFrames();
|
|
});
|
|
socket.once("error", (error) => {
|
|
process.stderr.write(`Codex app-server socket error: ${error.message}\n`);
|
|
process.exitCode = 1;
|
|
});
|
|
} catch (error) {
|
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
process.exitCode = 1;
|
|
}
|