Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b7f61e5
`Network`: support custom CA install
nikneym Jul 4, 2026
1157020
`Config`: ensure passed `--ca-path` paths contain hashed certs
nikneym Jul 5, 2026
3c4d098
`Network`: prefer `Config.isHashedDirectory` for consistency
nikneym Jul 5, 2026
bf916e7
`Config`: remove unnecessary import
nikneym Jul 5, 2026
fdb90a8
`help.zon`: obey alphabetical order
nikneym Jul 6, 2026
1b15985
`Config`: drop local in hashed dir check
nikneym Jul 6, 2026
d6ce373
remove `--disable-root-certificates` flag
nikneym Jul 8, 2026
1c97bc4
`crypto`: more bindings
nikneym Jul 8, 2026
2a53b1e
`createX509Store`: skip custom CA loading if none given
nikneym Jul 8, 2026
71536de
`createX509Store`: report if no certificates loaded
nikneym Jul 8, 2026
03cfdbe
`Config`: dupe received `dir` from arg iterator
nikneym Jul 15, 2026
1429202
update `createX509Store` in all call sites
nikneym Jul 15, 2026
6f828cb
`createX509Store`: track hashed directories separately
nikneym Jul 15, 2026
cbaef30
`Config`: fix crash related to absolute paths
nikneym Jul 15, 2026
83d3fb4
`cli`: introduce shared fields
nikneym Jul 21, 2026
6a9a1f9
`Config`: cert store creation is done in Config if custom CA given
nikneym Jul 21, 2026
0e32b9c
`Config`: remove dead `isHashedDirectory` function
nikneym Jul 22, 2026
4eeb9f5
`Config`: add `customCertStore` helper
nikneym Jul 22, 2026
f18d958
`Network`: rework how CA stores are created
nikneym Jul 22, 2026
9d6c54a
changes for Zig 0.16
nikneym Jul 22, 2026
b791512
`libcrypto`: remove dead bindings
nikneym Jul 22, 2026
a4ba95b
changes for Zig 0.16 (again)
nikneym Jul 22, 2026
45c8f74
adjust help for --ca-* options
krichprollsch Jul 23, 2026
b90d9de
remove dead condition
krichprollsch Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 136 additions & 9 deletions src/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Storage = @import("storage/Storage.zig");
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;

const log = lp.log;
const crypto = @import("sys/libcrypto.zig");
const Allocator = std.mem.Allocator;

// TCP keepalive parameters applied to accepted CDP connections.
Expand Down Expand Up @@ -73,18 +74,108 @@ fn logFilterScopesValidator(allocator: Allocator, args: *std.process.Args.Iterat
}
}

fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator) !?log.Level {
fn logLevelValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?log.Level) !void {
const str = args.next() orelse return error.MissingArgument;
if (std.mem.eql(u8, str, "error")) {
return .err;
target.* = .err;
return;
}

return std.meta.stringToEnum(log.Level, str) orelse {
target.* = std.meta.stringToEnum(log.Level, str) orelse {
log.fatal(.app, "invalid option choice", .{ .arg = "--log-level", .value = str });
return error.InvalidArgument;
};
}

const Cert = struct {
/// On successful CLI argument parsing phase, ownership of this transferred
/// to `Network`. Consider it as invalid.
store: ?*crypto.X509_STORE = null,
// Number of certificate sources loaded into `store`.
count: usize = 0,

fn deinit(self: *Cert) void {
if (self.store) |store| {
crypto.X509_STORE_free(store);
}
self.* = .{};
}

/// Returns the store, creating it on first use. The store is shared by
/// every `--ca-cert`/`--ca-path` occurrence.
fn getOrCreate(self: *Cert) !*crypto.X509_STORE {
if (self.store) |store| {
return store;
}
const store = crypto.X509_STORE_new() orelse
return error.FailedToCreateCertStore;
self.store = store;
return store;
}
};

fn caCertValidator(
_: Allocator,
args: *std.process.Args.Iterator,
cert: *Cert,
) !void {
const file_name = args.next() orelse return error.MissingArgument;
const store = try cert.getOrCreate();
errdefer cert.deinit();

if (crypto.X509_STORE_load_locations(store, file_name, null) != 1) {
log.fatal(.app, "Invalid CA cert", .{ .arg = "--ca-cert", .value = file_name });
return error.InvalidArgument;
}
cert.count += 1;
}

fn caPathValidator(
allocator: Allocator,
args: *std.process.Args.Iterator,
cert: *Cert,
) !void {
const dir_path = args.next() orelse return error.MissingArgument;

var dir = std.Io.Dir.cwd().openDir(lp.io, dir_path, .{ .iterate = true }) catch {
log.fatal(.app, "Invalid CA path", .{ .arg = "--ca-path", .value = dir_path });
return error.InvalidArgument;
};
defer dir.close(lp.io);

const store = try cert.getOrCreate();
errdefer cert.deinit();

// Eagerly load every certificate in the directory rather than
// registering a lazy hashed lookup: the directory doesn't need to be
// c_rehash'ed, bad entries surface at startup and `count` reflects
// what was actually loaded.
const count_before = cert.count;
var it = dir.iterate();
while (it.next(lp.io) catch {
log.fatal(.app, "Invalid CA path", .{ .arg = "--ca-path", .value = dir_path });
return error.InvalidArgument;
}) |entry| {
if (entry.kind != .file and entry.kind != .sym_link) continue;

const path = try std.fs.path.joinZ(allocator, &.{ dir_path, entry.name });
defer allocator.free(path);

if (crypto.X509_STORE_load_locations(store, path, null) != 1) {
log.warn(.app, "Skipping invalid CA cert", .{ .arg = "--ca-path", .value = path });
continue;
}
cert.count += 1;
}

// An empty directory (or one with no readable certificates) is
// indistinguishable from a typo; treat it as an error.
if (cert.count == count_before) {
log.fatal(.app, "No certificates loaded", .{ .arg = "--ca-path", .value = dir_path });
return error.InvalidArgument;
}
}

/// Common CLI args.
const CommonOptions = .{
.{ .name = "obey_robots", .type = bool },
Expand Down Expand Up @@ -118,24 +209,46 @@ const CommonOptions = .{
.{ .name = "v8_flags_unsafe", .type = ?[]const u8 },
.{ .name = "v8_max_heap_mb", .type = ?u32 },
.{ .name = "watchdog_ms", .type = ?u32 },
.{
.name = "ca_cert",
.field_name = "cert",
.type = .{
.cli = [:0]const u8,
.memory = Cert,
},
.default = Cert{},
.validator = caCertValidator,
},
.{
.name = "ca_path",
.field_name = "cert",
.type = .{
.cli = []const u8,
.memory = Cert,
},
.default = Cert{},
.validator = caPathValidator,
},
};

fn dumpValidator(_: Allocator, args: *std.process.Args.Iterator) !?DumpFormat {
fn dumpValidator(_: Allocator, args: *std.process.Args.Iterator, target: *?DumpFormat) !void {
// Peek next argument.
var peek_args = args.*;
if (peek_args.next()) |next_arg| {
const mode = std.meta.stringToEnum(DumpFormat, next_arg) orelse {
return .html;
target.* = .html;
return;
};

// Skip the argument we peek if successful.
_ = args.next();
return mode;
target.* = mode;
return;
}

// Means we couldn't get something like `--dump html` but we do have
// `--dump`; which should fall to `html` by default.
return .html;
target.* = .html;
}

pub const AiProvider = std.meta.Tag(zenai.provider.Client);
Expand All @@ -160,13 +273,13 @@ pub const AgentVerbosity = enum {
}
};

fn waitScriptFileValidator(allocator: Allocator, args: *std.process.Args.Iterator) !?[:0]const u8 {
fn waitScriptFileValidator(allocator: Allocator, args: *std.process.Args.Iterator, target: *?[:0]const u8) !void {
const path = args.next() orelse {
log.fatal(.app, "missing argument value", .{ .arg = "--wait-script-file" });
return error.InvalidArgument;
};

return std.Io.Dir.cwd().readFileAllocOptions(lp.io, path, allocator, .limited(1024 * 1024), .of(u8), 0) catch |err| {
target.* = std.Io.Dir.cwd().readFileAllocOptions(lp.io, path, allocator, .limited(1024 * 1024), .of(u8), 0) catch |err| {
log.fatal(.app, "failed to read file", .{ .arg = "--wait-script-file", .path = path, .err = err });
return error.InvalidArgument;
};
Expand Down Expand Up @@ -635,6 +748,20 @@ pub fn storageSqlitePath(self: *const Config) ?[:0]const u8 {
};
}

/// Returns the user-supplied certificate store (`--ca-cert`/`--ca-path`),
/// if any was loaded during argument parsing. The caller takes ownership.
pub fn customCertStore(self: *const Config) ?*crypto.X509_STORE {
return switch (self.mode) {
inline .serve, .fetch, .mcp, .agent => |opts| {
const store = opts.cert.store orelse return null;
// Validators guarantee a created store loaded something.
lp.assert(opts.cert.count > 0, "empty custom cert store", .{});
return store;
},
else => null,
};
}

pub const DumpFormat = enum {
html,
markdown,
Expand Down
2 changes: 1 addition & 1 deletion src/Updater.zig
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ config: *const Config,
pub fn init(allocator: Allocator, config: *const Config) !Updater {
Network.globalInit(allocator);
errdefer Network.globalDeinit();
const x509_store = try Network.createX509Store(allocator);
const x509_store = try Network.prepareX509Store(allocator, config);

return .{
.x509_store = x509_store,
Expand Down
Loading
Loading