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

@@ -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,41 +49,58 @@ 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);
// timeouts
// 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))));
@@ -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)));
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CAINFO_BLOB, ca_blob));
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,7 +162,15 @@ pub const Connection = struct {
}
pub fn setMethod(self: *const Connection, method: Method) !void {
try Http.setMethod(self.easy, method);
const easy = self.easy;
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))),
.PUT => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "put")),
.DELETE => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "delete")),
.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 {
@@ -125,10 +179,24 @@ pub const Connection = struct {
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 {
try errorCheck(c.curl_easy_perform(self.easy));
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(self.easy, c.CURLINFO_RESPONSE_CODE, &http_code));
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;
}
@@ -136,17 +204,6 @@ pub const Connection = struct {
}
};
// 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))),
.PUT => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "put")),
.DELETE => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "delete")),
.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 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