const std = @import("std"); const Allocator = std.mem.Allocator; const Location = @import("resolver.zig").Location; const Ip2location = @import("Ip2location.zig"); const Cache = Ip2location.Cache; const Self = @This(); const log = std.log.scoped(.ipwhois); allocator: Allocator, /// Zig 0.16 requires an explicit `Io` for filesystem and HTTP work. io: std.Io, http_client: std.http.Client, cache: *Cache, pub fn init(allocator: Allocator, io: std.Io, cache_path: []const u8) !Self { const cache = try allocator.create(Cache); errdefer allocator.destroy(cache); cache.* = try .init(allocator, io, cache_path); return .{ .allocator = allocator, .io = io, .http_client = std.http.Client{ .allocator = allocator, .io = io }, .cache = cache, }; } pub fn deinit(self: *Self) void { self.cache.deinit(); self.allocator.destroy(self.cache); self.http_client.deinit(); } pub fn lookup(self: *Self, ip_str: []const u8) ?Location { // Parse IP to u128 for cache lookup const parsed = Ip2location.packIp(ip_str) orelse return null; const ip_u128 = parsed.key; const family = parsed.family; // Check cache first if (self.cache.get(ip_u128)) |result| return result; // Fetch from API const result = self.fetch(ip_str) catch |err| { log.err("API lookup failed: {}", .{err}); return null; }; // Store in cache self.cache.put(ip_u128, family, result) catch |err| { log.warn("Failed to cache result: {}", .{err}); }; return result; } fn fetch(self: *Self, ip_str: []const u8) !Location { 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); try w.writeAll("https://ipwho.is/"); try w.writeAll(ip_str); // Request only the fields we need. `country_code` drives unit selection, // which cannot be derived from `country` without matching display text. try w.writeAll("?fields=city,region,country,country_code,latitude,longitude&output=json"); var response_buf: [4096]u8 = undefined; var writer = std.Io.Writer.fixed(&response_buf); const result = try self.http_client.fetch(.{ .location = .{ .url = w.buffered() }, .method = .GET, .response_writer = &writer, .extra_headers = &.{ .{ .name = "User-Agent", .value = "wttr.in" }, }, }); if (result.status != .ok) { log.err("API returned status {}", .{result.status}); return error.ApiError; } const response_body = response_buf[0..writer.end]; // Parse JSON response const parsed = try std.json.parseFromSlice( std.json.Value, self.allocator, response_body, .{}, ); defer parsed.deinit(); const obj = parsed.value.object; // Check for success field if (obj.get("success")) |success| { if (success == .bool and !success.bool) { const msg = if (obj.get("message")) |m| if (m == .string) m.string else "unknown" else "unknown"; log.err("API returned error for ip {s}: {s}", .{ ip_str, msg }); return error.ApiError; } } const lat = obj.get("latitude") orelse return error.MissingLatitude; const lon = obj.get("longitude") orelse return error.MissingLongitude; if (lat != .float and lat != .integer) { log.err("Latitude returned from ipwho.is for ip {s} is not a number", .{ip_str}); return error.MissingLatitude; } if (lon != .float and lon != .integer) { log.err("Longitude returned from ipwho.is for ip {s} is not a number", .{ip_str}); return error.MissingLongitude; } const city = getString(obj, "city"); const region = getString(obj, "region"); const country = getString(obj, "country"); const display_name = Location.buildDisplayName( self.allocator, city, region, country, ip_str, ); const lat_val: f64 = if (lat == .float) @floatCast(lat.float) else @floatFromInt(lat.integer); const lon_val: f64 = if (lon == .float) @floatCast(lon.float) else @floatFromInt(lon.integer); return Location{ .allocator = self.allocator, .name = display_name, .coords = .{ .latitude = lat_val, .longitude = lon_val, }, .iso_country = Location.isoFrom(getString(obj, "country_code")), }; } inline fn getString(obj: std.json.ObjectMap, key: []const u8) []const u8 { const maybe_val = obj.get(key); if (maybe_val == null) return ""; if (maybe_val.? != .string) return ""; return maybe_val.?.string; }