mirror of
https://github.com/oven-sh/bun
synced 2026-02-04 07:58:54 +00:00
Compare commits
6 Commits
dylan/pyth
...
claude/soc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
323676070e | ||
|
|
1cc64bd98f | ||
|
|
786ca6e769 | ||
|
|
c14b979792 | ||
|
|
56429a65e4 | ||
|
|
381411d298 |
@@ -546,6 +546,7 @@ src/http/MimeType.zig
|
||||
src/http/ProxyTunnel.zig
|
||||
src/http/SendFile.zig
|
||||
src/http/Signals.zig
|
||||
src/http/SOCKSProxy.zig
|
||||
src/http/ThreadSafeStreamBuffer.zig
|
||||
src/http/URLPath.zig
|
||||
src/http/websocket_client.zig
|
||||
|
||||
84
src/http.zig
84
src/http.zig
@@ -200,6 +200,11 @@ pub fn onClose(
|
||||
tunnel.shutdown();
|
||||
tunnel.detachAndDeref();
|
||||
}
|
||||
if (client.socks_proxy) |socks| {
|
||||
client.socks_proxy = null;
|
||||
socks.shutdown();
|
||||
socks.detachAndDeref();
|
||||
}
|
||||
const in_progress = client.state.stage != .done and client.state.stage != .fail and client.state.flags.is_redirect_pending == false;
|
||||
if (client.state.flags.is_redirect_pending) {
|
||||
// if the connection is closed and we are pending redirect just do the redirect
|
||||
@@ -433,6 +438,7 @@ request_content_len_buf: ["-4294967295".len]u8 = undefined,
|
||||
http_proxy: ?URL = null,
|
||||
proxy_authorization: ?[]u8 = null,
|
||||
proxy_tunnel: ?*ProxyTunnel = null,
|
||||
socks_proxy: ?*SOCKSProxy = null,
|
||||
signals: Signals = .{},
|
||||
async_http_id: u32 = 0,
|
||||
hostname: ?[]u8 = null,
|
||||
@@ -451,6 +457,10 @@ pub fn deinit(this: *HTTPClient) void {
|
||||
this.proxy_tunnel = null;
|
||||
tunnel.detachAndDeref();
|
||||
}
|
||||
if (this.socks_proxy) |socks| {
|
||||
this.socks_proxy = null;
|
||||
socks.detachAndDeref();
|
||||
}
|
||||
this.unix_socket_path.deinit();
|
||||
this.unix_socket_path = JSC.ZigString.Slice.empty;
|
||||
}
|
||||
@@ -460,7 +470,7 @@ pub fn isKeepAlivePossible(this: *HTTPClient) bool {
|
||||
// TODO keepalive for unix sockets
|
||||
if (this.unix_socket_path.length() > 0) return false;
|
||||
// is not possible to reuse Proxy with TSL, so disable keepalive if url is tunneling HTTPS
|
||||
if (this.proxy_tunnel != null or (this.http_proxy != null and this.url.isHTTPS())) {
|
||||
if (this.proxy_tunnel != null or this.socks_proxy != null or (this.http_proxy != null and this.url.isHTTPS())) {
|
||||
log("Keep-Alive release (proxy tunneling https)", .{});
|
||||
return false;
|
||||
}
|
||||
@@ -708,6 +718,14 @@ pub fn doRedirect(
|
||||
log("close socket in redirect", .{});
|
||||
NewHTTPContext(is_ssl).closeSocket(socket);
|
||||
}
|
||||
} else if (this.socks_proxy) |socks| {
|
||||
log("close the socks proxy in redirect", .{});
|
||||
this.socks_proxy = null;
|
||||
socks.detachAndDeref();
|
||||
if (!socket.isClosed()) {
|
||||
log("close socket in redirect", .{});
|
||||
NewHTTPContext(is_ssl).closeSocket(socket);
|
||||
}
|
||||
} else {
|
||||
// we need to clean the client reference before closing the socket because we are going to reuse the same ref in a another request
|
||||
if (this.isKeepAlivePossible()) {
|
||||
@@ -739,6 +757,10 @@ pub fn doRedirect(
|
||||
this.proxy_tunnel = null;
|
||||
tunnel.detachAndDeref();
|
||||
}
|
||||
if (this.socks_proxy) |socks| {
|
||||
this.socks_proxy = null;
|
||||
socks.detachAndDeref();
|
||||
}
|
||||
|
||||
return this.start(.{ .bytes = request_body }, body_out_str);
|
||||
}
|
||||
@@ -883,8 +905,13 @@ noinline fn sendInitialRequestPayload(this: *HTTPClient, comptime is_first_call:
|
||||
|
||||
const request = this.buildRequest(this.state.original_request_body.len());
|
||||
|
||||
if (this.http_proxy) |_| {
|
||||
if (this.url.isHTTPS()) {
|
||||
if (this.http_proxy) |proxy| {
|
||||
if (proxy.isSOCKS()) {
|
||||
log("start SOCKS proxy tunneling", .{});
|
||||
// SOCKS proxy - always requires tunneling regardless of target protocol
|
||||
this.flags.proxy_tunneling = true;
|
||||
// Don't write any HTTP headers yet - SOCKS handshake happens first
|
||||
} else if (this.url.isHTTPS()) {
|
||||
log("start proxy tunneling (https proxy)", .{});
|
||||
//DO the tunneling!
|
||||
this.flags.proxy_tunneling = true;
|
||||
@@ -1283,9 +1310,23 @@ pub fn closeAndFail(this: *HTTPClient, err: anyerror, comptime is_ssl: bool, soc
|
||||
|
||||
fn startProxyHandshake(this: *HTTPClient, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket, start_payload: []const u8) void {
|
||||
log("startProxyHandshake", .{});
|
||||
// if we have options we pass them (ca, reject_unauthorized, etc) otherwise use the default
|
||||
const ssl_options = if (this.tls_props != null) this.tls_props.?.* else JSC.API.ServerConfig.SSLConfig.zero;
|
||||
ProxyTunnel.start(this, is_ssl, socket, ssl_options, start_payload);
|
||||
|
||||
if (this.http_proxy) |proxy| {
|
||||
if (proxy.isSOCKS()) {
|
||||
// Start SOCKS proxy handshake
|
||||
this.socks_proxy = SOCKSProxy.create(this.allocator, proxy, this.url.hostname, this.url.getPortAuto()) catch |err| {
|
||||
this.closeAndFail(err, is_ssl, socket);
|
||||
return;
|
||||
};
|
||||
if (this.socks_proxy) |socks| {
|
||||
socks.sendAuthHandshake(is_ssl, socket);
|
||||
}
|
||||
} else {
|
||||
// HTTP proxy - use existing ProxyTunnel
|
||||
const ssl_options = if (this.tls_props != null) this.tls_props.?.* else JSC.API.ServerConfig.SSLConfig.zero;
|
||||
ProxyTunnel.start(this, is_ssl, socket, ssl_options, start_payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fn handleShortRead(
|
||||
@@ -1399,12 +1440,24 @@ pub fn handleOnDataHeaders(
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.flags.proxy_tunneling and this.proxy_tunnel == null) {
|
||||
if (this.flags.proxy_tunneling and this.proxy_tunnel == null and this.socks_proxy == null) {
|
||||
// we are proxing we dont need to cloneMetadata yet
|
||||
this.startProxyHandshake(is_ssl, socket, body_buf);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle SOCKS proxy data
|
||||
if (this.socks_proxy) |socks| {
|
||||
const consumed = socks.handleData(this, body_buf, is_ssl, socket) catch |err| {
|
||||
this.closeAndFail(err, is_ssl, socket);
|
||||
return;
|
||||
};
|
||||
if (consumed) {
|
||||
return; // SOCKS handshake consumed the data
|
||||
}
|
||||
// Fall through to normal HTTP processing if SOCKS proxy is connected
|
||||
}
|
||||
|
||||
// we have body data incoming so we clone metadata and keep going
|
||||
this.cloneMetadata();
|
||||
|
||||
@@ -1537,6 +1590,11 @@ fn fail(this: *HTTPClient, err: anyerror) void {
|
||||
// always detach the socket from the tunnel in case of fail
|
||||
tunnel.detachAndDeref();
|
||||
}
|
||||
if (this.socks_proxy) |socks| {
|
||||
this.socks_proxy = null;
|
||||
socks.shutdown();
|
||||
socks.detachAndDeref();
|
||||
}
|
||||
if (this.state.stage != .done and this.state.stage != .fail) {
|
||||
this.state.request_stage = .fail;
|
||||
this.state.response_stage = .fail;
|
||||
@@ -1626,6 +1684,15 @@ pub fn progressUpdate(this: *HTTPClient, comptime is_ssl: bool, ctx: *NewHTTPCon
|
||||
log("close socket", .{});
|
||||
NewHTTPContext(is_ssl).closeSocket(socket);
|
||||
}
|
||||
} else if (this.socks_proxy) |socks| {
|
||||
log("close the socks proxy", .{});
|
||||
this.socks_proxy = null;
|
||||
socks.shutdown();
|
||||
socks.detachAndDeref();
|
||||
if (!socket.isClosed()) {
|
||||
log("close socket", .{});
|
||||
NewHTTPContext(is_ssl).closeSocket(socket);
|
||||
}
|
||||
} else {
|
||||
if (this.isKeepAlivePossible() and !socket.isClosedOrHasError()) {
|
||||
log("release socket", .{});
|
||||
@@ -2159,7 +2226,7 @@ pub fn handleResponseMetadata(
|
||||
}
|
||||
}
|
||||
|
||||
if (this.flags.proxy_tunneling and this.proxy_tunnel == null) {
|
||||
if (this.flags.proxy_tunneling and this.proxy_tunnel == null and this.socks_proxy == null) {
|
||||
if (response.status_code == 200) {
|
||||
// signal to continue the proxing
|
||||
return ShouldContinue.continue_streaming;
|
||||
@@ -2452,6 +2519,7 @@ const SSLConfig = @import("./bun.js/api/server.zig").ServerConfig.SSLConfig;
|
||||
const uws = bun.uws;
|
||||
const HTTPCertError = @import("./http/HTTPCertError.zig");
|
||||
const ProxyTunnel = @import("./http/ProxyTunnel.zig");
|
||||
const SOCKSProxy = @import("./http/SOCKSProxy.zig");
|
||||
pub const Headers = @import("./http/Headers.zig");
|
||||
pub const MimeType = @import("./http/MimeType.zig");
|
||||
pub const URLPath = @import("./http/URLPath.zig");
|
||||
|
||||
@@ -263,6 +263,9 @@ pub fn connect(this: *@This(), client: *HTTPClient, comptime is_ssl: bool) !NewH
|
||||
if (url.protocol.len == 0 or strings.eqlComptime(url.protocol, "https") or strings.eqlComptime(url.protocol, "http")) {
|
||||
return try this.context(is_ssl).connect(client, url.hostname, url.getPortAuto());
|
||||
}
|
||||
if (url.isSOCKS()) {
|
||||
return try this.context(is_ssl).connect(client, url.hostname, url.getPortAuto());
|
||||
}
|
||||
return error.UnsupportedProxyProtocol;
|
||||
}
|
||||
return try custom_context.connect(client, client.url.hostname, client.url.getPortAuto());
|
||||
@@ -274,6 +277,9 @@ pub fn connect(this: *@This(), client: *HTTPClient, comptime is_ssl: bool) !NewH
|
||||
if (url.protocol.len == 0 or strings.eqlComptime(url.protocol, "https") or strings.eqlComptime(url.protocol, "http")) {
|
||||
return try this.context(is_ssl).connect(client, url.hostname, url.getPortAuto());
|
||||
}
|
||||
if (url.isSOCKS()) {
|
||||
return try this.context(is_ssl).connect(client, url.hostname, url.getPortAuto());
|
||||
}
|
||||
return error.UnsupportedProxyProtocol;
|
||||
}
|
||||
}
|
||||
|
||||
318
src/http/SOCKSProxy.zig
Normal file
318
src/http/SOCKSProxy.zig
Normal file
@@ -0,0 +1,318 @@
|
||||
const SOCKSProxy = @This();
|
||||
const RefCount = bun.ptr.RefCount(@This(), "ref_count", SOCKSProxy.deinit, .{});
|
||||
pub const ref = SOCKSProxy.RefCount.ref;
|
||||
pub const deref = SOCKSProxy.RefCount.deref;
|
||||
|
||||
state: SOCKSState = .init,
|
||||
destination_host: []const u8 = "",
|
||||
destination_port: u16 = 0,
|
||||
proxy_url: URL,
|
||||
allocator: std.mem.Allocator,
|
||||
ref_count: RefCount,
|
||||
write_buffer: std.ArrayList(u8),
|
||||
write_offset: usize = 0,
|
||||
|
||||
const SOCKSState = enum {
|
||||
init,
|
||||
auth_handshake,
|
||||
auth_handshake_pending,
|
||||
auth_complete,
|
||||
connect_request,
|
||||
connect_request_pending,
|
||||
connected,
|
||||
failed,
|
||||
};
|
||||
|
||||
const SOCKSVersion = enum(u8) {
|
||||
v5 = 0x05,
|
||||
};
|
||||
|
||||
const SOCKSAuthMethod = enum(u8) {
|
||||
no_auth = 0x00,
|
||||
gssapi = 0x01,
|
||||
username_password = 0x02,
|
||||
no_acceptable = 0xFF,
|
||||
};
|
||||
|
||||
const SOCKSCommand = enum(u8) {
|
||||
connect = 0x01,
|
||||
bind = 0x02,
|
||||
udp_associate = 0x03,
|
||||
};
|
||||
|
||||
const SOCKSAddressType = enum(u8) {
|
||||
ipv4 = 0x01,
|
||||
domain_name = 0x03,
|
||||
ipv6 = 0x04,
|
||||
};
|
||||
|
||||
const SOCKSReply = enum(u8) {
|
||||
succeeded = 0x00,
|
||||
general_failure = 0x01,
|
||||
connection_not_allowed = 0x02,
|
||||
network_unreachable = 0x03,
|
||||
host_unreachable = 0x04,
|
||||
connection_refused = 0x05,
|
||||
ttl_expired = 0x06,
|
||||
command_not_supported = 0x07,
|
||||
address_type_not_supported = 0x08,
|
||||
};
|
||||
|
||||
pub fn create(allocator: std.mem.Allocator, proxy_url: URL, destination_host: []const u8, destination_port: u16) !*SOCKSProxy {
|
||||
// Clone the destination_host to ensure memory safety
|
||||
const cloned_host = try allocator.dupe(u8, destination_host);
|
||||
|
||||
const socks_proxy = bun.new(SOCKSProxy, .{
|
||||
.ref_count = .init(),
|
||||
.proxy_url = proxy_url,
|
||||
.destination_host = cloned_host,
|
||||
.destination_port = destination_port,
|
||||
.allocator = allocator,
|
||||
.write_buffer = std.ArrayList(u8).init(allocator),
|
||||
.write_offset = 0,
|
||||
});
|
||||
|
||||
return socks_proxy;
|
||||
}
|
||||
|
||||
pub fn sendAuthHandshake(this: *SOCKSProxy, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void {
|
||||
// SOCKS5 authentication handshake
|
||||
// +----+----------+----------+
|
||||
// |VER | NMETHODS | METHODS |
|
||||
// +----+----------+----------+
|
||||
// | 1 | 1 | 1 to 255 |
|
||||
// +----+----------+----------+
|
||||
const auth_request = [_]u8{ @intFromEnum(SOCKSVersion.v5), 1, @intFromEnum(SOCKSAuthMethod.no_auth) };
|
||||
|
||||
// Prepare write buffer
|
||||
this.write_buffer.clearRetainingCapacity();
|
||||
this.write_buffer.appendSlice(&auth_request) catch return;
|
||||
this.write_offset = 0;
|
||||
|
||||
this.flushWriteBuffer(is_ssl, socket);
|
||||
}
|
||||
|
||||
fn flushWriteBuffer(this: *SOCKSProxy, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void {
|
||||
if (this.write_offset >= this.write_buffer.items.len) {
|
||||
// All data has been written
|
||||
this.completeWrite();
|
||||
return;
|
||||
}
|
||||
|
||||
const remaining = this.write_buffer.items[this.write_offset..];
|
||||
const bytes_written = socket.write(remaining);
|
||||
|
||||
if (bytes_written > 0) {
|
||||
this.write_offset += @intCast(bytes_written);
|
||||
|
||||
if (this.write_offset >= this.write_buffer.items.len) {
|
||||
// All data has been written
|
||||
this.completeWrite();
|
||||
} else {
|
||||
// Still have data to write, mark as pending
|
||||
this.markWritePending();
|
||||
}
|
||||
} else {
|
||||
// No bytes written, mark as pending and wait for socket to be writable
|
||||
this.markWritePending();
|
||||
}
|
||||
}
|
||||
|
||||
fn markWritePending(this: *SOCKSProxy) void {
|
||||
switch (this.state) {
|
||||
.auth_handshake => this.state = .auth_handshake_pending,
|
||||
.connect_request => this.state = .connect_request_pending,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn completeWrite(this: *SOCKSProxy) void {
|
||||
switch (this.state) {
|
||||
.auth_handshake, .auth_handshake_pending => this.state = .auth_handshake,
|
||||
.connect_request, .connect_request_pending => this.state = .connect_request,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sendConnectRequest(this: *SOCKSProxy, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) !void {
|
||||
// SOCKS5 connect request
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
|
||||
// Clear and reuse the write buffer
|
||||
this.write_buffer.clearRetainingCapacity();
|
||||
|
||||
// Version, Command, Reserved
|
||||
try this.write_buffer.appendSlice(&[_]u8{ @intFromEnum(SOCKSVersion.v5), @intFromEnum(SOCKSCommand.connect), 0x00 });
|
||||
|
||||
// Address type and address
|
||||
if (strings.isIPAddress(this.destination_host)) {
|
||||
if (strings.indexOf(this.destination_host, ":")) |_| {
|
||||
// IPv6
|
||||
try this.write_buffer.append(@intFromEnum(SOCKSAddressType.ipv6));
|
||||
const parsed = std.net.Ip6Address.parse(this.destination_host, 0) catch {
|
||||
return error.InvalidIPv6Address;
|
||||
};
|
||||
try this.write_buffer.appendSlice(std.mem.asBytes(&parsed.sa.addr));
|
||||
} else {
|
||||
// IPv4
|
||||
try this.write_buffer.append(@intFromEnum(SOCKSAddressType.ipv4));
|
||||
const parsed = std.net.Ip4Address.parse(this.destination_host, 0) catch {
|
||||
return error.InvalidIPv4Address;
|
||||
};
|
||||
try this.write_buffer.appendSlice(std.mem.asBytes(&parsed.sa.addr));
|
||||
}
|
||||
} else {
|
||||
// Domain name
|
||||
try this.write_buffer.append(@intFromEnum(SOCKSAddressType.domain_name));
|
||||
if (this.destination_host.len > 255) {
|
||||
return error.DomainNameTooLong;
|
||||
}
|
||||
try this.write_buffer.append(@intCast(this.destination_host.len));
|
||||
try this.write_buffer.appendSlice(this.destination_host);
|
||||
}
|
||||
|
||||
// Port (big-endian)
|
||||
const port_bytes = std.mem.toBytes(std.mem.nativeToBig(u16, this.destination_port));
|
||||
try this.write_buffer.appendSlice(&port_bytes);
|
||||
|
||||
// Reset write offset and start writing
|
||||
this.write_offset = 0;
|
||||
this.state = .connect_request;
|
||||
this.flushWriteBuffer(is_ssl, socket);
|
||||
}
|
||||
|
||||
pub fn onWritable(this: *SOCKSProxy, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) void {
|
||||
// Socket is writable again, try to continue any pending writes
|
||||
switch (this.state) {
|
||||
.auth_handshake_pending, .connect_request_pending => {
|
||||
this.flushWriteBuffer(is_ssl, socket);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handleData(this: *SOCKSProxy, client: *HTTPClient, data: []const u8, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket) !bool {
|
||||
_ = client;
|
||||
switch (this.state) {
|
||||
.auth_handshake_pending => {
|
||||
// Still writing auth handshake, ignore incoming data for now
|
||||
return true;
|
||||
},
|
||||
.auth_handshake => {
|
||||
if (data.len < 2) {
|
||||
return error.IncompleteSOCKSResponse;
|
||||
}
|
||||
|
||||
const version = data[0];
|
||||
const method = data[1];
|
||||
|
||||
if (version != @intFromEnum(SOCKSVersion.v5)) {
|
||||
return error.UnsupportedSOCKSVersion;
|
||||
}
|
||||
|
||||
if (method == @intFromEnum(SOCKSAuthMethod.no_acceptable)) {
|
||||
return error.SOCKSAuthenticationFailed;
|
||||
}
|
||||
|
||||
if (method == @intFromEnum(SOCKSAuthMethod.no_auth)) {
|
||||
this.state = .auth_complete;
|
||||
try this.sendConnectRequest(is_ssl, socket);
|
||||
} else {
|
||||
return error.UnsupportedSOCKSAuthMethod;
|
||||
}
|
||||
|
||||
return true; // Data was consumed by SOCKS handshake
|
||||
},
|
||||
.connect_request_pending => {
|
||||
// Still writing connect request, ignore incoming data for now
|
||||
return true;
|
||||
},
|
||||
.connect_request => {
|
||||
if (data.len < 4) {
|
||||
return error.IncompleteSOCKSResponse;
|
||||
}
|
||||
|
||||
const version = data[0];
|
||||
const reply = data[1];
|
||||
// data[2] is reserved
|
||||
const atyp = data[3];
|
||||
|
||||
if (version != @intFromEnum(SOCKSVersion.v5)) {
|
||||
return error.UnsupportedSOCKSVersion;
|
||||
}
|
||||
|
||||
if (reply != @intFromEnum(SOCKSReply.succeeded)) {
|
||||
return error.SOCKSConnectionFailed;
|
||||
}
|
||||
|
||||
// Parse the bound address (we don't need it, but need to skip it)
|
||||
var offset: usize = 4;
|
||||
switch (atyp) {
|
||||
@intFromEnum(SOCKSAddressType.ipv4) => offset += 4,
|
||||
@intFromEnum(SOCKSAddressType.ipv6) => offset += 16,
|
||||
@intFromEnum(SOCKSAddressType.domain_name) => {
|
||||
if (data.len <= offset) return error.IncompleteSOCKSResponse;
|
||||
offset += 1 + data[offset]; // domain length + domain
|
||||
},
|
||||
else => return error.UnsupportedSOCKSAddressType,
|
||||
}
|
||||
offset += 2; // port
|
||||
|
||||
if (data.len < offset) {
|
||||
return error.IncompleteSOCKSResponse;
|
||||
}
|
||||
|
||||
this.state = .connected;
|
||||
log("SOCKS proxy connected successfully", .{});
|
||||
|
||||
// SOCKS handshake complete, HTTP traffic can now flow through the tunnel
|
||||
// Don't change proxy_tunneling flag - let the normal flow handle it
|
||||
|
||||
// If there's any remaining data after the SOCKS response, process it as HTTP
|
||||
if (data.len > offset) {
|
||||
return false; // Let HTTP client process remaining data
|
||||
}
|
||||
|
||||
return true; // Data was consumed by SOCKS handshake
|
||||
},
|
||||
.connected => {
|
||||
// Pass through data to the HTTP client
|
||||
return false; // Let HTTP client handle this data
|
||||
},
|
||||
else => {
|
||||
return error.UnexpectedSOCKSState;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(this: *SOCKSProxy) void {
|
||||
this.state = .failed;
|
||||
}
|
||||
|
||||
pub fn shutdown(this: *SOCKSProxy) void {
|
||||
this.close();
|
||||
}
|
||||
|
||||
pub fn detachAndDeref(this: *SOCKSProxy) void {
|
||||
this.deref();
|
||||
}
|
||||
|
||||
fn deinit(this: *SOCKSProxy) void {
|
||||
// Free cloned destination_host memory
|
||||
this.allocator.free(this.destination_host);
|
||||
// Clean up write buffer
|
||||
this.write_buffer.deinit();
|
||||
bun.destroy(this);
|
||||
}
|
||||
|
||||
const bun = @import("bun");
|
||||
const std = @import("std");
|
||||
const strings = bun.strings;
|
||||
const NewHTTPContext = bun.http.NewHTTPContext;
|
||||
const HTTPClient = bun.http;
|
||||
const URL = bun.URL;
|
||||
const log = bun.Output.scoped(.http_socks_proxy, false);
|
||||
12
src/url.zig
12
src/url.zig
@@ -112,6 +112,18 @@ pub const URL = struct {
|
||||
return strings.eqlComptime(this.protocol, "http");
|
||||
}
|
||||
|
||||
pub inline fn isSOCKS5(this: *const URL) bool {
|
||||
return strings.eqlComptime(this.protocol, "socks5");
|
||||
}
|
||||
|
||||
pub inline fn isSOCKS5h(this: *const URL) bool {
|
||||
return strings.eqlComptime(this.protocol, "socks5h");
|
||||
}
|
||||
|
||||
pub inline fn isSOCKS(this: *const URL) bool {
|
||||
return this.isSOCKS5() or this.isSOCKS5h();
|
||||
}
|
||||
|
||||
pub fn displayHostname(this: *const URL) string {
|
||||
if (this.hostname.len > 0) {
|
||||
return this.hostname;
|
||||
|
||||
103
test/js/bun/http/socks-proxy-env.test.ts
Normal file
103
test/js/bun/http/socks-proxy-env.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
describe("SOCKS proxy environment variables", () => {
|
||||
test("should read SOCKS proxy from http_proxy environment variable", async () => {
|
||||
const dir = tempDirWithFiles("socks-http-proxy", {
|
||||
"test.js": `
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:8888/nonexistent");
|
||||
console.log("UNEXPECTED_SUCCESS");
|
||||
} catch (error) {
|
||||
console.log("PROXY_ATTEMPTED");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const env = {
|
||||
...bunEnv,
|
||||
http_proxy: "socks5://127.0.0.1:65432",
|
||||
};
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
env,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout.trim()).toBe("PROXY_ATTEMPTED");
|
||||
});
|
||||
|
||||
test("should read SOCKS proxy from https_proxy environment variable", async () => {
|
||||
const dir = tempDirWithFiles("socks-https-proxy", {
|
||||
"test.js": `
|
||||
try {
|
||||
const response = await fetch("https://127.0.0.1:8888/nonexistent");
|
||||
console.log("UNEXPECTED_SUCCESS");
|
||||
} catch (error) {
|
||||
console.log("PROXY_ATTEMPTED");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const env = {
|
||||
...bunEnv,
|
||||
https_proxy: "socks5h://127.0.0.1:65432",
|
||||
};
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
env,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout.trim()).toBe("PROXY_ATTEMPTED");
|
||||
});
|
||||
|
||||
test("should handle invalid SOCKS proxy URLs gracefully", async () => {
|
||||
const dir = tempDirWithFiles("socks-invalid-url", {
|
||||
"test.js": `
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:8888/test", {
|
||||
proxy: "invalid-proxy-url"
|
||||
});
|
||||
console.log("UNEXPECTED_SUCCESS");
|
||||
} catch (error) {
|
||||
console.log("INVALID_PROXY_ERROR");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout.trim()).toBe("INVALID_PROXY_ERROR");
|
||||
});
|
||||
});
|
||||
112
test/js/bun/http/socks-proxy.test.ts
Normal file
112
test/js/bun/http/socks-proxy.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
describe("SOCKS proxy", () => {
|
||||
test("should detect SOCKS5 proxy URLs in environment variables", async () => {
|
||||
const dir = tempDirWithFiles("socks-env-test", {
|
||||
"test.js": `
|
||||
console.log(JSON.stringify({
|
||||
http_proxy: process.env.http_proxy,
|
||||
https_proxy: process.env.https_proxy
|
||||
}));
|
||||
`,
|
||||
});
|
||||
|
||||
const env = {
|
||||
...bunEnv,
|
||||
http_proxy: "socks5://127.0.0.1:1080",
|
||||
https_proxy: "socks5h://proxy.example.com:9050",
|
||||
};
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
env,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
const output = JSON.parse(stdout.trim());
|
||||
expect(output.http_proxy).toBe("socks5://127.0.0.1:1080");
|
||||
expect(output.https_proxy).toBe("socks5h://proxy.example.com:9050");
|
||||
});
|
||||
|
||||
test("should handle connection errors gracefully for unreachable SOCKS proxy", async () => {
|
||||
const dir = tempDirWithFiles("socks-error-test", {
|
||||
"test.js": `
|
||||
try {
|
||||
const response = await fetch("http://127.0.0.1:1234/test", {
|
||||
proxy: "socks5://127.0.0.1:65432"
|
||||
});
|
||||
console.log("UNEXPECTED_SUCCESS");
|
||||
} catch (error) {
|
||||
console.log("CONNECTION_ERROR");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
expect(stdout.trim()).toBe("CONNECTION_ERROR");
|
||||
});
|
||||
|
||||
test("should support SOCKS5 and SOCKS5h URL schemes", async () => {
|
||||
const dir = tempDirWithFiles("socks-schemes-test", {
|
||||
"test.js": `
|
||||
// Test that the URLs are parsed without throwing
|
||||
try {
|
||||
await fetch("http://127.0.0.1:1234/test", {
|
||||
proxy: "socks5://127.0.0.1:1080"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("socks5-attempted");
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch("http://127.0.0.1:1234/test", {
|
||||
proxy: "socks5h://127.0.0.1:1080"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("socks5h-attempted");
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test.js"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
|
||||
const output = stdout.trim();
|
||||
expect(output).toContain("socks5-attempted");
|
||||
expect(output).toContain("socks5h-attempted");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user