enable ip2location unconditionally as it can work without an api key
This commit is contained in:
parent
ce958837e0
commit
1e6a5e28ca
5 changed files with 66 additions and 74 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue