Files
healer-man/server/websocket.mjs
T
2026-09-05 20:42:38 -04:00

120 lines
4.3 KiB
JavaScript

import { createHash } from "node:crypto";
const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
function frame(opcode, payload = Buffer.alloc(0)) {
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
let header;
if (body.length < 126) {
header = Buffer.from([0x80 | opcode, body.length]);
} else if (body.length <= 0xffff) {
header = Buffer.allocUnsafe(4);
header[0] = 0x80 | opcode;
header[1] = 126;
header.writeUInt16BE(body.length, 2);
} else {
header = Buffer.allocUnsafe(10);
header[0] = 0x80 | opcode;
header[1] = 127;
header.writeBigUInt64BE(BigInt(body.length), 2);
}
return Buffer.concat([header, body]);
}
export function upgradeArenaWebSocket(request, socket, head, arenaService) {
const url = new URL(request.url ?? "/", "http://localhost");
if (url.pathname !== "/ws/arena") return false;
const key = String(request.headers["sec-websocket-key"] ?? "");
const version = String(request.headers["sec-websocket-version"] ?? "");
const accountId = arenaService.consumeSocketTicket(url.searchParams.get("ticket") ?? "");
if (!accountId || !key || version !== "13") {
socket.end("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
return true;
}
const accept = createHash("sha1").update(`${key}${WEBSOCKET_GUID}`).digest("base64");
socket.write([
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${accept}`,
"\r\n",
].join("\r\n"));
let buffered = Buffer.from(head ?? Buffer.alloc(0));
let closed = false;
const send = (message) => {
if (!closed && socket.writable) socket.write(frame(0x1, JSON.stringify(message)));
};
const unsubscribe = arenaService.subscribe(accountId, send);
function close(code = 1000, reason = "") {
if (closed) return;
closed = true;
const reasonBytes = Buffer.from(String(reason).slice(0, 100));
const body = Buffer.allocUnsafe(2 + reasonBytes.length);
body.writeUInt16BE(code, 0);
reasonBytes.copy(body, 2);
if (socket.writable) socket.end(frame(0x8, body));
unsubscribe();
}
function parse() {
while (buffered.length >= 2) {
const first = buffered[0];
const second = buffered[1];
const final = Boolean(first & 0x80);
const opcode = first & 0x0f;
const masked = Boolean(second & 0x80);
let length = second & 0x7f;
let offset = 2;
if (!final || !masked) return close(1002, "Invalid client frame");
if (length === 126) {
if (buffered.length < 4) return;
length = buffered.readUInt16BE(2);
offset = 4;
} else if (length === 127) {
if (buffered.length < 10) return;
const largeLength = buffered.readBigUInt64BE(2);
if (largeLength > 65536n) return close(1009, "Frame too large");
length = Number(largeLength);
offset = 10;
}
if (length > 65536) return close(1009, "Frame too large");
if (buffered.length < offset + 4 + length) return;
const mask = buffered.subarray(offset, offset + 4);
const payload = Buffer.from(buffered.subarray(offset + 4, offset + 4 + length));
buffered = buffered.subarray(offset + 4 + length);
for (let index = 0; index < payload.length; index += 1) payload[index] ^= mask[index % 4];
if (opcode === 0x8) return close();
if (opcode === 0x9) {
if (socket.writable) socket.write(frame(0xa, payload));
continue;
}
if (opcode !== 0x1) return close(1003, "Text messages only");
try {
const message = JSON.parse(payload.toString("utf8"));
const result = arenaService.receive(accountId, message);
if (!result.accepted) send({ type: "intent-rejected", sequence: message?.sequence, reason: result.reason });
} catch {
send({ type: "protocol-error", message: "Arena messages must be valid JSON." });
}
}
}
socket.on("data", (chunk) => {
if (closed) return;
buffered = Buffer.concat([buffered, chunk]);
parse();
});
socket.on("error", () => close(1011, "Socket error"));
socket.on("close", () => {
if (!closed) {
closed = true;
unsubscribe();
}
});
if (buffered.length) parse();
send({ type: "connected", queue: arenaService.queueState(accountId) });
return true;
}