mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
## Summary
Add `proxy` option to WebSocket constructor for connecting through HTTP
CONNECT proxies.
### Features
- Support for `ws://` and `wss://` through HTTP proxies
- Support for `ws://` and `wss://` through HTTPS proxies (with
`rejectUnauthorized: false`)
- Proxy authentication via URL credentials (Basic auth)
- Custom proxy headers support
- Full TLS options (`ca`, `cert`, `key`, etc.) for target connections
using `SSLConfig.fromJS`
### API
```javascript
// String format
new WebSocket("wss://example.com", { proxy: "http://proxy:8080" })
// With credentials
new WebSocket("wss://example.com", { proxy: "http://user:pass@proxy:8080" })
// Object format with custom headers
new WebSocket("wss://example.com", {
proxy: { url: "http://proxy:8080", headers: { "X-Custom": "value" } }
})
// HTTPS proxy
new WebSocket("ws://example.com", {
proxy: "https://proxy:8443",
tls: { rejectUnauthorized: false }
})
```
### Implementation
| File | Changes |
|------|---------|
| `WebSocketUpgradeClient.zig` | Proxy state machine and CONNECT
handling |
| `WebSocketProxyTunnel.zig` | **New** - TLS tunnel inside CONNECT for
wss:// through HTTP proxy |
| `JSWebSocket.cpp` | Parse proxy option and TLS options using
`SSLConfig.fromJS` |
| `WebSocket.cpp` | Pass proxy parameters to Zig, handle HTTPS proxy
socket selection |
| `bun.d.ts` | Add `proxy` and full TLS options to WebSocket types |
### Supported Scenarios
| Scenario | Status |
|----------|--------|
| ws:// through HTTP proxy | ✅ Working |
| wss:// through HTTP proxy | ✅ Working (TLS tunnel) |
| ws:// through HTTPS proxy | ✅ Working (with `rejectUnauthorized:
false`) |
| wss:// through HTTPS proxy | ✅ Working (with `rejectUnauthorized:
false`) |
| Proxy authentication (Basic) | ✅ Working |
| Custom proxy headers | ✅ Working |
| Custom CA for HTTPS proxy | ✅ Working |
## Test plan
- [x] API tests verify proxy option is accepted in various formats
- [x] Functional tests with local HTTP CONNECT proxy server
- [x] Proxy authentication tests (Basic auth)
- [x] HTTPS proxy tests with `rejectUnauthorized: false`
- [x] Error handling tests (auth failures, wrong credentials)
Run tests: `bun test test/js/web/websocket/websocket-proxy.test.ts`
## Changelog
- Added `proxy` option to `WebSocket` constructor for HTTP/HTTPS proxy
support
- Added full TLS options (`ca`, `cert`, `key`, `passphrase`, etc.) to
`WebSocket` constructor
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
198 lines
5.8 KiB
TypeScript
198 lines
5.8 KiB
TypeScript
/**
|
|
* Shared utilities for WebSocket proxy tests.
|
|
* Used by both websocket-proxy.test.ts and ws-proxy.test.ts
|
|
*/
|
|
|
|
import { tls as tlsCerts } from "harness";
|
|
import net from "net";
|
|
import tls from "tls";
|
|
|
|
export interface ConnectProxyOptions {
|
|
requireAuth?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Create an HTTP CONNECT proxy server using Node's net module.
|
|
* This proxy handles the CONNECT method to establish tunnels for WebSocket connections.
|
|
*/
|
|
export function createConnectProxy(options: ConnectProxyOptions = {}): net.Server {
|
|
return net.createServer(clientSocket => {
|
|
let buffer = Buffer.alloc(0);
|
|
let tunnelEstablished = false;
|
|
let targetSocket: net.Socket | null = null;
|
|
|
|
clientSocket.on("data", data => {
|
|
// If tunnel is already established, forward data directly
|
|
if (tunnelEstablished && targetSocket) {
|
|
targetSocket.write(data);
|
|
return;
|
|
}
|
|
|
|
buffer = Buffer.concat([buffer, data]);
|
|
const bufferStr = buffer.toString();
|
|
|
|
// Check if we have complete headers
|
|
const headerEnd = bufferStr.indexOf("\r\n\r\n");
|
|
if (headerEnd === -1) return;
|
|
|
|
const headerPart = bufferStr.substring(0, headerEnd);
|
|
const lines = headerPart.split("\r\n");
|
|
const requestLine = lines[0];
|
|
const headers: Record<string, string> = {};
|
|
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
if (line === "") break;
|
|
const colonIdx = line.indexOf(": ");
|
|
if (colonIdx > 0) {
|
|
headers[line.substring(0, colonIdx).toLowerCase()] = line.substring(colonIdx + 2);
|
|
}
|
|
}
|
|
|
|
// Check for CONNECT method
|
|
const match = requestLine.match(/^CONNECT\s+([^:]+):(\d+)\s+HTTP/);
|
|
if (!match) {
|
|
clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
clientSocket.end();
|
|
return;
|
|
}
|
|
|
|
const [, targetHost, targetPort] = match;
|
|
|
|
// Check auth if required
|
|
if (options.requireAuth) {
|
|
const authHeader = headers["proxy-authorization"];
|
|
if (!authHeader) {
|
|
clientSocket.write("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n");
|
|
clientSocket.end();
|
|
return;
|
|
}
|
|
|
|
const auth = Buffer.from(authHeader.replace("Basic ", "").trim(), "base64").toString("utf8");
|
|
if (auth !== "proxy_user:proxy_pass") {
|
|
clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
clientSocket.end();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Get any data after the headers (shouldn't be any for CONNECT)
|
|
const remainingData = buffer.subarray(headerEnd + 4);
|
|
|
|
// Connect to target
|
|
targetSocket = net.connect(parseInt(targetPort), targetHost, () => {
|
|
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
tunnelEstablished = true;
|
|
|
|
// Forward any remaining data
|
|
if (remainingData.length > 0) {
|
|
targetSocket!.write(remainingData);
|
|
}
|
|
|
|
// Set up bidirectional piping
|
|
targetSocket!.on("data", chunk => {
|
|
clientSocket.write(chunk);
|
|
});
|
|
});
|
|
|
|
targetSocket.on("error", () => {
|
|
if (!tunnelEstablished) {
|
|
clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
}
|
|
clientSocket.end();
|
|
});
|
|
|
|
targetSocket.on("close", () => clientSocket.destroy());
|
|
clientSocket.on("close", () => targetSocket?.destroy());
|
|
});
|
|
|
|
clientSocket.on("error", () => {
|
|
targetSocket?.destroy();
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create an HTTPS CONNECT proxy server using Node's tls module.
|
|
* This proxy handles TLS-encrypted CONNECT tunnels.
|
|
*/
|
|
export function createTLSConnectProxy(): tls.Server {
|
|
return tls.createServer(
|
|
{
|
|
key: tlsCerts.key,
|
|
cert: tlsCerts.cert,
|
|
},
|
|
clientSocket => {
|
|
let buffer = Buffer.alloc(0);
|
|
let tunnelEstablished = false;
|
|
let targetSocket: net.Socket | null = null;
|
|
|
|
clientSocket.on("data", data => {
|
|
if (tunnelEstablished && targetSocket) {
|
|
targetSocket.write(data);
|
|
return;
|
|
}
|
|
|
|
buffer = Buffer.concat([buffer, data]);
|
|
const bufferStr = buffer.toString();
|
|
|
|
const headerEnd = bufferStr.indexOf("\r\n\r\n");
|
|
if (headerEnd === -1) return;
|
|
|
|
const headerPart = bufferStr.substring(0, headerEnd);
|
|
const lines = headerPart.split("\r\n");
|
|
const requestLine = lines[0];
|
|
|
|
const match = requestLine.match(/^CONNECT\s+([^:]+):(\d+)\s+HTTP/);
|
|
if (!match) {
|
|
clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
clientSocket.end();
|
|
return;
|
|
}
|
|
|
|
const [, targetHost, targetPort] = match;
|
|
const remainingData = buffer.subarray(headerEnd + 4);
|
|
|
|
targetSocket = net.connect(parseInt(targetPort), targetHost, () => {
|
|
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
tunnelEstablished = true;
|
|
|
|
if (remainingData.length > 0) {
|
|
targetSocket!.write(remainingData);
|
|
}
|
|
|
|
targetSocket!.on("data", chunk => {
|
|
clientSocket.write(chunk);
|
|
});
|
|
});
|
|
|
|
targetSocket.on("error", () => {
|
|
if (!tunnelEstablished) {
|
|
clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
}
|
|
clientSocket.end();
|
|
});
|
|
|
|
targetSocket.on("close", () => clientSocket.destroy());
|
|
clientSocket.on("close", () => targetSocket?.destroy());
|
|
});
|
|
|
|
clientSocket.on("error", () => {
|
|
targetSocket?.destroy();
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper to start a proxy server and get its port.
|
|
*/
|
|
export async function startProxy(server: net.Server | tls.Server): Promise<number> {
|
|
return new Promise<number>(resolve => {
|
|
server.listen(0, "127.0.0.1", () => {
|
|
const addr = server.address() as net.AddressInfo;
|
|
resolve(addr.port);
|
|
});
|
|
});
|
|
}
|