connect proxy

This commit is contained in:
Karl Seguin
2025-08-04 12:35:16 +08:00
parent 74b40b97ec
commit 7831aabe5a
4 changed files with 124 additions and 146 deletions

View File

@@ -33,9 +33,8 @@ pub const App = struct {
run_mode: RunMode,
platform: ?*const Platform = null,
tls_verify_host: bool = true,
http_proxy: ?std.Uri = null,
proxy_type: ?Http.ProxyType = null,
proxy_auth: ?Http.ProxyAuth = null,
http_proxy: ?[:0]const u8 = null,
proxy_bearer_token: ?[:0]const u8 = null,
};
pub fn init(allocator: Allocator, config: Config) !*App {
@@ -53,7 +52,9 @@ pub const App = struct {
var http = try Http.init(allocator, .{
.max_concurrent_transfers = 3,
.http_proxy = config.http_proxy,
.tls_verify_host = config.tls_verify_host,
.proxy_bearer_token = config.proxy_bearer_token,
});
errdefer http.deinit();

View File

@@ -57,7 +57,7 @@ blocking_active: if (builtin.mode == .Debug) bool else void = if (builtin.mode =
const RequestQueue = std.DoublyLinkedList(Request);
pub fn init(allocator: Allocator, ca_blob: c.curl_blob, opts: Http.Opts) !*Client {
pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, opts: Http.Opts) !*Client {
var transfer_pool = std.heap.MemoryPool(Transfer).init(allocator);
errdefer transfer_pool.deinit();
@@ -70,10 +70,10 @@ pub fn init(allocator: Allocator, ca_blob: c.curl_blob, opts: Http.Opts) !*Clien
const multi = c.curl_multi_init() orelse return error.FailedToInitializeMulti;
errdefer _ = c.curl_multi_cleanup(multi);
var handles = try Handles.init(allocator, client, ca_blob, opts);
var handles = try Handles.init(allocator, client, ca_blob, &opts);
errdefer handles.deinit(allocator);
var blocking = try Handle.init(client, ca_blob, opts);
var blocking = try Handle.init(client, ca_blob, &opts);
errdefer blocking.deinit();
client.* = .{
@@ -104,7 +104,7 @@ pub fn deinit(self: *Client) void {
pub fn abort(self: *Client) void {
while (self.handles.in_use.first) |node| {
var transfer = Transfer.fromEasy(node.data.easy) catch |err| {
var transfer = Transfer.fromEasy(node.data.conn.easy) catch |err| {
log.err(.http, "get private info", .{ .err = err, .source = "abort" });
continue;
};
@@ -173,19 +173,19 @@ pub fn blockingRequest(self: *Client, req: Request) !void {
}
fn makeRequest(self: *Client, handle: *Handle, req: Request) !void {
const easy = handle.easy;
const conn = handle.conn;
const easy = conn.easy;
const header_list = blk: {
errdefer self.handles.release(handle);
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_URL, req.url.ptr));
try conn.setMethod(req.method);
try conn.setURL(req.url);
try Http.setMethod(easy, req.method);
if (req.body) |b| {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, b.ptr));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(b.len))));
try conn.setBody(b);
}
var header_list = c.curl_slist_append(null, "User-Agent: Lightpanda/1.0");
var header_list = conn.commonHeaders();
errdefer c.curl_slist_free_all(header_list);
if (req.content_type) |ct| {
@@ -269,7 +269,7 @@ fn endTransfer(self: *Client, transfer: *Transfer) void {
transfer.deinit();
self.transfer_pool.destroy(transfer);
errorMCheck(c.curl_multi_remove_handle(self.multi, handle.easy)) catch |err| {
errorMCheck(c.curl_multi_remove_handle(self.multi, handle.conn.easy)) catch |err| {
log.fatal(.http, "Failed to abort", .{ .err = err });
};
@@ -284,7 +284,8 @@ const Handles = struct {
const HandleList = std.DoublyLinkedList(*Handle);
fn init(allocator: Allocator, client: *Client, ca_blob: c.curl_blob, opts: Http.Opts) !Handles {
// pointer to opts is not stable, don't hold a reference to it!
fn init(allocator: Allocator, client: *Client, ca_blob: ?c.curl_blob, opts: *const Http.Opts) !Handles {
const count = opts.max_concurrent_transfers;
std.debug.assert(count > 0);
@@ -340,22 +341,16 @@ const Handles = struct {
// wraps a c.CURL (an easy handle)
const Handle = struct {
easy: *c.CURL,
client: *Client,
conn: Http.Connection,
node: ?Handles.HandleList.Node,
fn init(client: *Client, ca_blob: c.curl_blob, opts: Http.Opts) !Handle {
const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy;
errdefer _ = c.curl_easy_cleanup(easy);
// pointer to opts is not stable, don't hold a reference to it!
fn init(client: *Client, ca_blob: ?c.curl_blob, opts: *const Http.Opts) !Handle {
const conn = try Http.Connection.init(ca_blob, opts);
errdefer conn.deinit();
// timeouts
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_TIMEOUT_MS, @as(c_long, @intCast(opts.timeout_ms))));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CONNECTTIMEOUT_MS, @as(c_long, @intCast(opts.connect_timeout_ms))));
// redirect behavior
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_MAXREDIRS, @as(c_long, @intCast(opts.max_redirects))));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_FOLLOWLOCATION, @as(c_long, 2)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_REDIR_PROTOCOLS_STR, "HTTP,HTTPS")); // remove FTP and FTPS from the default
const easy = conn.easy;
// callbacks
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HEADERDATA, easy));
@@ -363,30 +358,15 @@ const Handle = struct {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_WRITEDATA, easy));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_WRITEFUNCTION, Transfer.dataCallback));
// tls
if (opts.tls_verify_host) {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CAINFO_BLOB, ca_blob));
} else {
// Verify peer checks that the cert is signed by a CA, verify host makes sure the
// cert contains the server name.
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)));
}
// debug
if (comptime Http.ENABLE_DEBUG) {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_VERBOSE, @as(c_long, 1)));
}
return .{
.easy = easy,
.conn = conn,
.node = null,
.client = client,
};
}
fn deinit(self: *const Handle) void {
_ = c.curl_easy_cleanup(self.easy);
self.conn.deinit();
}
};

View File

@@ -38,8 +38,8 @@ const Http = @This();
opts: Opts,
client: *Client,
ca_blob: c.curl_blob,
cert_arena: ArenaAllocator,
ca_blob: ?c.curl_blob,
arena: ArenaAllocator,
pub fn init(allocator: Allocator, opts: Opts) !Http {
try errorCheck(c.curl_global_init(c.CURL_GLOBAL_SSL));
@@ -49,37 +49,54 @@ pub fn init(allocator: Allocator, opts: Opts) !Http {
std.debug.print("curl version: {s}\n\n", .{c.curl_version()});
}
var cert_arena = ArenaAllocator.init(allocator);
errdefer cert_arena.deinit();
const ca_blob = try loadCerts(allocator, cert_arena.allocator());
var arena = ArenaAllocator.init(allocator);
errdefer arena.deinit();
var client = try Client.init(allocator, ca_blob, opts);
var adjusted_opts = opts;
if (opts.proxy_bearer_token) |bt| {
adjusted_opts.proxy_bearer_token = try std.fmt.allocPrintZ(
arena.allocator(),
"Proxy-Authorization: Bearer {s}",
.{bt},
);
}
var ca_blob: ?c.curl_blob = null;
if (opts.tls_verify_host) {
ca_blob = try loadCerts(allocator, arena.allocator());
}
var client = try Client.init(allocator, ca_blob, adjusted_opts);
errdefer client.deinit();
return .{
.opts = opts,
.arena = arena,
.client = client,
.ca_blob = ca_blob,
.cert_arena = cert_arena,
.opts = adjusted_opts,
};
}
pub fn deinit(self: *Http) void {
self.client.deinit();
c.curl_global_cleanup();
self.cert_arena.deinit();
self.arena.deinit();
}
pub fn newConnection(self: *Http) !Connection {
return Connection.init(self.ca_blob, self.opts);
return Connection.init(self.ca_blob, &self.opts);
}
pub const Connection = struct {
easy: *c.CURL,
opts: Connection.Opts,
// Is called by Handles when already partially initialized. Done like this
// so that we have a stable pointer to error_buffer.
pub fn init(ca_blob: c.curl_blob, opts: Opts) !Connection {
const Opts = struct {
proxy_bearer_token: ?[:0]const u8,
};
// pointer to opts is not stable, don't hold a reference to it!
pub fn init(ca_blob_: ?c.curl_blob, opts: *const Http.Opts) !Connection {
const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy;
errdefer _ = c.curl_easy_cleanup(easy);
@@ -92,10 +109,36 @@ pub const Connection = struct {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_FOLLOWLOCATION, @as(c_long, 2)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_REDIR_PROTOCOLS_STR, "HTTP,HTTPS")); // remove FTP and FTPS from the default
// proxy
if (opts.http_proxy) |proxy| {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY, proxy.ptr));
}
// tls
// try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)));
// try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)));
if (ca_blob_) |ca_blob| {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CAINFO_BLOB, ca_blob));
if (opts.http_proxy != null) {
// Note, this can be difference for the proxy and for the main
// request. Might be something worth exposting as command
// line arguments at some point.
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_CAINFO_BLOB , ca_blob));
}
} else {
std.debug.assert(opts.tls_verify_host == false);
// Verify peer checks that the cert is signed by a CA, verify host makes sure the
// cert contains the server name.
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)));
if (opts.http_proxy != null) {
// Note, this can be difference for the proxy and for the main
// request. Might be something worth exposting as command
// line arguments at some point.
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYHOST, @as(c_long, 0)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXY_SSL_VERIFYPEER , @as(c_long, 0)));
}
}
// debug
if (comptime Http.ENABLE_DEBUG) {
@@ -104,6 +147,9 @@ pub const Connection = struct {
return .{
.easy = easy,
.opts = .{
.proxy_bearer_token = opts.proxy_bearer_token,
},
};
}
@@ -116,28 +162,7 @@ pub const Connection = struct {
}
pub fn setMethod(self: *const Connection, method: Method) !void {
try Http.setMethod(self.easy, method);
}
pub fn setBody(self: *const Connection, body: []const u8) !void {
const easy = self.easy;
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(body.len))));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, body.ptr));
}
pub fn request(self: *const Connection) !u16 {
try errorCheck(c.curl_easy_perform(self.easy));
var http_code: c_long = undefined;
try errorCheck(c.curl_easy_getinfo(self.easy, c.CURLINFO_RESPONSE_CODE, &http_code));
if (http_code < 0 or http_code > std.math.maxInt(u16)) {
return 0;
}
return @intCast(http_code);
}
};
// used by Connection and Handle
pub fn setMethod(easy: *c.CURL, method: Method) !void {
switch (method) {
.GET => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPGET, @as(c_long, 1))),
.POST => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPPOST, @as(c_long, 1))),
@@ -146,7 +171,39 @@ pub fn setMethod(easy: *c.CURL, method: Method) !void {
.HEAD => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "head")),
.OPTIONS => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "options")),
}
}
}
pub fn setBody(self: *const Connection, body: []const u8) !void {
const easy = self.easy;
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(body.len))));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, body.ptr));
}
pub fn commonHeaders(self: *const Connection) *c.curl_slist {
var header_list = c.curl_slist_append(null, "User-Agent: Lightpanda/1.0");
if (self.opts.proxy_bearer_token) |hdr| {
header_list = c.curl_slist_append(header_list, hdr);
}
return header_list;
}
pub fn request(self: *const Connection) !u16 {
const easy = self.easy;
const header_list = self.commonHeaders();
defer c.curl_slist_free_all(header_list);
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list));
try errorCheck(c.curl_easy_perform(easy));
var http_code: c_long = undefined;
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_RESPONSE_CODE, &http_code));
if (http_code < 0 or http_code > std.math.maxInt(u16)) {
return 0;
}
return @intCast(http_code);
}
};
pub fn errorCheck(code: c.CURLcode) errors.Error!void {
if (code == c.CURLE_OK) {
@@ -173,6 +230,8 @@ pub const Opts = struct {
tls_verify_host: bool = true,
connect_timeout_ms: u31 = 5000,
max_concurrent_transfers: u8 = 5,
http_proxy: ?[:0]const u8 = null,
proxy_bearer_token: ?[:0]const u8 = null,
};
pub const Method = enum {
@@ -184,16 +243,6 @@ pub const Method = enum {
OPTIONS,
};
pub const ProxyType = enum {
forward,
connect,
};
pub const ProxyAuth = union(enum) {
basic: struct { user_pass: []const u8 },
bearer: struct { token: []const u8 },
};
// TODO: on BSD / Linux, we could just read the PEM file directly.
// This whole rescan + decode is really just needed for MacOS. On Linux
// bundle.rescan does find the .pem file(s) which could be in a few different

View File

@@ -85,8 +85,7 @@ fn run(alloc: Allocator) !void {
.run_mode = args.mode,
.platform = &platform,
.http_proxy = args.httpProxy(),
.proxy_type = args.proxyType(),
.proxy_auth = args.proxyAuth(),
.proxy_bearer_token = args.proxyBearerToken(),
.tls_verify_host = args.tlsVerifyHost(),
});
defer app.deinit();
@@ -156,23 +155,16 @@ const Command = struct {
};
}
fn httpProxy(self: *const Command) ?std.Uri {
fn httpProxy(self: *const Command) ?[:0]const u8 {
return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.http_proxy,
else => unreachable,
};
}
fn proxyType(self: *const Command) ?Http.ProxyType {
fn proxyBearerToken(self: *const Command) ?[:0]const u8 {
return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.proxy_type,
else => unreachable,
};
}
fn proxyAuth(self: *const Command) ?Http.ProxyAuth {
return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.proxy_auth,
inline .serve, .fetch => |opts| opts.common.proxy_bearer_token,
else => unreachable,
};
}
@@ -221,9 +213,8 @@ const Command = struct {
};
const Common = struct {
http_proxy: ?std.Uri = null,
proxy_type: ?Http.ProxyType = null,
proxy_auth: ?Http.ProxyAuth = null,
http_proxy: ?[:0]const u8 = null,
proxy_bearer_token: ?[:0]const u8 = null,
tls_verify_host: bool = true,
log_level: ?log.Level = null,
log_format: ?log.Format = null,
@@ -242,21 +233,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, forward.
\\ 'connect' creates a tunnel through the proxy via
\\ and initial CONNECT request.
\\ '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.
\\
\\--proxy_bearer_token
\\ The token to send for bearer authentication with the proxy
\\ The <token> to send for bearer authentication with the proxy
\\ Proxy-Authorization: Bearer <token>
\\
\\--proxy_basic_auth
\\ The user:password to send for basic authentication with the proxy
\\ Proxy-Authorization: Basic <base64(user:password)>
\\
\\--log_level The log level: debug, info, warn, error or fatal.
\\ Defaults to
++ (if (builtin.mode == .Debug) " info." else "warn.") ++
@@ -521,48 +501,16 @@ fn parseCommonArg(
log.fatal(.app, "missing argument value", .{ .arg = "--http_proxy" });
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;
};
common.http_proxy = try allocator.dupeZ(u8, str);
return true;
}
if (std.mem.eql(u8, "--proxy_bearer_token", opt)) {
if (common.proxy_auth != null) {
log.fatal(.app, "proxy auth already set", .{ .arg = "--proxy_bearer_token" });
return error.InvalidArgument;
}
const str = args.next() orelse {
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_bearer_token" });
return error.InvalidArgument;
};
common.proxy_auth = .{ .bearer = .{ .token = str } };
return true;
}
if (std.mem.eql(u8, "--proxy_basic_auth", opt)) {
if (common.proxy_auth != null) {
log.fatal(.app, "proxy auth already set", .{ .arg = "--proxy_basic_auth" });
return error.InvalidArgument;
}
const str = args.next() orelse {
log.fatal(.app, "missing argument value", .{ .arg = "--proxy_basic_auth" });
return error.InvalidArgument;
};
common.proxy_auth = .{ .basic = .{ .user_pass = str } };
common.proxy_bearer_token = try allocator.dupeZ(u8, str);
return true;
}