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,
exec_name: []const u8,
http_headers: HttpHeaders,
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 {
return switch (self.mode) {
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 {
return switch (self.mode) {
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 {
return switch (self.mode) {
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) {
help: bool, // false when being printed because of an error
fetch: Fetch,
@@ -164,6 +171,49 @@ pub const Common = struct {
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 {
// MAX_HELP_LEN|
const common_options =
@@ -292,16 +342,12 @@ pub fn parseArgs(allocator: Allocator) !Config {
var args = try std.process.argsWithAllocator(allocator);
defer args.deinit();
const exec_name = std.fs.path.basename(args.next().?);
var config = Config{
.mode = .{ .help = false },
.exec_name = try allocator.dupe(u8, exec_name),
};
const exec_name = try allocator.dupe(u8, std.fs.path.basename(args.next().?));
const mode_string = args.next() orelse "";
const mode = std.meta.stringToEnum(RunMode, mode_string) orelse blk: {
const inferred_mode = inferMode(mode_string) orelse return config;
const run_mode = std.meta.stringToEnum(RunMode, mode_string) orelse blk: {
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
// we can create a new one. Not great, but this fallback is temporary
// as we transition to this command mode approach.
@@ -314,13 +360,15 @@ pub fn parseArgs(allocator: Allocator) !Config {
break :blk inferred_mode;
};
config.mode = switch (mode) {
const mode: Mode = switch (run_mode) {
.help => .{ .help = true },
.serve => .{ .serve = parseServeArgs(allocator, &args) catch return config },
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch return config },
.serve => .{ .serve = parseServeArgs(allocator, &args) catch
return init(allocator, exec_name, .{ .help = false }) },
.fetch => .{ .fetch = parseFetchArgs(allocator, &args) catch
return init(allocator, exec_name, .{ .help = false }) },
.version => .{ .version = {} },
};
return config;
return init(allocator, exec_name, mode);
}
fn inferMode(opt: []const u8) ?RunMode {

View File

@@ -29,7 +29,7 @@ _plugins: PluginArray = .{},
pub const init: Navigator = .{};
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 {

View File

@@ -97,8 +97,7 @@ http_proxy: ?[:0]const u8 = null,
// CDP.
use_proxy: bool,
// The complete user-agent header line
user_agent: [:0]const u8,
config: *const Config,
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())));
const user_agent = try config.userAgent(allocator);
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);
var handles = try Handles.init(allocator, client, ca_blob, config);
errdefer handles.deinit(allocator);
const http_proxy = config.httpProxy();
@@ -154,7 +147,7 @@ pub fn init(allocator: Allocator, ca_blob: ?c.curl_blob, config: *const Config)
.allocator = allocator,
.http_proxy = http_proxy,
.use_proxy = http_proxy != null,
.user_agent = user_agent,
.config = config,
.transfer_pool = transfer_pool,
};
@@ -172,7 +165,7 @@ pub fn deinit(self: *Client) void {
}
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 {
@@ -660,8 +653,6 @@ const Handles = struct {
client: *Client,
ca_blob: ?c.curl_blob,
config: *const Config,
user_agent: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
) !Handles {
const count: usize = config.httpMaxConcurrent();
if (count == 0) return error.InvalidMaxConcurrent;
@@ -671,7 +662,7 @@ const Handles = struct {
var available: HandleList = .{};
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);
}
@@ -722,10 +713,8 @@ pub const Handle = struct {
client: *Client,
ca_blob: ?c.curl_blob,
config: *const Config,
user_agent: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
) !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();
const easy = conn.easy;

View File

@@ -45,8 +45,6 @@ config: *const Config,
client: *Client,
ca_blob: ?c.curl_blob,
arena: ArenaAllocator,
user_agent: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
pub fn init(allocator: Allocator, config: *const Config) !Http {
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);
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;
if (config.tlsVerifyHost()) {
ca_blob = try loadCerts(allocator, arena.allocator());
@@ -79,8 +70,6 @@ pub fn init(allocator: Allocator, config: *const Config) !Http {
.client = client,
.ca_blob = ca_blob,
.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 {
return Connection.init(self.ca_blob, self.config, self.user_agent, self.proxy_bearer_header);
}
pub fn newHeaders(self: *const Http) Headers {
return Headers.init(self.user_agent);
return Connection.init(self.ca_blob, self.config);
}
pub const Connection = struct {
easy: *c.CURL,
user_agent: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
http_headers: *const Config.HttpHeaders,
pub fn init(
ca_blob_: ?c.curl_blob,
config: *const Config,
user_agent: [:0]const u8,
proxy_bearer_header: ?[:0]const u8,
) !Connection {
const easy = c.curl_easy_init() orelse return error.FailedToInitializeEasy;
errdefer _ = c.curl_easy_cleanup(easy);
@@ -188,8 +170,7 @@ pub const Connection = struct {
return .{
.easy = easy,
.user_agent = user_agent,
.proxy_bearer_header = proxy_bearer_header,
.http_headers = &config.http_headers,
};
}
@@ -243,7 +224,7 @@ pub const Connection = struct {
// These are headers that may not be send to the users for inteception.
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);
}
}
@@ -251,7 +232,7 @@ pub const Connection = struct {
pub fn request(self: *const Connection) !u16 {
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();
try self.secretHeaders(&header_list);
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 {
const args = try Config.parseArgs(main_arena);
defer args.deinit(main_arena);
switch (args.mode) {
.help => {

View File

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

View File

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

View File

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