diff --git a/src/app.zig b/src/app.zig index de5e3cb3..6572f538 100644 --- a/src/app.zig +++ b/src/app.zig @@ -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(); diff --git a/src/http/Client.zig b/src/http/Client.zig index e2597fdb..1da6d857 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -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(); } }; diff --git a/src/http/Http.zig b/src/http/Http.zig index b260f78b..bc299480 100644 --- a/src/http/Http.zig +++ b/src/http/Http.zig @@ -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 diff --git a/src/main.zig b/src/main.zig index c3df56e4..20d09a9c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 to send for bearer authentication with the proxy \\ Proxy-Authorization: Bearer \\ - \\--proxy_basic_auth - \\ The user:password to send for basic authentication with the proxy - \\ Proxy-Authorization: Basic - \\ \\--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; }