mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
### 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>
157 lines
4.0 KiB
TypeScript
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,
|
|
};
|
|
}
|