Compare commits

..

2 commits

Author SHA1 Message Date
5504f44747
fix double free in listen host variable
All checks were successful
Generic zig build / build (push) Successful in 2m21s
Generic zig build / deploy (push) Successful in 18s
2026-10-26 15:34:52 -07:00
90abfbf79f
create new http client for Ipwhois 2026-10-26 15:27:48 -07:00
4 changed files with 37 additions and 10 deletions

View file

@ -15,7 +15,7 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Zig - name: Setup Zig
uses: https://codeberg.org/mlugg/setup-zig@v2.2.1 uses: https://github.com/mlugg/setup-zig@v2.2.1
- name: Build project - name: Build project
run: zig build --summary all run: zig build --summary all
- name: Run tests - name: Run tests

View file

@ -71,7 +71,7 @@ pub fn load(allocator: std.mem.Allocator, env: *const std.process.Environ.Map) !
defer allocator.free(default_cache_dir); defer allocator.free(default_cache_dir);
return .{ return .{
.listen_host = env.get("WTTR_LISTEN_HOST") orelse try allocator.dupe(u8, "0.0.0.0"), .listen_host = try allocator.dupe(u8, env.get("WTTR_LISTEN_HOST") orelse "0.0.0.0"),
.listen_port = if (env.get("WTTR_LISTEN_PORT")) |p| .listen_port = if (env.get("WTTR_LISTEN_PORT")) |p|
try std.fmt.parseInt(u16, p, 10) try std.fmt.parseInt(u16, p, 10)
else else
@ -181,3 +181,19 @@ test "config loads defaults" {
try std.testing.expectEqual(@as(u16, 8002), cfg.listen_port); try std.testing.expectEqual(@as(u16, 8002), cfg.listen_port);
try std.testing.expectEqual(@as(usize, 10_000), cfg.cache_size); try std.testing.expectEqual(@as(usize, 10_000), cfg.cache_size);
} }
test "config owns WTTR_LISTEN_HOST independently of the environment map" {
const allocator = std.testing.allocator;
var env: std.process.Environ.Map = .init(allocator);
// Declared first so it runs last, matching `main`: the config is torn down
// while the environment map is still alive, then the map frees its values.
// A config that borrowed the map's string would free it twice.
defer env.deinit();
try env.put("WTTR_CACHE_DIR", @import("build_options").cache_dir);
try env.put("WTTR_LISTEN_HOST", "127.0.0.1");
const cfg = try Config.load(allocator, &env);
defer cfg.deinit(allocator);
try std.testing.expectEqualStrings("127.0.0.1", cfg.listen_host);
}

View file

@ -13,7 +13,6 @@ allocator: Allocator,
/// every method that touches a file or the network. /// every method that touches a file or the network.
io: std.Io, io: std.Io,
api_key: ?[]const u8, api_key: ?[]const u8,
http_client: std.http.Client,
cache: *Cache, cache: *Cache,
pub fn init(allocator: Allocator, io: std.Io, api_key: ?[]const u8, cache_path: []const u8) !Self { pub fn init(allocator: Allocator, io: std.Io, api_key: ?[]const u8, cache_path: []const u8) !Self {
@ -24,7 +23,6 @@ pub fn init(allocator: Allocator, io: std.Io, api_key: ?[]const u8, cache_path:
.allocator = allocator, .allocator = allocator,
.io = io, .io = io,
.api_key = if (api_key) |k| try allocator.dupe(u8, k) else null, .api_key = if (api_key) |k| try allocator.dupe(u8, k) else null,
.http_client = std.http.Client{ .allocator = allocator, .io = io },
.cache = cache, .cache = cache,
}; };
} }
@ -32,7 +30,6 @@ pub fn init(allocator: Allocator, io: std.Io, api_key: ?[]const u8, cache_path:
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.cache.deinit(); self.cache.deinit();
self.allocator.destroy(self.cache); self.allocator.destroy(self.cache);
self.http_client.deinit();
if (self.api_key) |k| if (self.api_key) |k|
self.allocator.free(k); self.allocator.free(k);
} }
@ -106,9 +103,15 @@ fn fetch(self: *Self, ip_str: []const u8) !Location {
if (self.api_key) |key| if (self.api_key) |key|
try w.print("&key={s}", .{key}); try w.print("&key={s}", .{key});
// A fresh client per request, deliberately: a long-lived `std.http.Client`
// validates certificates against the time of its first request. See the
// matching comment in `IpWhoIs.fetch` for the failure this prevents.
var client: std.http.Client = .{ .allocator = self.allocator, .io = self.io };
defer client.deinit();
var response_buf: [4096]u8 = undefined; var response_buf: [4096]u8 = undefined;
var writer = std.Io.Writer.fixed(&response_buf); var writer = std.Io.Writer.fixed(&response_buf);
const result = try self.http_client.fetch(.{ const result = try client.fetch(.{
.location = .{ .url = w.buffered() }, .location = .{ .url = w.buffered() },
.method = .GET, .method = .GET,
.response_writer = &writer, .response_writer = &writer,

View file

@ -11,7 +11,6 @@ const log = std.log.scoped(.ipwhois);
allocator: Allocator, allocator: Allocator,
/// Zig 0.16 requires an explicit `Io` for filesystem and HTTP work. /// Zig 0.16 requires an explicit `Io` for filesystem and HTTP work.
io: std.Io, io: std.Io,
http_client: std.http.Client,
cache: *Cache, cache: *Cache,
pub fn init(allocator: Allocator, io: std.Io, cache_path: []const u8) !Self { pub fn init(allocator: Allocator, io: std.Io, cache_path: []const u8) !Self {
@ -21,7 +20,6 @@ pub fn init(allocator: Allocator, io: std.Io, cache_path: []const u8) !Self {
return .{ return .{
.allocator = allocator, .allocator = allocator,
.io = io, .io = io,
.http_client = std.http.Client{ .allocator = allocator, .io = io },
.cache = cache, .cache = cache,
}; };
} }
@ -29,7 +27,6 @@ pub fn init(allocator: Allocator, io: std.Io, cache_path: []const u8) !Self {
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.cache.deinit(); self.cache.deinit();
self.allocator.destroy(self.cache); self.allocator.destroy(self.cache);
self.http_client.deinit();
} }
pub fn lookup(self: *Self, ip_str: []const u8) ?Location { pub fn lookup(self: *Self, ip_str: []const u8) ?Location {
@ -69,9 +66,20 @@ fn fetch(self: *Self, ip_str: []const u8) !Location {
// which cannot be derived from `country` without matching display text. // which cannot be derived from `country` without matching display text.
try w.writeAll("?fields=city,region,country,country_code,latitude,longitude&output=json"); try w.writeAll("?fields=city,region,country,country_code,latitude,longitude&output=json");
// A fresh client per request, deliberately. `std.http.Client` latches the
// wall clock into `now` on its first HTTPS request and validates every
// later certificate against that frozen time. A client kept for the life of
// the server therefore rejects any certificate issued after its first
// lookup (`CertificateNotYetValid`, which std reports only as
// `TlsInitializationFailed`), so lookups start failing as soon as the
// provider rotates its certificate and keep failing until a restart.
// Lookups are cached permanently, so the per-request CA bundle scan is rare.
var client: std.http.Client = .{ .allocator = self.allocator, .io = self.io };
defer client.deinit();
var response_buf: [4096]u8 = undefined; var response_buf: [4096]u8 = undefined;
var writer = std.Io.Writer.fixed(&response_buf); var writer = std.Io.Writer.fixed(&response_buf);
const result = try self.http_client.fetch(.{ const result = try client.fetch(.{
.location = .{ .url = w.buffered() }, .location = .{ .url = w.buffered() },
.method = .GET, .method = .GET,
.response_writer = &writer, .response_writer = &writer,