From c97a32e24b4875fc57a3c85aa299d7f4a2edd7e9 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 24 Jun 2025 15:10:20 +0800 Subject: [PATCH 1/2] Initial work on CONNECT proxy. Cannot currently connect to the proxy over TLS (though, once connected, it can connect to the actual site over TLS). No support for authentication. --- src/app.zig | 9 +- src/http/client.zig | 226 ++++++++++++++++++++++++++++++++++++++------ src/main.zig | 136 ++++++++++++++++---------- 3 files changed, 291 insertions(+), 80 deletions(-) diff --git a/src/app.zig b/src/app.zig index 803c55f0..30489755 100644 --- a/src/app.zig +++ b/src/app.zig @@ -3,7 +3,8 @@ const Allocator = std.mem.Allocator; const log = @import("log.zig"); const Loop = @import("runtime/loop.zig").Loop; -const HttpClient = @import("http/client.zig").Client; +const http = @import("http/client.zig"); + const Telemetry = @import("telemetry/telemetry.zig").Telemetry; const Notification = @import("notification.zig").Notification; @@ -14,7 +15,7 @@ pub const App = struct { config: Config, allocator: Allocator, telemetry: Telemetry, - http_client: HttpClient, + http_client: http.Client, app_dir_path: ?[]const u8, notification: *Notification, @@ -29,6 +30,7 @@ pub const App = struct { run_mode: RunMode, tls_verify_host: bool = true, http_proxy: ?std.Uri = null, + proxy_type: ?http.ProxyType = null, }; pub fn init(allocator: Allocator, config: Config) !*App { @@ -52,9 +54,10 @@ pub const App = struct { .telemetry = undefined, .app_dir_path = app_dir_path, .notification = notification, - .http_client = try HttpClient.init(allocator, .{ + .http_client = try http.Client.init(allocator, .{ .max_concurrent = 3, .http_proxy = config.http_proxy, + .proxy_type = config.proxy_type, .tls_verify_host = config.tls_verify_host, }), .config = config, diff --git a/src/http/client.zig b/src/http/client.zig index 72fc466f..859e2827 100644 --- a/src/http/client.zig +++ b/src/http/client.zig @@ -41,6 +41,11 @@ const BUFFER_LEN = 32 * 1024; const MAX_HEADER_LINE_LEN = 4096; +pub const ProxyType = enum { + simple, + connect, +}; + // Thread-safe. Holds our root certificate, connection pool and state pool // Used to create Requests. pub const Client = struct { @@ -48,6 +53,7 @@ pub const Client = struct { allocator: Allocator, state_pool: StatePool, http_proxy: ?Uri, + proxy_type: ?ProxyType, root_ca: tls.config.CertBundle, tls_verify_host: bool = true, connection_manager: ConnectionManager, @@ -56,6 +62,7 @@ pub const Client = struct { const Opts = struct { max_concurrent: usize = 3, http_proxy: ?std.Uri = null, + proxy_type: ?ProxyType = null, tls_verify_host: bool = true, max_idle_connection: usize = 10, }; @@ -76,6 +83,7 @@ pub const Client = struct { .allocator = allocator, .state_pool = state_pool, .http_proxy = opts.http_proxy, + .proxy_type = if (opts.http_proxy == null) null else (opts.proxy_type orelse .connect), .tls_verify_host = opts.tls_verify_host, .connection_manager = connection_manager, .request_pool = std.heap.MemoryPool(Request).init(allocator), @@ -186,6 +194,16 @@ pub const Client = struct { pub fn freeSlotCount(self: *Client) usize { return self.state_pool.freeSlotCount(); } + + fn isConnectProxy(self: *const Client) bool { + const proxy_type = self.proxy_type orelse return false; + return proxy_type == .connect; + } + + fn isSimpleProxy(self: *const Client) bool { + const proxy_type = self.proxy_type orelse return false; + return proxy_type == .simple; + } }; const RequestOpts = struct { @@ -330,6 +348,7 @@ pub const Request = struct { _keepalive: bool, // extracted from request_uri + _request_port: u16, _request_host: []const u8, // extracted from connect_uri @@ -420,6 +439,7 @@ pub const Request = struct { ._connect_host = decomposed.connect_host, ._connect_port = decomposed.connect_port, ._request_host = decomposed.request_host, + ._request_port = decomposed.request_port, ._state = state, ._client = client, ._aborter = null, @@ -455,6 +475,7 @@ pub const Request = struct { connect_port: u16, connect_host: []const u8, connect_uri: *const std.Uri, + request_port: u16, request_host: []const u8, }; fn decomposeURL(client: *const Client, uri: *const Uri) !DecomposedURL { @@ -470,8 +491,10 @@ pub const Request = struct { connect_host = proxy.host.?.percent_encoded; } + const is_connect_proxy = client.isConnectProxy(); + var secure: bool = undefined; - const scheme = connect_uri.scheme; + const scheme = if (is_connect_proxy) uri.scheme else connect_uri.scheme; if (std.ascii.eqlIgnoreCase(scheme, "https")) { secure = true; } else if (std.ascii.eqlIgnoreCase(scheme, "http")) { @@ -479,13 +502,15 @@ pub const Request = struct { } else { return error.UnsupportedUriScheme; } - const connect_port: u16 = connect_uri.port orelse if (secure) 443 else 80; + const request_port: u16 = uri.port orelse if (secure) 443 else 80; + const connect_port: u16 = connect_uri.port orelse (if (is_connect_proxy) 80 else request_port); return .{ .secure = secure, .connect_port = connect_port, .connect_host = connect_host, .connect_uri = connect_uri, + .request_port = request_port, .request_host = request_host, }; } @@ -595,13 +620,18 @@ pub const Request = struct { }; self._connection = connection; + const is_connect_proxy = self._client.isConnectProxy(); + if (is_connect_proxy) { + try SyncHandler.connect(self); + } + if (self._secure) { self._connection.?.tls = .{ .blocking = try tls.client(std.net.Stream{ .handle = socket }, .{ - .host = self._connect_host, + .host = if (is_connect_proxy) self._request_host else self._connect_host, .root_ca = self._client.root_ca, .insecure_skip_verify = self._tls_verify_host == false, - // .key_log_callback = tls.config.key_log.callback, + .key_log_callback = tls.config.key_log.callback, }), }; } @@ -682,7 +712,7 @@ pub const Request = struct { if (self._secure) { connection.tls = .{ .nonblocking = try tls.nb.Client().init(self._client.allocator, .{ - .host = self._connect_host, + .host = if (self._client.isConnectProxy()) self._request_host else self._connect_host, .root_ca = self._client.root_ca, .insecure_skip_verify = self._tls_verify_host == false, // .key_log_callback = tls.config.key_log.callback, @@ -831,7 +861,7 @@ pub const Request = struct { } fn buildHeader(self: *Request) ![]const u8 { - const proxied = self.connect_uri != self.request_uri; + const proxied = self._client.isSimpleProxy(); const buf = self._state.header_buf; var fbs = std.io.fixedBufferStream(buf); @@ -851,6 +881,16 @@ pub const Request = struct { return buf[0..fbs.pos]; } + fn buildConnectHeader(self: *Request) ![]const u8 { + const buf = self._state.header_buf; + var fbs = std.io.fixedBufferStream(buf); + var writer = fbs.writer(); + + try writer.print("CONNECT {s}:{d} HTTP/1.1\r\n", .{ self._request_host, self._request_port }); + try writer.print("Host: {s}:{d}\r\n\r\n", .{ self._request_host, self._request_port }); + return buf[0..fbs.pos]; + } + fn requestStarting(self: *Request) void { const notification = self.notification orelse return; if (self._notified_start) { @@ -895,6 +935,15 @@ pub const Request = struct { .headers = response.headers.items, }); } + + fn shouldProxyConnect(self: *const Request) bool { + // if the connection comes from a keepalive pool, than we already + // made a CONNECT request + if (self._connection_from_keepalive) { + return false; + } + return self._client.isConnectProxy(); + } }; // Handles asynchronous requests @@ -958,6 +1007,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type { const SendQueue = std.DoublyLinkedList([]const u8); const SendState = enum { + connect, handshake, header, body, @@ -986,7 +1036,19 @@ fn AsyncHandler(comptime H: type, comptime L: type) type { if (self.shutdown) { return self.maybeShutdown(); } + result catch |err| return self.handleError("Connection failed", err); + + if (self.request.shouldProxyConnect()) { + self.state = .connect; + const header = self.request.buildConnectHeader() catch |err| { + return self.handleError("Failed to build CONNECT header", err); + }; + self.send(header); + self.receive(); + return; + } + self.conn.connected() catch |err| { self.handleError("connected handler error", err); }; @@ -1056,6 +1118,12 @@ fn AsyncHandler(comptime H: type, comptime L: type) type { return; } + if (self.state == .connect) { + // We're in a proxy CONNECT flow. There's nothing for us to + // do except for wait for the response. + return; + } + self.conn.sent() catch |err| { self.handleError("send handling", err); }; @@ -1099,7 +1167,27 @@ fn AsyncHandler(comptime H: type, comptime L: type) type { return self.handleError("Connection closed", error.ConnectionResetByPeer); } - const status = self.conn.received(self.read_buf[0 .. self.read_pos + n]) catch |err| { + const data = self.read_buf[0 .. self.read_pos + n]; + + if (self.state == .connect) { + const success = self.reader.connectResponse(data) catch |err| { + return self.handleError("Invalid CONNECT response", err); + }; + + if (!success) { + self.receive(); + } else { + // CONNECT was successful, resume our normal flow + self.state = .handshake; + self.reader = self.request.newReader(); + self.conn.connected() catch |err| { + self.handleError("connected handler error", err); + }; + } + return; + } + + const status = self.conn.received(data) catch |err| { if (err == error.TlsAlertCloseNotify and self.state == .handshake and self.maybeRetryRequest()) { return; } @@ -1438,7 +1526,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type { const handler = self.handler; switch (self.protocol) { .plain => switch (handler.state) { - .handshake => unreachable, + .handshake, .connect => unreachable, .header => { handler.state = .body; if (handler.request.body) |body| { @@ -1455,6 +1543,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type { return; } switch (handler.state) { + .connect => unreachable, .handshake => return self.sendSecureHeader(tls_client), .header => { handler.state = .body; @@ -1589,6 +1678,37 @@ const SyncHandler = struct { } } + // Unfortunately, this is called from the Request doSendSync since we need + // to do this before setting up our TLS connection. + fn connect(request: *Request) !void { + const socket = request._connection.?.socket; + + const header = try request.buildConnectHeader(); + try Conn.writeAll(socket, header); + + var pos: usize = 0; + var reader = request.newReader(); + var read_buf = request._state.read_buf; + + while (true) { + // we would never 'maybeRetryOrErr' on a CONNECT request, because + // we only send CONNECT requests on newly established connections + // and maybeRetryOrErr is only for connections that might have been + // closed while being kept-alive + const n = try posix.read(socket, read_buf[pos..]); + if (n == 0) { + return error.ConnectionResetByPeer; + } + pos += n; + if (try reader.connectResponse(read_buf[0..pos])) { + // returns true if we have a successful connect response + return; + } + + // we don't have enough data yet. + } + } + fn maybeRetryOrErr(self: *SyncHandler, err: anyerror) !Response { var request = self.request; @@ -1828,6 +1948,26 @@ const Reader = struct { return .{ .use_get = use_get, .location = location }; } + fn connectResponse(self: *Reader, data: []u8) !bool { + const result = try self.process(data); + if (self.header_done == false) { + return false; + } + + if (result.done == false) { + // CONNECT responses should not have a body. If the header is + // done, then the entire response should be done. + return error.InvalidConnectResponse; + } + + const status = self.response.status; + if (status < 200 or status > 299) { + return error.InvalidConnectResponseStatus; + } + + return true; + } + fn process(self: *Reader, data: []u8) ProcessError!Result { if (self.body_reader) |*br| { const ok, const result = try br.process(data); @@ -2790,14 +2930,14 @@ test "HttpClient Reader: fuzz" { } test "HttpClient: invalid url" { - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); const uri = try Uri.parse("http:///"); try testing.expectError(error.UriMissingHost, client.request(.GET, &uri)); } test "HttpClient: sync connect error" { - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); const uri = try Uri.parse("HTTP://127.0.0.1:9920"); @@ -2809,7 +2949,7 @@ test "HttpClient: sync connect error" { test "HttpClient: sync no body" { for (0..2) |i| { - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/simple"); @@ -2831,7 +2971,7 @@ test "HttpClient: sync no body" { test "HttpClient: sync tls no body" { for (0..1) |_| { - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); const uri = try Uri.parse("https://127.0.0.1:9581/http_client/simple"); @@ -2850,7 +2990,33 @@ test "HttpClient: sync tls no body" { test "HttpClient: sync with body" { for (0..2) |i| { - var client = try testClient(); + var client = try testClient(.{}); + defer client.deinit(); + + const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo"); + var req = try client.request(.GET, &uri); + defer req.deinit(); + + var res = try req.sendSync(.{}); + + if (i == 0) { + try testing.expectEqual("over 9000!", try res.peek()); + } + try testing.expectEqual("over 9000!", try res.next()); + try testing.expectEqual(201, res.header.status); + try testing.expectEqual(5, res.header.count()); + try testing.expectEqual("Close", res.header.get("connection")); + try testing.expectEqual("10", res.header.get("content-length")); + try testing.expectEqual("127.0.0.1", res.header.get("_host")); + try testing.expectEqual("Lightpanda/1.0", res.header.get("_user-agent")); + try testing.expectEqual("*/*", res.header.get("_accept")); + } +} + +test "HttpClient: sync with body proxy CONNECT" { + for (0..2) |i| { + const proxy_uri = try Uri.parse("http://127.0.0.1:9582/"); + var client = try testClient(.{ .proxy_type = .connect, .http_proxy = proxy_uri }); defer client.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/echo"); @@ -2875,7 +3041,7 @@ test "HttpClient: sync with body" { test "HttpClient: sync with gzip body" { for (0..2) |i| { - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/gzip"); @@ -2897,7 +3063,7 @@ test "HttpClient: sync tls with body" { defer arr.deinit(testing.allocator); try arr.ensureTotalCapacity(testing.allocator, 20); - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); for (0..5) |_| { defer arr.clearRetainingCapacity(); @@ -2927,7 +3093,7 @@ test "HttpClient: sync redirect from TLS to Plaintext" { for (0..5) |_| { defer arr.clearRetainingCapacity(); - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); const uri = try Uri.parse("https://127.0.0.1:9581/http_client/redirect/insecure"); @@ -2957,7 +3123,7 @@ test "HttpClient: sync redirect plaintext to TLS" { for (0..5) |_| { defer arr.clearRetainingCapacity(); - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/redirect/secure"); @@ -2978,7 +3144,7 @@ test "HttpClient: sync redirect plaintext to TLS" { } test "HttpClient: sync GET redirect" { - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); const uri = try Uri.parse("http://127.0.0.1:9582/http_client/redirect"); @@ -3024,7 +3190,7 @@ test "HttpClient: async connect error" { }; var reset: Thread.ResetEvent = .{}; - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); var handler = Handler{ @@ -3056,7 +3222,7 @@ test "HttpClient: async connect error" { test "HttpClient: async no body" { defer testing.reset(); - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -3075,7 +3241,7 @@ test "HttpClient: async no body" { test "HttpClient: async with body" { defer testing.reset(); - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -3100,7 +3266,7 @@ test "HttpClient: async with body" { test "HttpClient: async with gzip body" { defer testing.reset(); - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -3123,7 +3289,7 @@ test "HttpClient: async with gzip body" { test "HttpClient: async redirect" { defer testing.reset(); - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -3153,7 +3319,7 @@ test "HttpClient: async redirect" { test "HttpClient: async tls no body" { defer testing.reset(); - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); for (0..5) |_| { var handler = try CaptureHandler.init(); @@ -3178,7 +3344,7 @@ test "HttpClient: async tls no body" { test "HttpClient: async tls with body" { defer testing.reset(); for (0..5) |_| { - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -3202,7 +3368,7 @@ test "HttpClient: async tls with body" { test "HttpClient: async redirect from TLS to Plaintext" { defer testing.reset(); for (0..1) |_| { - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -3228,7 +3394,7 @@ test "HttpClient: async redirect from TLS to Plaintext" { test "HttpClient: async redirect plaintext to TLS" { defer testing.reset(); for (0..5) |_| { - var client = try testClient(); + var client = try testClient(.{}); defer client.deinit(); var handler = try CaptureHandler.init(); @@ -3441,6 +3607,8 @@ fn testReader(state: *State, res: *TestResponse, data: []const u8) !void { return error.NeverDone; } -fn testClient() !Client { - return try Client.init(testing.allocator, .{ .max_concurrent = 1 }); +fn testClient(opts: Client.Opts) !Client { + var o = opts; + o.max_concurrent = 1; + return try Client.init(testing.allocator, o); } diff --git a/src/main.zig b/src/main.zig index ef68d139..41a97373 100644 --- a/src/main.zig +++ b/src/main.zig @@ -23,6 +23,7 @@ const Allocator = std.mem.Allocator; const log = @import("log.zig"); const server = @import("server.zig"); const App = @import("app.zig").App; +const http = @import("http/client.zig"); const Platform = @import("runtime/js.zig").Platform; const Browser = @import("browser/browser.zig").Browser; @@ -83,6 +84,7 @@ fn run(alloc: Allocator) !void { var app = try App.init(alloc, .{ .run_mode = args.mode, .http_proxy = args.httpProxy(), + .proxy_type = args.proxyType(), .tls_verify_host = args.tlsVerifyHost(), }); defer app.deinit(); @@ -155,6 +157,13 @@ const Command = struct { }; } + fn proxyType(self: *const Command) ?http.ProxyType { + return switch (self.mode) { + inline .serve, .fetch => |opts| opts.common.proxy_type, + else => unreachable, + }; + } + fn logLevel(self: *const Command) ?log.Level { return switch (self.mode) { inline .serve, .fetch => |opts| opts.common.log_level, @@ -198,6 +207,7 @@ const Command = struct { const Common = struct { http_proxy: ?std.Uri = null, + proxy_type: ?http.ProxyType = null, tls_verify_host: bool = true, log_level: ?log.Level = null, log_format: ?log.Format = null, @@ -216,6 +226,13 @@ const Command = struct { \\--http_proxy The HTTP proxy to use for all HTTP requests. \\ Defaults to none. \\ + \\--proxy_type The type of proxy: connect, simple. + \\ 'connect' creates a tunnel through the proxy via + \\ and initial CONNECT request. + \\ 'simple' sends the full URL in the request target + \\ and expects the proxy to MITM the request. + \\ Defaults to connect when --http_proxy is set. + \\ \\--log_level The log level: debug, info, warn, error or fatal. \\ Defaults to ++ (if (builtin.mode == .Debug) " info." else "warn.") ++ @@ -456,6 +473,22 @@ fn parseCommonArg( return error.InvalidArgument; }; common.http_proxy = try std.Uri.parse(try allocator.dupe(u8, str)); + if (common.http_proxy.?.host == null) { + log.fatal(.app, "invalid http proxy", .{ .arg = "--http_proxy", .hint = "missing scheme?" }); + return error.InvalidArgument; + } + return true; + } + + if (std.mem.eql(u8, "--proxy_type", opt)) { + const str = args.next() orelse { + log.fatal(.app, "missing argument value", .{ .arg = "--proxy_type" }); + return error.InvalidArgument; + }; + common.proxy_type = std.meta.stringToEnum(http.ProxyType, str) orelse { + log.fatal(.app, "invalid option choice", .{ .arg = "--proxy_type", .value = str }); + return error.InvalidArgument; + }; return true; } @@ -573,58 +606,65 @@ fn serveHTTP(address: std.net.Address) !void { var conn = try listener.accept(); defer conn.stream.close(); var http_server = std.http.Server.init(conn, &read_buffer); - - var request = http_server.receiveHead() catch |err| switch (err) { - error.HttpConnectionClosing => continue :ACCEPT, - else => { - std.debug.print("Test HTTP Server error: {}\n", .{err}); - return err; - }, - }; - - const path = request.head.target; - if (std.mem.eql(u8, path, "/loader")) { - try request.respond("Hello!", .{ - .extra_headers = &.{.{ .name = "Connection", .value = "close" }}, - }); - } else if (std.mem.eql(u8, path, "/http_client/simple")) { - try request.respond("", .{ - .extra_headers = &.{.{ .name = "Connection", .value = "close" }}, - }); - } else if (std.mem.eql(u8, path, "/http_client/redirect")) { - try request.respond("", .{ - .status = .moved_permanently, - .extra_headers = &.{ - .{ .name = "Connection", .value = "close" }, - .{ .name = "LOCATION", .value = "../http_client/echo" }, + REQUEST: while (true) { + var request = http_server.receiveHead() catch |err| switch (err) { + error.HttpConnectionClosing => continue :ACCEPT, + else => { + std.debug.print("Test HTTP Server error: {}\n", .{err}); + return err; }, - }); - } else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) { - try request.respond("", .{ - .status = .moved_permanently, - .extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" } }, - }); - } else if (std.mem.eql(u8, path, "/http_client/gzip")) { - const body = &.{ 0x1f, 0x8b, 0x08, 0x08, 0x01, 0xc6, 0x19, 0x68, 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x00, 0x73, 0x54, 0xc8, 0x4b, 0x2d, 0x57, 0x48, 0x2a, 0xca, 0x2f, 0x2f, 0x4e, 0x2d, 0x52, 0x48, 0x2a, 0xcd, 0xcc, 0x29, 0x51, 0x48, 0xcb, 0x2f, 0x52, 0xc8, 0x4d, 0x4c, 0xce, 0xc8, 0xcc, 0x4b, 0x2d, 0xe6, 0x02, 0x00, 0xe7, 0xc3, 0x4b, 0x27, 0x21, 0x00, 0x00, 0x00 }; - try request.respond(body, .{ - .extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "Content-Encoding", .value = "gzip" } }, - }); - } else if (std.mem.eql(u8, path, "/http_client/echo")) { - var headers: std.ArrayListUnmanaged(std.http.Header) = .{}; + }; - var it = request.iterateHeaders(); - while (it.next()) |hdr| { - try headers.append(aa, .{ - .name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}), - .value = hdr.value, + if (request.head.method == .CONNECT) { + try request.respond("", .{ .status = .ok }); + continue :REQUEST; + } + + const path = request.head.target; + if (std.mem.eql(u8, path, "/loader")) { + try request.respond("Hello!", .{ + .extra_headers = &.{.{ .name = "Connection", .value = "close" }}, + }); + } else if (std.mem.eql(u8, path, "/http_client/simple")) { + try request.respond("", .{ + .extra_headers = &.{.{ .name = "Connection", .value = "close" }}, + }); + } else if (std.mem.eql(u8, path, "/http_client/redirect")) { + try request.respond("", .{ + .status = .moved_permanently, + .extra_headers = &.{ + .{ .name = "Connection", .value = "close" }, + .{ .name = "LOCATION", .value = "../http_client/echo" }, + }, + }); + } else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) { + try request.respond("", .{ + .status = .moved_permanently, + .extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" } }, + }); + } else if (std.mem.eql(u8, path, "/http_client/gzip")) { + const body = &.{ 0x1f, 0x8b, 0x08, 0x08, 0x01, 0xc6, 0x19, 0x68, 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x00, 0x73, 0x54, 0xc8, 0x4b, 0x2d, 0x57, 0x48, 0x2a, 0xca, 0x2f, 0x2f, 0x4e, 0x2d, 0x52, 0x48, 0x2a, 0xcd, 0xcc, 0x29, 0x51, 0x48, 0xcb, 0x2f, 0x52, 0xc8, 0x4d, 0x4c, 0xce, 0xc8, 0xcc, 0x4b, 0x2d, 0xe6, 0x02, 0x00, 0xe7, 0xc3, 0x4b, 0x27, 0x21, 0x00, 0x00, 0x00 }; + try request.respond(body, .{ + .extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "Content-Encoding", .value = "gzip" } }, + }); + } else if (std.mem.eql(u8, path, "/http_client/echo")) { + var headers: std.ArrayListUnmanaged(std.http.Header) = .{}; + + var it = request.iterateHeaders(); + while (it.next()) |hdr| { + try headers.append(aa, .{ + .name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}), + .value = hdr.value, + }); + } + try headers.append(aa, .{ .name = "Connection", .value = "Close" }); + + try request.respond("over 9000!", .{ + .status = .created, + .extra_headers = headers.items, }); } - try headers.append(aa, .{ .name = "Connection", .value = "Close" }); - - try request.respond("over 9000!", .{ - .status = .created, - .extra_headers = headers.items, - }); + continue :ACCEPT; } } } From 1e7ee4e0a1ea57918e7cef2149be0606c08af6bb Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 25 Jun 2025 12:21:44 +0800 Subject: [PATCH 2/2] proxy_type 'simple' renamed to 'forward' --- src/http/client.zig | 4 ++-- src/main.zig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/http/client.zig b/src/http/client.zig index 859e2827..d1d4227e 100644 --- a/src/http/client.zig +++ b/src/http/client.zig @@ -42,7 +42,7 @@ const BUFFER_LEN = 32 * 1024; const MAX_HEADER_LINE_LEN = 4096; pub const ProxyType = enum { - simple, + forward, connect, }; @@ -202,7 +202,7 @@ pub const Client = struct { fn isSimpleProxy(self: *const Client) bool { const proxy_type = self.proxy_type orelse return false; - return proxy_type == .simple; + return proxy_type == .forward; } }; diff --git a/src/main.zig b/src/main.zig index 41a97373..c26d4082 100644 --- a/src/main.zig +++ b/src/main.zig @@ -226,10 +226,10 @@ const Command = struct { \\--http_proxy The HTTP proxy to use for all HTTP requests. \\ Defaults to none. \\ - \\--proxy_type The type of proxy: connect, simple. + \\--proxy_type The type of proxy: connect, forward. \\ 'connect' creates a tunnel through the proxy via \\ and initial CONNECT request. - \\ 'simple' sends the full URL in the request target + \\ 'forward' sends the full URL in the request target \\ and expects the proxy to MITM the request. \\ Defaults to connect when --http_proxy is set. \\