mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## Summary Adds support for overriding special WebSocket headers (`Host`, `Sec-WebSocket-Key`, and `Sec-WebSocket-Protocol`) via the headers option when creating a WebSocket connection. ## Changes - Modified `WebSocketUpgradeClient.zig` to check for and use user-provided special headers - Added header value validation to prevent CRLF injection attacks - Updated the NonUTF8Headers struct to automatically filter duplicate headers - When a custom `Sec-WebSocket-Protocol` header is provided, it properly updates the subprotocols list for validation ## Implementation Details The implementation adds minimal code by: 1. Using the existing `NonUTF8Headers` struct's methods to find valid header overrides 2. Automatically filtering out WebSocket-specific headers in the format method to prevent duplication 3. Maintaining a single, clean code path in `buildRequestBody()` ## Testing Added comprehensive tests in `websocket-custom-headers.test.ts` that verify: - Custom Host header support - Custom Sec-WebSocket-Key header support - Custom Sec-WebSocket-Protocol header support - Header override behavior when both protocols array and header are provided - CRLF injection prevention - Protection of system headers (Connection, Upgrade, etc.) - Support for additional custom headers All existing WebSocket tests continue to pass, ensuring backward compatibility. ## Security The implementation includes validation to: - Reject header values with control characters (preventing CRLF injection) - Prevent users from overriding critical system headers like Connection and Upgrade - Validate header values according to RFC 7230 specifications ## Use Cases This feature enables: - Testing WebSocket servers with specific header requirements - Connecting through proxies that require custom Host headers - Implementing custom WebSocket subprotocol negotiation - Debugging WebSocket connections with specific keys Fixes #[issue_number] --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
111 lines
2.9 KiB
JavaScript
111 lines
2.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import { createServer } from "http";
|
|
import crypto from "crypto";
|
|
|
|
const port = 0;
|
|
|
|
function generateAccept(key) {
|
|
return crypto
|
|
.createHash("sha1")
|
|
.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
|
.digest("base64");
|
|
}
|
|
|
|
const server = createServer();
|
|
|
|
let connectedSocket = null;
|
|
let requestHeaders = {};
|
|
|
|
server.on("upgrade", (request, socket, head) => {
|
|
// Store the headers
|
|
requestHeaders = request.headers;
|
|
|
|
const key = request.headers["sec-websocket-key"];
|
|
const accept = generateAccept(key);
|
|
|
|
// Build response headers
|
|
let responseHeaders = [
|
|
"HTTP/1.1 101 Switching Protocols",
|
|
"Upgrade: websocket",
|
|
"Connection: Upgrade",
|
|
`Sec-WebSocket-Accept: ${accept}`,
|
|
];
|
|
|
|
// Echo back the protocol if provided
|
|
if (request.headers["sec-websocket-protocol"]) {
|
|
// Just echo back the first protocol
|
|
const protocols = request.headers["sec-websocket-protocol"].split(",")[0].trim();
|
|
responseHeaders.push(`Sec-WebSocket-Protocol: ${protocols}`);
|
|
}
|
|
|
|
responseHeaders.push("", ""); // Empty line to end headers
|
|
|
|
socket.write(responseHeaders.join("\r\n"));
|
|
|
|
connectedSocket = socket;
|
|
|
|
// Send headers as first message (simple text frame)
|
|
const message = JSON.stringify({
|
|
type: "headers",
|
|
headers: requestHeaders,
|
|
});
|
|
|
|
// Simple WebSocket text frame
|
|
const messageBuffer = Buffer.from(message);
|
|
|
|
// Handle payload length encoding
|
|
let frame;
|
|
if (messageBuffer.length < 126) {
|
|
frame = Buffer.allocUnsafe(2 + messageBuffer.length);
|
|
frame[0] = 0x81; // FIN + text opcode
|
|
frame[1] = messageBuffer.length; // Payload length (no masking for server)
|
|
messageBuffer.copy(frame, 2);
|
|
} else if (messageBuffer.length < 65536) {
|
|
frame = Buffer.allocUnsafe(4 + messageBuffer.length);
|
|
frame[0] = 0x81; // FIN + text opcode
|
|
frame[1] = 126; // Extended payload length (16-bit)
|
|
frame.writeUInt16BE(messageBuffer.length, 2);
|
|
messageBuffer.copy(frame, 4);
|
|
} else {
|
|
// For very large messages (unlikely in our test)
|
|
frame = Buffer.allocUnsafe(10 + messageBuffer.length);
|
|
frame[0] = 0x81; // FIN + text opcode
|
|
frame[1] = 127; // Extended payload length (64-bit)
|
|
frame.writeBigUInt64BE(BigInt(messageBuffer.length), 2);
|
|
messageBuffer.copy(frame, 10);
|
|
}
|
|
|
|
socket.write(frame);
|
|
|
|
socket.on("data", (data) => {
|
|
// Simple echo - just bounce back any frames we receive
|
|
// This is not a full WebSocket implementation but enough for testing
|
|
if (data[0] === 0x88) {
|
|
// Close frame
|
|
socket.end();
|
|
}
|
|
});
|
|
|
|
socket.on("error", (err) => {
|
|
console.error("Socket error:", err);
|
|
});
|
|
});
|
|
|
|
server.listen(port, () => {
|
|
const { port } = server.address();
|
|
const url = `ws://localhost:${port}`;
|
|
|
|
if (process.send) {
|
|
process.send({ href: url });
|
|
} else {
|
|
console.log(url);
|
|
}
|
|
});
|
|
|
|
process.on("SIGTERM", () => {
|
|
if (connectedSocket) {
|
|
connectedSocket.end();
|
|
}
|
|
server.close();
|
|
process.exit(0);
|
|
}); |