From 1e6a5e28ca3e8ca11426264c80adc748225819fc Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 6 Jan 2026 12:13:45 -0800 Subject: [PATCH] enable ip2location unconditionally as it can work without an api key --- src/http/Server.zig | 4 +-- src/location/GeoIp.zig | 64 +++++++++++++--------------------- src/location/Ip2location.zig | 67 +++++++++++++++++++++--------------- src/location/resolver.zig | 3 +- src/main.zig | 2 +- 5 files changed, 66 insertions(+), 74 deletions(-) diff --git a/src/http/Server.zig b/src/http/Server.zig index 5737f6e..cd87ed7 100644 --- a/src/http/Server.zig +++ b/src/http/Server.zig @@ -121,10 +121,8 @@ const MockHarness = struct { const geoip = try allocator.create(GeoIp); errdefer allocator.destroy(geoip); - geoip.* = GeoIp.init(allocator, config.geolite_path, null, null) catch { - allocator.destroy(geoip); + geoip.* = GeoIp.init(allocator, config.geolite_path, null, config.ip2location_cache_file) catch return error.SkipZigTest; - }; errdefer geoip.deinit(); var geocache = try allocator.create(GeoCache); diff --git a/src/location/GeoIp.zig b/src/location/GeoIp.zig index ecc65f8..ae4f4f7 100644 --- a/src/location/GeoIp.zig +++ b/src/location/GeoIp.zig @@ -10,11 +10,10 @@ const GeoIP = @This(); const log = std.log.scoped(.geoip); mmdb: *c.MMDB_s, -ip2location_client: ?*Ip2location, -ip2location_cache: ?*Ip2location.Cache, +ip2location_client: *Ip2location, allocator: std.mem.Allocator, -pub fn init(allocator: std.mem.Allocator, db_path: []const u8, api_key: ?[]const u8, cache_path: ?[]const u8) !GeoIP { +pub fn init(allocator: std.mem.Allocator, db_path: []const u8, api_key: ?[]const u8, cache_path: []const u8) !GeoIP { const path_z = try std.heap.c_allocator.dupeZ(u8, db_path); defer std.heap.c_allocator.free(path_z); @@ -22,32 +21,25 @@ pub fn init(allocator: std.mem.Allocator, db_path: []const u8, api_key: ?[]const errdefer allocator.destroy(mmdb); const status = c.MMDB_open(path_z.ptr, c.MMDB_MODE_MMAP, mmdb); - if (status != c.MMDB_SUCCESS) { + if (status != c.MMDB_SUCCESS) return error.CannotOpenDatabase; + + const client: *Ip2location = try allocator.create(Ip2location); + errdefer allocator.destroy(client); + client.* = try Ip2location.init(allocator, api_key, cache_path); + errdefer { + client.deinit(); + allocator.destroy(client); } - var client: ?*Ip2location = null; - var cache: ?*Ip2location.Cache = null; - - if (api_key) |key| { - client = try allocator.create(Ip2location); - client.?.* = try Ip2location.init(allocator, key); - - if (cache_path) |path| { - cache = try allocator.create(Ip2location.Cache); - cache.?.* = try Ip2location.Cache.init(allocator, path); - std.log.info("IP2Location fallback: enabled (cache: {s})", .{path}); - } else { - std.log.info("IP2Location fallback: enabled (no cache)", .{}); - } - } else { - std.log.info("IP2Location fallback: disabled (no API key configured)", .{}); - } + std.log.info( + "IP2Location fallback: {s} (cache: {s})", + .{ if (api_key) |_| "key provided, 50k/mo limit" else "no key, 1k/day limit", cache_path }, + ); return GeoIP{ .mmdb = mmdb, .ip2location_client = client, - .ip2location_cache = cache, .allocator = allocator, }; } @@ -55,14 +47,9 @@ pub fn init(allocator: std.mem.Allocator, db_path: []const u8, api_key: ?[]const pub fn deinit(self: *GeoIP) void { c.MMDB_close(self.mmdb); self.allocator.destroy(self.mmdb); - if (self.ip2location_client) |client| { - client.deinit(); - self.allocator.destroy(client); - } - if (self.ip2location_cache) |cache| { - cache.deinit(); - self.allocator.destroy(cache); - } + self.ip2location_client.deinit(); + log.debug("destroying client", .{}); + self.allocator.destroy(self.ip2location_client); } pub fn lookup(self: *GeoIP, ip: []const u8) ?Coordinates { @@ -74,11 +61,8 @@ pub fn lookup(self: *GeoIP, ip: []const u8) ?Coordinates { if (self.extractCoordinates(ip, result)) |coords| return coords; - // Fallback to IP2Location if configured - if (self.ip2location_client) |client| - return client.lookupWithCache(ip, self.ip2location_cache); - - return null; + // Fallback to IP2Location + return self.ip2location_client.lookup(ip); } fn lookupInternal(mmdb: *c.MMDB_s, ip: []const u8) !c.MMDB_lookup_result_s { @@ -161,11 +145,11 @@ test "MMDB functions are callable" { } test "GeoIP init with invalid path fails" { - const result = GeoIP.init(std.testing.allocator, "/nonexistent/path.mmdb", null, null); + const result = GeoIP.init(std.testing.allocator, "/nonexistent/path.mmdb", null, ""); try std.testing.expectError(error.CannotOpenDatabase, result); } -test "isUSIP detects US IPs" { +test "isUSIp detects US IPs" { const allocator = std.testing.allocator; const Config = @import("../Config.zig"); const config = try Config.load(allocator); @@ -178,9 +162,8 @@ test "isUSIP detects US IPs" { try GeoLite2.ensureDatabase(std.testing.allocator, db_path); } - var geoip = GeoIP.init(std.testing.allocator, db_path, null, null) catch { + var geoip = GeoIP.init(std.testing.allocator, db_path, null, config.ip2location_cache_file) catch return error.SkipZigTest; - }; defer geoip.deinit(); // Test that the function doesn't crash with various IPs @@ -203,9 +186,8 @@ test "lookup works" { try GeoLite2.ensureDatabase(std.testing.allocator, db_path); } - var geoip = GeoIP.init(std.testing.allocator, db_path, null, null) catch { + var geoip = GeoIP.init(std.testing.allocator, db_path, null, config.ip2location_cache_file) catch return error.SkipZigTest; - }; defer geoip.deinit(); // Test that the function doesn't crash with various IPs diff --git a/src/location/Ip2location.zig b/src/location/Ip2location.zig index 3fd691d..df58e9c 100644 --- a/src/location/Ip2location.zig +++ b/src/location/Ip2location.zig @@ -7,23 +7,31 @@ const Self = @This(); const log = std.log.scoped(.ip2location); allocator: Allocator, -api_key: []const u8, +api_key: ?[]const u8, http_client: std.http.Client, +cache: *Cache, -pub fn init(allocator: Allocator, api_key: []const u8) !Self { +pub fn init(allocator: Allocator, api_key: ?[]const u8, cache_path: []const u8) !Self { + const cache = try allocator.create(Cache); + errdefer allocator.destroy(cache); + cache.* = try .init(allocator, cache_path); return .{ .allocator = allocator, - .api_key = try allocator.dupe(u8, api_key), + .api_key = if (api_key) |k| try allocator.dupe(u8, k) else null, .http_client = std.http.Client{ .allocator = allocator }, + .cache = cache, }; } pub fn deinit(self: *Self) void { + self.cache.deinit(); + self.allocator.destroy(self.cache); self.http_client.deinit(); - self.allocator.free(self.api_key); + if (self.api_key) |k| + self.allocator.free(k); } -pub fn lookupWithCache(self: *Self, ip_str: []const u8, cache: ?*Cache) ?Coordinates { +pub fn lookup(self: *Self, ip_str: []const u8) ?Coordinates { // Parse IP to u128 for cache lookup const addr = std.net.Address.parseIp(ip_str, 0) catch return null; const ip_u128: u128 = switch (addr.any.family) { @@ -34,46 +42,39 @@ pub fn lookupWithCache(self: *Self, ip_str: []const u8, cache: ?*Cache) ?Coordin const family: u8 = if (addr.any.family == std.posix.AF.INET) 4 else 6; // Check cache first - if (cache) |c| { - if (c.get(ip_u128)) |coords| { - return coords; - } - } + if (self.cache.get(ip_u128)) |coords| + return coords; // Fetch from API - const coords = self.lookup(ip_str) catch |err| { + const coords = self.fetch(ip_str) catch |err| { log.err("API lookup failed: {}", .{err}); return null; }; // Store in cache - if (cache) |c| { - c.put(ip_u128, family, coords) catch |err| { - log.warn("Failed to cache result: {}", .{err}); - }; - } + self.cache.put(ip_u128, family, coords) catch |err| { + log.warn("Failed to cache result: {}", .{err}); + }; return coords; } -pub fn lookup(self: *Self, ip_str: []const u8) !Coordinates { +fn fetch(self: *Self, ip_str: []const u8) !Coordinates { log.info("Fetching geolocation for IP {s}", .{ip_str}); if (@import("builtin").is_test) return error.LookupUnavailableInUnitTest; + var buf: [256]u8 = undefined; + var w = std.Io.Writer.fixed(&buf); // Build URL: https://api.ip2location.io/?key=XXX&ip=1.2.3.4 - const url = try std.fmt.allocPrint( - self.allocator, - "https://api.ip2location.io/?key={s}&ip={s}", - .{ self.api_key, ip_str }, - ); - defer self.allocator.free(url); - - const uri = try std.Uri.parse(url); + try w.writeAll("https://api.ip2location.io/?ip="); + try w.writeAll(ip_str); + if (self.api_key) |key| + try w.print("&key={s}", .{key}); var response_buf: [4096]u8 = undefined; var writer = std.io.Writer.fixed(&response_buf); const result = try self.http_client.fetch(.{ - .location = .{ .uri = uri }, + .location = .{ .url = w.buffered() }, .method = .GET, .response_writer = &writer, }); @@ -97,7 +98,18 @@ pub fn lookup(self: *Self, ip_str: []const u8) !Coordinates { const obj = parsed.value.object; const lat = obj.get("latitude") orelse return error.MissingLatitude; const lon = obj.get("longitude") orelse return error.MissingLongitude; - + if (lat == .null) return error.MissingLatitude; + if (lat != .float) + log.err( + "Latitude returned from ip2location.io for ip {s} is not a float: {f}", + .{ ip_str, std.json.fmt(lat, .{}) }, + ); + if (lon == .null) return error.MissingLongitude; + if (lon != .float) + log.err( + "Longitude returned from ip2location.io for ip {s} is not a float: {f}", + .{ ip_str, std.json.fmt(lon, .{}) }, + ); return Coordinates{ .latitude = @floatCast(lat.float), .longitude = @floatCast(lon.float), @@ -131,6 +143,7 @@ pub const Cache = struct { .entries = std.AutoHashMap(u128, Coordinates).init(allocator), .file = null, }; + errdefer allocator.free(cache.path); // Try to open existing cache file if (std.fs.openFileAbsolute(path, .{ .mode = .read_write })) |file| { diff --git a/src/location/resolver.zig b/src/location/resolver.zig index 789247c..68b6566 100644 --- a/src/location/resolver.zig +++ b/src/location/resolver.zig @@ -254,9 +254,8 @@ test "resolve IP address with GeoIP" { try GeoLite2.ensureDatabase(allocator, config.geolite_path); } - var geoip = GeoIp.init(allocator, config.geolite_path, null, null) catch { + var geoip = GeoIp.init(allocator, config.geolite_path, null, config.ip2location_cache_file) catch return error.SkipZigTest; - }; defer geoip.deinit(); var geocache = try GeoCache.init(allocator, null); diff --git a/src/main.zig b/src/main.zig index 148bfdb..cedc265 100644 --- a/src/main.zig +++ b/src/main.zig @@ -43,7 +43,7 @@ pub fn main() !u8 { allocator, cfg.geolite_path, cfg.ip2location_api_key, - if (cfg.ip2location_api_key != null) cfg.ip2location_cache_file else null, + cfg.ip2location_cache_file, ) catch |err| { std.log.err("Failed to load GeoIP database from {s}: {}", .{ cfg.geolite_path, err }); return err;