Files
bun.sh/test/js/web/fetch/websocket.helpers.ts
Ciro Spaciari 1779ee807c fix(fetch) handle 101 (#22390)
### What does this PR do?
Allow upgrade to websockets using fetch
This will avoid hanging in http.request and is a step necessary to
implement the upgrade event in the node:http client.
Changes in node:http need to be made in another PR to support 'upgrade'
event (see https://github.com/oven-sh/bun/pull/22412)
### How did you verify your code works?
Test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-09-04 18:06:47 -07:00

157 lines
4.0 KiB
TypeScript

import { createHash, randomBytes } from "node:crypto";
// RFC 6455 magic GUID
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
function makeKey() {
return randomBytes(16).toString("base64");
}
function acceptFor(key) {
return createHash("sha1")
.update(key + WS_GUID)
.digest("base64");
}
export function encodeCloseFrame(code = 1000, reason = "") {
const reasonBuf = Buffer.from(reason, "utf8");
const payloadLen = 2 + reasonBuf.length; // 2 bytes for code + reason
const header = [];
let headerLen = 2;
if (payloadLen < 126) {
// masked bit (0x80) + length
header.push(0x88, 0x80 | payloadLen);
} else if (payloadLen <= 0xffff) {
headerLen += 2;
header.push(0x88, 0x80 | 126, payloadLen >> 8, payloadLen & 0xff);
} else {
throw new Error("Close reason too long");
}
const mask = randomBytes(4);
const buf = Buffer.alloc(headerLen + 4 + payloadLen);
Buffer.from(header).copy(buf, 0);
mask.copy(buf, headerLen);
// write code + reason
const unmasked = Buffer.alloc(payloadLen);
unmasked.writeUInt16BE(code, 0);
reasonBuf.copy(unmasked, 2);
// apply mask
for (let i = 0; i < payloadLen; i++) {
buf[headerLen + 4 + i] = unmasked[i] ^ mask[i & 3];
}
return buf;
}
export function* decodeFrames(buffer) {
let i = 0;
while (i + 2 <= buffer.length) {
const b0 = buffer[i++];
const b1 = buffer[i++];
const fin = (b0 & 0x80) !== 0;
const opcode = b0 & 0x0f;
const masked = (b1 & 0x80) !== 0;
let len = b1 & 0x7f;
if (len === 126) {
if (i + 2 > buffer.length) break;
len = buffer.readUInt16BE(i);
i += 2;
} else if (len === 127) {
if (i + 8 > buffer.length) break;
const big = buffer.readBigUInt64BE(i);
i += 8;
if (big > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("frame too large");
len = Number(big);
}
let mask;
if (masked) {
if (i + 4 > buffer.length) break;
mask = buffer.subarray(i, i + 4);
i += 4;
}
if (i + len > buffer.length) break;
let payload = buffer.subarray(i, i + len);
i += len;
if (masked && mask) {
const unmasked = Buffer.alloc(len);
for (let j = 0; j < len; j++) unmasked[j] = payload[j] ^ mask[j & 3];
payload = unmasked;
}
if (!fin) throw new Error("fragmentation not supported in this demo");
if (opcode === 0x1) {
// text
yield payload.toString("utf8");
} else if (opcode === 0x8) {
// CLOSE
yield { type: "close" };
return;
} else if (opcode === 0x9) {
// PING -> respond with PONG if you implement writes here
yield { type: "ping", data: payload };
} else if (opcode === 0xa) {
// PONG
yield { type: "pong", data: payload };
} else {
// ignore other opcodes for brevity
}
}
}
// Encode a single unfragmented TEXT frame (client -> server must be masked)
export function encodeTextFrame(str) {
const payload = Buffer.from(str, "utf8");
const len = payload.length;
let headerLen = 2;
if (len >= 126 && len <= 0xffff) headerLen += 2;
else if (len > 0xffff) headerLen += 8;
const maskKeyLen = 4;
const buf = Buffer.alloc(headerLen + maskKeyLen + len);
// FIN=1, RSV=0, opcode=0x1 (text)
buf[0] = 0x80 | 0x1;
// Set masked bit and length field(s)
let offset = 1;
if (len < 126) {
buf[offset++] = 0x80 | len; // mask bit + length
} else if (len <= 0xffff) {
buf[offset++] = 0x80 | 126;
buf.writeUInt16BE(len, offset);
offset += 2;
} else {
buf[offset++] = 0x80 | 127;
buf.writeBigUInt64BE(BigInt(len), offset);
offset += 8;
}
// Mask key
const mask = randomBytes(4);
mask.copy(buf, offset);
offset += 4;
// Mask the payload
for (let i = 0; i < len; i++) {
buf[offset + i] = payload[i] ^ mask[i & 3];
}
return buf;
}
export function upgradeHeaders() {
const secWebSocketKey = makeKey();
return {
"Connection": "Upgrade",
"Upgrade": "websocket",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Key": secWebSocketKey,
};
}