Files
bun.sh/test/js/web/websocket/websocket-server-echo-headers-simple.mjs
robobun ee7608f7cf feat: support overriding Host, Sec-WebSocket-Key, and Sec-WebSocket-Protocol headers in WebSocket client (#22545)
## 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>
2025-09-11 19:36:01 -07:00

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);
});