Eliminates duplication in the creation of HTTP headers

This commit is contained in:
Nikolay Govorov
2026-02-04 09:08:57 +00:00
parent f71aa1cad2
commit a72782f91e
8 changed files with 107 additions and 92 deletions

View File

@@ -32,9 +32,24 @@ pub const RunMode = enum {
mode: Mode, mode: Mode,
exec_name: []const u8, exec_name: []const u8,
http_headers: HttpHeaders,
const Config = @This(); const Config = @This();
pub fn init(allocator: Allocator, exec_name: []const u8, mode: Mode) !Config {
var config = Config{
.mode = mode,
.exec_name = exec_name,
.http_headers = undefined,
};
config.http_headers = try HttpHeaders.init(allocator, &config);
return config;
}
pub fn deinit(self: *const Config, allocator: Allocator) void {
self.http_headers.deinit(allocator);
}
pub fn tlsVerifyHost(self: *const Config) bool { pub fn tlsVerifyHost(self: *const Config) bool {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.tls_verify_host, inline .serve, .fetch => |opts| opts.common.tls_verify_host,
@@ -52,7 +67,7 @@ pub fn httpProxy(self: *const Config) ?[:0]const u8 {
pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 { pub fn proxyBearerToken(self: *const Config) ?[:0]const u8 {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.proxy_bearer_token, inline .serve, .fetch => |opts| opts.common.proxy_bearer_token,
else => unreachable, .help, .version => null,
}; };
} }
@@ -112,18 +127,10 @@ pub fn logFilterScopes(self: *const Config) ?[]const log.Scope {
pub fn userAgentSuffix(self: *const Config) ?[]const u8 { pub fn userAgentSuffix(self: *const Config) ?[]const u8 {
return switch (self.mode) { return switch (self.mode) {
inline .serve, .fetch => |opts| opts.common.user_agent_suffix, inline .serve, .fetch => |opts| opts.common.user_agent_suffix,
else => unreachable, .help, .version => null,
}; };
} }
pub fn userAgent(self: *const Config, allocator: Allocator) ![:0]const u8 {
const base = "User-Agent: Lightpanda/1.0";
if (self.userAgentSuffix()) |suffix| {
return try std.fmt.allocPrintSentinel(allocator, "{s} {s}", .{ base, suffix }, 0);
}
return base;
}
pub const Mode = union(RunMode) { pub const Mode = union(RunMode) {
help: bool, // false when being printed because of an error help: bool, // false when being printed because of an error
fetch: Fetch, fetch: Fetch,
@@ -164,6 +171,49 @@ pub const Common = struct {
user_agent_suffix: ?[]const u8 = null, user_agent_suffix: ?[]const u8 = null,
}; };
/// Pre-formatted HTTP headers for reuse across Http and Client.
/// Must be initialized with an allocator that outlives all HTTP connections.
pub const HttpHeaders = struct {
const user_agent_base: [:0]const u8 = "Lightpanda/1.0";
user_agent: [:0]const u8, // User agent value (e.g. "Lightpanda/1.0")
user_agent_header: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
pub fn init(allocator: Allocator, config: *const Config) !HttpHeaders {
const user_agent: [:0]const u8 = if (config.userAgentSuffix()) |suffix|
try std.fmt.allocPrintSentinel(allocator, "{s} {s}", .{ user_agent_base, suffix }, 0)
else
user_agent_base;
errdefer if (config.userAgentSuffix() != null) allocator.free(user_agent);
const user_agent_header = try std.fmt.allocPrintSentinel(allocator, "User-Agent: {s}", .{user_agent}, 0);
errdefer allocator.free(user_agent_header);
const proxy_bearer_header: ?[:0]const u8 = if (config.proxyBearerToken()) |token|
try std.fmt.allocPrintSentinel(allocator, "Proxy-Authorization: Bearer {s}", .{token}, 0)
else
null;
return .{
.user_agent = user_agent,
.user_agent_header = user_agent_header,
.proxy_bearer_header = proxy_bearer_header,
};
}
pub fn deinit(self: *const HttpHeaders, allocator: Allocator) void {
if (self.proxy_bearer_header) |hdr| {
allocator.free(hdr);
}
allocator.free(self.user_agent_header);
if (self.user_agent.ptr != user_agent_base.ptr) {
allocator.free(self.user_agent);
}
}
};
pub fn printUsageAndExit(self: *const Config, success: bool) void { pub fn printUsageAndExit(self: *const Config, success: bool) void {
// MAX_HELP_LEN| // MAX_HELP_LEN|
const common_options = const common_options =
@@ -292,16 +342,12 @@ pub fn parseArgs(allocator: Allocator) !Config {
var args = try std.process.argsWithAllocator(allocator); var args = try std.process.argsWithAllocator(allocator);
defer args.deinit(); defer args.deinit();
const exec_name = std.fs.path.basename(args.next().?); const exec_name = try allocator.dupe(u8, std.fs.path.basename(args.next().?));
var config = Config{
.mode = .{ .help = false },
.exec_name = try allocator.dupe(u8, exec_name),
};
const mode_string = args.next() orelse ""; const mode_string = args.next() orelse "";
const mode = std.meta.stringToEnum(RunMode, mode_string) orelse blk: { const run_mode = std.meta.stringToEnum(RunMode, mode_string) orelse blk: {
const inferred_mode = inferMode(mode_string) orelse return config; const inferred_mode = inferMode(mode_string) orelse
return init(allocator, exec_name, .{ .help = false });
// "command" wasn't a command but an option. We can't reset args, but // "command" wasn't a command but an option. We can't reset args, but
// we can create a new one. Not great, but this fallback is temporary // we can create a new one. Not great, but this fallback is temporary
// as we transition to this command mode approach. // as we transition to this command mode approach.
@@ -314,13 +360,15 @@ pub fn parseArgs(allocator: Allocator) !Config {
break :blk inferred_mode; break :blk inferred_mode;
}; };
config.mode = switch (mode) { const mode: Mode = switch (run_mode) {
.help => .{ .help = true }, .help => .{ .help = true },
.serve => .{ .serve = parseServeArgs(allocator, &args) catch return config }, .serve => .{ .serve = parseServeArgs(allocator, &args) catch
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch return config }, return init(allocator, exec_name, .{ .help = false }) },
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch
return init(allocator, exec_name, .{ .help = false }) },
.version => .{ .version = {} }, .version => .{ .version = {} },
}; };
return config; return init(allocator, exec_name, mode);
} }
fn inferMode(opt: []const u8) ?RunMode { fn inferMode(opt: []const u8) ?RunMode {

View File

@@ -29,7 +29,7 @@ _plugins: PluginArray = .{},
pub const init: Navigator = .{}; pub const init: Navigator = .{};
pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 { pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
return page._session.browser.app.http.user_agent; return page._session.browser.app.config.http_headers.user_agent;
} }
pub fn getAppName(_: *const Navigator) []const u8 { pub fn getAppName(_: *const Navigator) []const u8 {

View File

@@ -97,8 +97,7 @@ http_proxy: ?[:0]const u8 = null,
// CDP. // CDP.
use_proxy: bool, use_proxy: bool,
// The complete user-agent header line config: *const Config,
user_agent: [:0]const u8,
cdp_client: ?CDPClient = null, cdp_client: ?CDPClient = null,
@@ -134,13 +133,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, config: *const Config)
try errorMCheck(c.curl_multi_setopt(multi, c.CURLMOPT_MAX_HOST_CONNECTIONS, @as(c_long, config.httpMaxHostOpen()))); try errorMCheck(c.curl_multi_setopt(multi, c.CURLMOPT_MAX_HOST_CONNECTIONS, @as(c_long, config.httpMaxHostOpen())));
const user_agent = try config.userAgent(allocator); var handles = try Handles.init(allocator, client, ca_blob, config);
var proxy_bearer_header: ?[:0]const u8 = null;
if (config.proxyBearerToken()) |bt| {
proxy_bearer_header = try std.fmt.allocPrintSentinel(allocator, "Proxy-Authorization: Bearer {s}", .{bt}, 0);
}
var handles = try Handles.init(allocator, client, ca_blob, config, user_agent, proxy_bearer_header);
errdefer handles.deinit(allocator); errdefer handles.deinit(allocator);
const http_proxy = config.httpProxy(); const http_proxy = config.httpProxy();
@@ -154,7 +147,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, config: *const Config)
.allocator = allocator, .allocator = allocator,
.http_proxy = http_proxy, .http_proxy = http_proxy,
.use_proxy = http_proxy != null, .use_proxy = http_proxy != null,
.user_agent = user_agent, .config = config,
.transfer_pool = transfer_pool, .transfer_pool = transfer_pool,
}; };
@@ -172,7 +165,7 @@ pub fn deinit(self: *Client) void {
} }
pub fn newHeaders(self: *const Client) !Http.Headers { pub fn newHeaders(self: *const Client) !Http.Headers {
return Http.Headers.init(self.user_agent); return Http.Headers.init(self.config.http_headers.user_agent_header);
} }
pub fn abort(self: *Client) void { pub fn abort(self: *Client) void {
@@ -660,8 +653,6 @@ const Handles = struct {
client: *Client, client: *Client,
ca_blob: ?c.curl_blob, ca_blob: ?c.curl_blob,
config: *const Config, config: *const Config,
user_agent: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
) !Handles { ) !Handles {
const count: usize = config.httpMaxConcurrent(); const count: usize = config.httpMaxConcurrent();
if (count == 0) return error.InvalidMaxConcurrent; if (count == 0) return error.InvalidMaxConcurrent;
@@ -671,7 +662,7 @@ const Handles = struct {
var available: HandleList = .{}; var available: HandleList = .{};
for (0..count) |i| { for (0..count) |i| {
handles[i] = try Handle.init(client, ca_blob, config, user_agent, proxy_bearer_header); handles[i] = try Handle.init(client, ca_blob, config);
available.append(&handles[i].node); available.append(&handles[i].node);
} }
@@ -722,10 +713,8 @@ pub const Handle = struct {
client: *Client, client: *Client,
ca_blob: ?c.curl_blob, ca_blob: ?c.curl_blob,
config: *const Config, config: *const Config,
user_agent: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
) !Handle { ) !Handle {
const conn = try Http.Connection.init(ca_blob, config, user_agent, proxy_bearer_header); const conn = try Http.Connection.init(ca_blob, config);
errdefer conn.deinit(); errdefer conn.deinit();
const easy = conn.easy; const easy = conn.easy;

View File

@@ -45,8 +45,6 @@ config: *const Config,
client: *Client, client: *Client,
ca_blob: ?c.curl_blob, ca_blob: ?c.curl_blob,
arena: ArenaAllocator, arena: ArenaAllocator,
user_agent: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
pub fn init(allocator: Allocator, config: *const Config) !Http { pub fn init(allocator: Allocator, config: *const Config) !Http {
try errorCheck(c.curl_global_init(c.CURL_GLOBAL_SSL)); try errorCheck(c.curl_global_init(c.CURL_GLOBAL_SSL));
@@ -59,13 +57,6 @@ pub fn init(allocator: Allocator, config: *const Config) !Http {
var arena = ArenaAllocator.init(allocator); var arena = ArenaAllocator.init(allocator);
errdefer arena.deinit(); errdefer arena.deinit();
const user_agent = try config.userAgent(arena.allocator());
var proxy_bearer_header: ?[:0]const u8 = null;
if (config.proxyBearerToken()) |bt| {
proxy_bearer_header = try std.fmt.allocPrintSentinel(arena.allocator(), "Proxy-Authorization: Bearer {s}", .{bt}, 0);
}
var ca_blob: ?c.curl_blob = null; var ca_blob: ?c.curl_blob = null;
if (config.tlsVerifyHost()) { if (config.tlsVerifyHost()) {
ca_blob = try loadCerts(allocator, arena.allocator()); ca_blob = try loadCerts(allocator, arena.allocator());
@@ -79,8 +70,6 @@ pub fn init(allocator: Allocator, config: *const Config) !Http {
.client = client, .client = client,
.ca_blob = ca_blob, .ca_blob = ca_blob,
.config = config, .config = config,
.user_agent = user_agent,
.proxy_bearer_header = proxy_bearer_header,
}; };
} }
@@ -107,23 +96,16 @@ pub fn removeCDPClient(self: *Http) void {
} }
pub fn newConnection(self: *Http) !Connection { pub fn newConnection(self: *Http) !Connection {
return Connection.init(self.ca_blob, self.config, self.user_agent, self.proxy_bearer_header); return Connection.init(self.ca_blob, self.config);
}
pub fn newHeaders(self: *const Http) Headers {
return Headers.init(self.user_agent);
} }
pub const Connection = struct { pub const Connection = struct {
easy: *c.CURL, easy: *c.CURL,
user_agent: [:0]const u8, http_headers: *const Config.HttpHeaders,
proxy_bearer_header: ?[:0]const u8,
pub fn init( pub fn init(
ca_blob_: ?c.curl_blob, ca_blob_: ?c.curl_blob,
config: *const Config, config: *const Config,
user_agent: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
) !Connection { ) !Connection {
const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy; const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy;
errdefer _ = c.curl_easy_cleanup(easy); errdefer _ = c.curl_easy_cleanup(easy);
@@ -188,8 +170,7 @@ pub const Connection = struct {
return .{ return .{
.easy = easy, .easy = easy,
.user_agent = user_agent, .http_headers = &config.http_headers,
.proxy_bearer_header = proxy_bearer_header,
}; };
} }
@@ -243,7 +224,7 @@ pub const Connection = struct {
// These are headers that may not be send to the users for inteception. // These are headers that may not be send to the users for inteception.
pub fn secretHeaders(self: *const Connection, headers: *Headers) !void { pub fn secretHeaders(self: *const Connection, headers: *Headers) !void {
if (self.proxy_bearer_header) |hdr| { if (self.http_headers.proxy_bearer_header) |hdr| {
try headers.add(hdr); try headers.add(hdr);
} }
} }
@@ -251,7 +232,7 @@ pub const Connection = struct {
pub fn request(self: *const Connection) !u16 { pub fn request(self: *const Connection) !u16 {
const easy = self.easy; const easy = self.easy;
var header_list = try Headers.init(self.user_agent); var header_list = try Headers.init(self.http_headers.user_agent_header);
defer header_list.deinit(); defer header_list.deinit();
try self.secretHeaders(&header_list); try self.secretHeaders(&header_list);
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list.headers)); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list.headers));

View File

@@ -54,6 +54,7 @@ pub fn main() !void {
fn run(allocator: Allocator, main_arena: Allocator, sighandler: *SigHandler) !void { fn run(allocator: Allocator, main_arena: Allocator, sighandler: *SigHandler) !void {
const args = try Config.parseArgs(main_arena); const args = try Config.parseArgs(main_arena);
defer args.deinit(main_arena);
switch (args.mode) { switch (args.mode) {
.help => { .help => {

View File

@@ -32,15 +32,12 @@ pub fn main() !void {
wg.wait(); wg.wait();
} }
lp.log.opts.level = .warn; lp.log.opts.level = .warn;
const config = lp.Config{ const config = try lp.Config.init(allocator, "legacy-test", .{ .serve = .{
.mode = .{ .serve = .{ .common = .{
.common = .{ .tls_verify_host = false,
.tls_verify_host = false, .user_agent_suffix = "internal-tester",
.user_agent_suffix = "internal-tester", },
}, } });
} },
.exec_name = "legacy-test",
};
var app = try lp.App.init(allocator, &config); var app = try lp.App.init(allocator, &config);
defer app.deinit(); defer app.deinit();

View File

@@ -58,15 +58,12 @@ pub fn main() !void {
defer writer.deinit(); defer writer.deinit();
lp.log.opts.level = .warn; lp.log.opts.level = .warn;
const config = lp.Config{ const config = try lp.Config.init(allocator, "lightpanda-wpt", .{ .serve = .{
.mode = .{ .serve = .{ .common = .{
.common = .{ .tls_verify_host = false,
.tls_verify_host = false, .user_agent_suffix = "internal-tester",
.user_agent_suffix = "internal-tester", },
}, } });
} },
.exec_name = "lightpanda-wpt",
};
var app = try lp.App.init(allocator, &config); var app = try lp.App.init(allocator, &config);
defer app.deinit(); defer app.deinit();

View File

@@ -452,21 +452,22 @@ const Server = @import("Server.zig");
var test_cdp_server: ?Server = null; var test_cdp_server: ?Server = null;
var test_http_server: ?TestHTTPServer = null; var test_http_server: ?TestHTTPServer = null;
const test_config = Config{ var test_config: Config = undefined;
.mode = .{ .serve = .{
.common = .{
.tls_verify_host = false,
.user_agent_suffix = "internal-tester",
},
} },
.exec_name = "test",
};
test "tests:beforeAll" { test "tests:beforeAll" {
log.opts.level = .warn; log.opts.level = .warn;
log.opts.format = .pretty; log.opts.format = .pretty;
test_app = try App.init(@import("root").tracking_allocator, &test_config); const test_allocator = @import("root").tracking_allocator;
test_config = try Config.init(test_allocator, "test", .{ .serve = .{
.common = .{
.tls_verify_host = false,
.user_agent_suffix = "internal-tester",
},
} });
test_app = try App.init(test_allocator, &test_config);
errdefer test_app.deinit(); errdefer test_app.deinit();
test_browser = try Browser.init(test_app, .{}); test_browser = try Browser.init(test_app, .{});
@@ -510,6 +511,7 @@ test "tests:afterAll" {
test_notification.deinit(); test_notification.deinit();
test_browser.deinit(); test_browser.deinit();
test_app.deinit(); test_app.deinit();
test_config.deinit(@import("root").tracking_allocator);
} }
fn serveCDP(wg: *std.Thread.WaitGroup) !void { fn serveCDP(wg: *std.Thread.WaitGroup) !void {