Add SOCKS proxy support for Bun HTTP client

Implements SOCKS5 and SOCKS5h proxy support as requested in issue #16812.

This adds native SOCKS proxy functionality to Bun's HTTP client:
- Support for socks5:// and socks5h:// protocols
- Environment variable support (http_proxy, https_proxy)
- Direct proxy option support in fetch()
- Full SOCKS5 handshake implementation
- Integration with existing HTTP proxy infrastructure

Key changes:
- Add SOCKSProxy.zig implementing SOCKS5 protocol
- Update HTTPThread.zig to recognize SOCKS protocols
- Modify HTTP client to handle SOCKS proxy tunneling
- Add URL helpers for SOCKS protocol detection
- Include comprehensive test coverage

Resolves issue #16812

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-07-20 06:51:27 +00:00
parent f380458bae
commit 381411d298
7 changed files with 583 additions and 8 deletions

View File

@@ -0,0 +1,118 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { spawn, type ChildProcess } from "bun";
describe("SOCKS proxy environment variables", () => {
let mockSocksServer: ChildProcess;
let socksPort: number;
let httpPort: number;
beforeAll(async () => {
// Find available ports
socksPort = 9050 + Math.floor(Math.random() * 1000);
httpPort = 8080 + Math.floor(Math.random() * 1000);
// Start a mock SOCKS5 server for testing
mockSocksServer = spawn({
cmd: ["node", "-e", `
const net = require('net');
const server = net.createServer((socket) => {
console.log('SOCKS connection received');
socket.on('data', (data) => {
console.log('SOCKS data:', data.toString('hex'));
// Handle SOCKS5 auth handshake
if (data.length === 3 && data[0] === 0x05) {
// Send "no auth required" response
socket.write(Buffer.from([0x05, 0x00]));
return;
}
// Handle SOCKS5 connect request
if (data.length >= 4 && data[0] === 0x05 && data[1] === 0x01) {
// Send success response with dummy bind address
const response = Buffer.from([
0x05, 0x00, 0x00, 0x01, // VER, REP, RSV, ATYP
127, 0, 0, 1, // Bind IP (127.0.0.1)
0x1F, 0x90 // Bind port (8080)
]);
socket.write(response);
// Now proxy data to the target HTTP server
const targetSocket = net.connect(${httpPort}, '127.0.0.1');
socket.pipe(targetSocket);
targetSocket.pipe(socket);
return;
}
});
socket.on('error', console.error);
});
server.listen(${socksPort}, () => {
console.log('Mock SOCKS server listening on port ${socksPort}');
});
`],
stdout: "inherit",
stderr: "inherit",
});
// Start a simple HTTP server for testing
using httpServer = Bun.serve({
port: httpPort,
fetch(req) {
if (req.url.endsWith("/test")) {
return new Response("Hello from HTTP server via SOCKS");
}
return new Response("Not found", { status: 404 });
},
});
// Wait a bit for servers to start
await new Promise(resolve => setTimeout(resolve, 2000));
});
afterAll(() => {
if (mockSocksServer) {
mockSocksServer.kill();
}
});
test("should connect through SOCKS5 proxy via http_proxy environment variable", async () => {
const originalProxy = process.env.http_proxy;
try {
process.env.http_proxy = `socks5://127.0.0.1:${socksPort}`;
const response = await fetch(`http://127.0.0.1:${httpPort}/test`);
expect(response.status).toBe(200);
expect(await response.text()).toBe("Hello from HTTP server via SOCKS");
} finally {
if (originalProxy !== undefined) {
process.env.http_proxy = originalProxy;
} else {
delete process.env.http_proxy;
}
}
});
test("should connect through SOCKS5h proxy via http_proxy environment variable", async () => {
const originalProxy = process.env.http_proxy;
try {
process.env.http_proxy = `socks5h://127.0.0.1:${socksPort}`;
const response = await fetch(`http://localhost:${httpPort}/test`);
expect(response.status).toBe(200);
expect(await response.text()).toBe("Hello from HTTP server via SOCKS");
} finally {
if (originalProxy !== undefined) {
process.env.http_proxy = originalProxy;
} else {
delete process.env.http_proxy;
}
}
});
});

View File

@@ -0,0 +1,130 @@
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
import { spawn, type ChildProcess } from "bun";
describe("SOCKS proxy", () => {
let mockSocksServer: ChildProcess;
let socksPort: number;
let httpPort: number;
beforeAll(async () => {
// Find available ports
socksPort = 9050 + Math.floor(Math.random() * 1000);
httpPort = 8080 + Math.floor(Math.random() * 1000);
// Start a mock SOCKS5 server for testing
mockSocksServer = spawn({
cmd: ["node", "-e", `
const net = require('net');
const server = net.createServer((socket) => {
console.log('SOCKS connection received');
socket.on('data', (data) => {
console.log('SOCKS data:', data.toString('hex'));
// Handle SOCKS5 auth handshake
if (data.length === 3 && data[0] === 0x05) {
// Send "no auth required" response
socket.write(Buffer.from([0x05, 0x00]));
return;
}
// Handle SOCKS5 connect request
if (data.length >= 4 && data[0] === 0x05 && data[1] === 0x01) {
// Send success response with dummy bind address
const response = Buffer.from([
0x05, 0x00, 0x00, 0x01, // VER, REP, RSV, ATYP
127, 0, 0, 1, // Bind IP (127.0.0.1)
0x1F, 0x90 // Bind port (8080)
]);
socket.write(response);
// Now proxy data to the target HTTP server
const targetSocket = net.connect(${httpPort}, '127.0.0.1');
socket.pipe(targetSocket);
targetSocket.pipe(socket);
return;
}
});
socket.on('error', console.error);
});
server.listen(${socksPort}, () => {
console.log('Mock SOCKS server listening on port ${socksPort}');
});
`],
stdout: "inherit",
stderr: "inherit",
});
// Start a simple HTTP server for testing
using httpServer = Bun.serve({
port: httpPort,
fetch(req) {
if (req.url.endsWith("/test")) {
return new Response("Hello from HTTP server");
}
return new Response("Not found", { status: 404 });
},
});
// Wait a bit for servers to start
await new Promise(resolve => setTimeout(resolve, 1000));
});
afterAll(() => {
if (mockSocksServer) {
mockSocksServer.kill();
}
});
test("should connect through SOCKS5 proxy", async () => {
const response = await fetch(`http://127.0.0.1:${httpPort}/test`, {
// @ts-ignore - This might not be typed yet
proxy: `socks5://127.0.0.1:${socksPort}`,
});
expect(response.status).toBe(200);
expect(await response.text()).toBe("Hello from HTTP server");
});
test("should connect through SOCKS5h proxy", async () => {
const response = await fetch(`http://localhost:${httpPort}/test`, {
// @ts-ignore - This might not be typed yet
proxy: `socks5h://127.0.0.1:${socksPort}`,
});
expect(response.status).toBe(200);
expect(await response.text()).toBe("Hello from HTTP server");
});
test("should handle SOCKS proxy via environment variable", async () => {
const originalProxy = process.env.http_proxy;
try {
process.env.http_proxy = `socks5://127.0.0.1:${socksPort}`;
const response = await fetch(`http://127.0.0.1:${httpPort}/test`);
expect(response.status).toBe(200);
expect(await response.text()).toBe("Hello from HTTP server");
} finally {
if (originalProxy !== undefined) {
process.env.http_proxy = originalProxy;
} else {
delete process.env.http_proxy;
}
}
});
test("should handle SOCKS proxy connection failure", async () => {
const invalidPort = 65000;
const promise = fetch(`http://127.0.0.1:${httpPort}/test`, {
// @ts-ignore
proxy: `socks5://127.0.0.1:${invalidPort}`,
});
await expect(promise).rejects.toThrow();
});
});