Files

158 lines
3.8 KiB
JavaScript

import crypto from "node:crypto";
import { WebSocketServer } from "ws";
const wsServer = new WebSocketServer({ noServer: true });
function isWsLibrarySocket(socket) {
return socket && typeof socket.send === "function";
}
export function socketIsOpen(socket) {
if (!socket) return false;
if (isWsLibrarySocket(socket)) return socket.readyState === 1;
return !socket.destroyed;
}
export function closeWebSocket(socket) {
if (!socket) return;
try {
if (isWsLibrarySocket(socket)) {
socket.close();
socket.terminate?.();
return;
}
socket.end?.();
socket.destroy?.();
} catch {
// noop
}
}
export function sendWsFrame(socket, opcode, payload) {
if (!socket || socket.destroyed) return;
const len = payload.length;
let header;
if (len < 126) {
header = Buffer.alloc(2);
header[0] = 0x80 | opcode;
header[1] = len;
} else if (len < 65536) {
header = Buffer.alloc(4);
header[0] = 0x80 | opcode;
header[1] = 126;
header.writeUInt16BE(len, 2);
} else {
header = Buffer.alloc(10);
header[0] = 0x80 | opcode;
header[1] = 127;
header.writeBigUInt64BE(BigInt(len), 2);
}
try {
socket.write(header);
socket.write(payload);
} catch {
// noop
}
}
export function sendWsText(socket, text) {
if (isWsLibrarySocket(socket)) {
if (socket.readyState === 1) socket.send(String(text));
return;
}
sendWsFrame(socket, 0x01, Buffer.from(text, "utf-8"));
}
export function upgradeWebSocket(req, socket, head, onConnection) {
if (!Buffer.isBuffer(head)) return false;
wsServer.handleUpgrade(req, socket, head, (ws) => {
onConnection(ws);
});
return true;
}
export function acceptWebSocket(req, socket) {
const rawKey = req.headers["sec-websocket-key"];
const key = Array.isArray(rawKey) ? rawKey[0]?.trim() : rawKey?.trim();
if (!key) {
socket.destroy();
return false;
}
const acceptKey = crypto
.createHash("sha1")
.update(key + "258EAFA5-E914-47DA-95CA-5AB5DC085B11")
.digest("base64");
socket.write(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
`Sec-WebSocket-Accept: ${acceptKey}\r\n` +
"\r\n",
);
return true;
}
export function decodeWebSocketFrames(socket, onText) {
if (isWsLibrarySocket(socket)) {
socket.on("message", (data) => {
onText(Buffer.isBuffer(data) ? data.toString("utf-8") : String(data));
});
return;
}
let buffer = Buffer.alloc(0);
socket.on("data", (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
while (buffer.length >= 2) {
const firstByte = buffer[0];
const opcode = firstByte & 0x0f;
const secondByte = buffer[1];
const masked = (secondByte & 0x80) !== 0;
let payloadLen = secondByte & 0x7f;
let offset = 2;
if (payloadLen === 126) {
if (buffer.length < 4) return;
payloadLen = buffer.readUInt16BE(2);
offset = 4;
} else if (payloadLen === 127) {
if (buffer.length < 10) return;
payloadLen = Number(buffer.readBigUInt64BE(2));
offset = 10;
}
const maskSize = masked ? 4 : 0;
const totalLen = offset + maskSize + payloadLen;
if (buffer.length < totalLen) return;
let payload = buffer.subarray(offset + maskSize, totalLen);
if (masked) {
const mask = buffer.subarray(offset, offset + 4);
payload = Buffer.from(payload);
for (let i = 0; i < payload.length; i += 1) {
payload[i] ^= mask[i % 4];
}
}
buffer = buffer.subarray(totalLen);
if (opcode === 0x08) {
socket.end();
return;
}
if (opcode === 0x09) {
sendWsFrame(socket, 0x0a, payload);
continue;
}
if (opcode === 0x01) {
onText(payload.toString("utf-8"));
}
}
});
}