mirror of
https://github.com/oven-sh/bun
synced 2026-02-12 11:59:00 +00:00
* fix(ws/client): handle short reads on payload frame length In the WebSocket specification, control frames may not be fragmented. However, the frame parser should handle fragmented control frames nonetheless. Whether or not the frame parser is given a set of fragmented bytes to parse is subject to the strategy in which the client buffers received bytes. All stages of the frame parser currently supports parsing frames fragmented across multiple TCP segments except for the payload frame length parsing stage. This commit implements buffering the bytes of a frame's payload length into a client instance so that the websocket client is able to properly parse payload frame lengths despite there being a short read over incoming TCP data. A test is added to test/js/web/websocket/websocket-client-short-read.test.ts which creates a make-shift WebSocket server that performs short writes over a single WebSocket frame. The test passes with this commit. * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
101 lines
4.1 KiB
TypeScript
101 lines
4.1 KiB
TypeScript
import { TCPSocketListener } from "bun";
|
|
import { describe, test, expect } from "bun:test";
|
|
import { WebSocket } from "ws";
|
|
|
|
const hostname = process.env.HOST || "127.0.0.1";
|
|
const port = parseInt(process.env.PORT || "0");
|
|
|
|
describe("WebSocket", () => {
|
|
test("short read on payload length", async () => {
|
|
let server: TCPSocketListener | undefined;
|
|
let client: WebSocket | undefined;
|
|
let init = false;
|
|
|
|
try {
|
|
server = Bun.listen({
|
|
socket: {
|
|
data(socket, data) {
|
|
if (init) {
|
|
return;
|
|
}
|
|
|
|
init = true;
|
|
|
|
const frame = data.toString("utf-8");
|
|
if (!frame.startsWith("GET")) {
|
|
throw new Error("Invalid handshake");
|
|
}
|
|
|
|
const magic = /Sec-WebSocket-Key: (.*)\r\n/.exec(frame);
|
|
if (!magic) {
|
|
throw new Error("Missing Sec-WebSocket-Key");
|
|
}
|
|
|
|
const hasher = new Bun.CryptoHasher("sha1");
|
|
hasher.update(magic[1]);
|
|
hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
|
const accept = hasher.digest("base64");
|
|
|
|
// Respond with a websocket handshake.
|
|
socket.write(
|
|
"HTTP/1.1 101 Switching Protocols\r\n" +
|
|
"Upgrade: websocket\r\n" +
|
|
"Connection: Upgrade\r\n" +
|
|
`Sec-WebSocket-Accept: ${accept}\r\n` +
|
|
"\r\n",
|
|
);
|
|
socket.flush();
|
|
|
|
// Partially write a websocket text frame with an incomplete big-endian u16 length.
|
|
socket.write(Uint8Array.from([129, 126, 0]));
|
|
socket.flush();
|
|
|
|
// Write the remainder of the websocket text frame.
|
|
setTimeout(() => {
|
|
socket.write(
|
|
Uint8Array.from([
|
|
253, 123, 34, 106, 115, 111, 110, 114, 112, 99, 34, 58, 34, 50, 46, 48, 34, 44, 34, 109, 101, 116,
|
|
104, 111, 100, 34, 58, 34, 116, 114, 97, 110, 115, 97, 99, 116, 105, 111, 110, 78, 111, 116, 105, 102,
|
|
105, 99, 97, 116, 105, 111, 110, 34, 44, 34, 112, 97, 114, 97, 109, 115, 34, 58, 123, 34, 114, 101,
|
|
115, 117, 108, 116, 34, 58, 123, 34, 99, 111, 110, 116, 101, 120, 116, 34, 58, 123, 34, 115, 108, 111,
|
|
116, 34, 58, 50, 52, 57, 54, 48, 50, 49, 55, 57, 125, 44, 34, 118, 97, 108, 117, 101, 34, 58, 123, 34,
|
|
115, 105, 103, 110, 97, 116, 117, 114, 101, 34, 58, 34, 50, 80, 50, 120, 102, 51, 109, 85, 49, 118,
|
|
114, 110, 89, 99, 100, 49, 76, 105, 99, 104, 56, 69, 76, 104, 104, 88, 120, 55, 50, 111, 67, 105, 110,
|
|
77, 97, 81, 88, 101, 113, 106, 118, 68, 55, 111, 52, 101, 75, 77, 53, 70, 66, 51, 78, 76, 97, 104, 86,
|
|
55, 68, 87, 101, 81, 106, 105, 102, 98, 107, 53, 56, 75, 121, 104, 66, 119, 98, 119, 88, 49, 104, 103,
|
|
119, 103, 112, 112, 102, 118, 77, 71, 34, 44, 34, 115, 108, 111, 116, 34, 58, 50, 52, 57, 54, 48, 50,
|
|
49, 55, 57, 125, 125, 44, 34, 115, 117, 98, 115, 99, 114, 105, 112, 116, 105, 111, 110, 34, 58, 52,
|
|
48, 50, 56, 125, 125,
|
|
]),
|
|
);
|
|
socket.flush();
|
|
}, 0);
|
|
},
|
|
},
|
|
hostname,
|
|
port,
|
|
});
|
|
|
|
const { promise, resolve } = Promise.withResolvers<string>();
|
|
|
|
client = new WebSocket(`ws://${server.hostname}:${server.port}`);
|
|
client.addEventListener("error", err => {
|
|
throw new Error(err.message);
|
|
});
|
|
client.addEventListener("close", err => {
|
|
if (!err.wasClean) {
|
|
throw new Error(err.reason);
|
|
}
|
|
});
|
|
client.addEventListener("message", event => resolve(event.data.toString("utf-8")));
|
|
|
|
expect(await promise).toEqual(
|
|
`{"jsonrpc":"2.0","method":"transactionNotification","params":{"result":{"context":{"slot":249602179},"value":{"signature":"2P2xf3mU1vrnYcd1Lich8ELhhXx72oCinMaQXeqjvD7o4eKM5FB3NLahV7DWeQjifbk58KyhBwbwX1hgwgppfvMG","slot":249602179}},"subscription":4028}}`,
|
|
);
|
|
} finally {
|
|
client?.close();
|
|
server?.stop(true);
|
|
}
|
|
});
|
|
});
|