Compare commits
No commits in common. "1e6a5e28ca3e8ca11426264c80adc748225819fc" and "456057a2c054cf8ff5d1e1ed2e004d531405bff5" have entirely different histories.
1e6a5e28ca
...
456057a2c0
10 changed files with 106 additions and 169 deletions
19
src/cache/Cache.zig
vendored
19
src/cache/Cache.zig
vendored
|
|
@ -49,7 +49,8 @@ pub fn get(self: *Cache, key: []const u8) ?[]const u8 {
|
||||||
|
|
||||||
// Check L2 (disk)
|
// Check L2 (disk)
|
||||||
const cached = self.loadFromFile(key) catch return null;
|
const cached = self.loadFromFile(key) catch return null;
|
||||||
defer cached.deinit(self.allocator);
|
defer self.allocator.free(cached.key);
|
||||||
|
defer self.allocator.free(cached.value);
|
||||||
|
|
||||||
// L2 exists - promote to L1
|
// L2 exists - promote to L1
|
||||||
self.lru.put(key, cached.value, cached.expires) catch return null;
|
self.lru.put(key, cached.value, cached.expires) catch return null;
|
||||||
|
|
@ -85,11 +86,6 @@ const CacheEntry = struct {
|
||||||
key: []const u8,
|
key: []const u8,
|
||||||
value: []const u8,
|
value: []const u8,
|
||||||
expires: i64,
|
expires: i64,
|
||||||
|
|
||||||
pub fn deinit(self: CacheEntry, allocator: std.mem.Allocator) void {
|
|
||||||
allocator.free(self.key);
|
|
||||||
allocator.free(self.value);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Loads cached value from file (path calculated by the key). An error will be thrown
|
/// Loads cached value from file (path calculated by the key). An error will be thrown
|
||||||
|
|
@ -117,7 +113,7 @@ fn loadFromFilePath(self: *Cache, file_path: []const u8) !CacheEntry {
|
||||||
const reader = &file_reader.interface;
|
const reader = &file_reader.interface;
|
||||||
|
|
||||||
const cached = try deserialize(self.allocator, reader);
|
const cached = try deserialize(self.allocator, reader);
|
||||||
errdefer cached.deinit(self.allocator);
|
errdefer self.allocator.free(cached.value);
|
||||||
|
|
||||||
// Check if expired
|
// Check if expired
|
||||||
const now = std.time.milliTimestamp();
|
const now = std.time.milliTimestamp();
|
||||||
|
|
@ -183,7 +179,8 @@ fn loadFromDir(self: *Cache) !void {
|
||||||
defer self.allocator.free(file_path);
|
defer self.allocator.free(file_path);
|
||||||
|
|
||||||
const cached = self.loadFromFilePath(file_path) catch continue;
|
const cached = self.loadFromFilePath(file_path) catch continue;
|
||||||
defer cached.deinit(self.allocator);
|
defer self.allocator.free(cached.key);
|
||||||
|
defer self.allocator.free(cached.value);
|
||||||
|
|
||||||
// Populate L1 cache from L2
|
// Populate L1 cache from L2
|
||||||
self.lru.put(cached.key, cached.value, cached.expires) catch continue;
|
self.lru.put(cached.key, cached.value, cached.expires) catch continue;
|
||||||
|
|
@ -221,7 +218,8 @@ test "serialize and deserialize" {
|
||||||
var fixed_reader = std.Io.Reader.fixed(serialized);
|
var fixed_reader = std.Io.Reader.fixed(serialized);
|
||||||
|
|
||||||
const cached = try deserialize(allocator, &fixed_reader);
|
const cached = try deserialize(allocator, &fixed_reader);
|
||||||
defer cached.deinit(allocator);
|
defer allocator.free(cached.key);
|
||||||
|
defer allocator.free(cached.value);
|
||||||
|
|
||||||
try std.testing.expectEqualStrings(key, cached.key);
|
try std.testing.expectEqualStrings(key, cached.key);
|
||||||
try std.testing.expectEqualStrings(value, cached.value);
|
try std.testing.expectEqualStrings(value, cached.value);
|
||||||
|
|
@ -236,7 +234,8 @@ test "deserialize handles integer expires" {
|
||||||
var fixed_reader = std.Io.Reader.fixed(json);
|
var fixed_reader = std.Io.Reader.fixed(json);
|
||||||
|
|
||||||
const cached = try deserialize(allocator, &fixed_reader);
|
const cached = try deserialize(allocator, &fixed_reader);
|
||||||
defer cached.deinit(allocator);
|
defer allocator.free(cached.key);
|
||||||
|
defer allocator.free(cached.value);
|
||||||
|
|
||||||
try std.testing.expectEqualStrings("k", cached.key);
|
try std.testing.expectEqualStrings("k", cached.key);
|
||||||
try std.testing.expectEqualStrings("v", cached.value);
|
try std.testing.expectEqualStrings("v", cached.value);
|
||||||
|
|
|
||||||
|
|
@ -121,8 +121,10 @@ const MockHarness = struct {
|
||||||
|
|
||||||
const geoip = try allocator.create(GeoIp);
|
const geoip = try allocator.create(GeoIp);
|
||||||
errdefer allocator.destroy(geoip);
|
errdefer allocator.destroy(geoip);
|
||||||
geoip.* = GeoIp.init(allocator, config.geolite_path, null, config.ip2location_cache_file) catch
|
geoip.* = GeoIp.init(allocator, config.geolite_path, null, null) catch {
|
||||||
|
allocator.destroy(geoip);
|
||||||
return error.SkipZigTest;
|
return error.SkipZigTest;
|
||||||
|
};
|
||||||
errdefer geoip.deinit();
|
errdefer geoip.deinit();
|
||||||
|
|
||||||
var geocache = try allocator.create(GeoCache);
|
var geocache = try allocator.create(GeoCache);
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 291 B |
|
|
@ -48,11 +48,7 @@ pub fn handleWeather(
|
||||||
break :blk loc;
|
break :blk loc;
|
||||||
} else break :blk client_ip; // no location, just use client ip instead
|
} else break :blk client_ip; // no location, just use client ip instead
|
||||||
};
|
};
|
||||||
if (std.mem.eql(u8, "favicon.ico", location)) {
|
|
||||||
res.header("Content-Type", "image/x-icon");
|
|
||||||
res.body = @embedFile("favicon.ico");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.debug("location = {s}, client_ip = {s}", .{ location, client_ip });
|
log.debug("location = {s}, client_ip = {s}", .{ location, client_ip });
|
||||||
if (location.len == 0) {
|
if (location.len == 0) {
|
||||||
res.content_type = .TEXT;
|
res.content_type = .TEXT;
|
||||||
|
|
@ -168,64 +164,27 @@ fn handleWeatherInternal(
|
||||||
break :blk false;
|
break :blk false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add coordinates header using response allocator
|
const output = if (params.format) |fmt| blk: {
|
||||||
const coords_header = try std.fmt.allocPrint(res.arena, "{d:.4},{d:.4}", .{ location.coords.latitude, location.coords.longitude });
|
|
||||||
res.headers.add("X-Location-Coordinates", coords_header);
|
|
||||||
|
|
||||||
res.body = blk: {
|
|
||||||
if (params.format) |fmt| {
|
|
||||||
// Anything except the json will be plain text
|
|
||||||
res.content_type = .TEXT;
|
|
||||||
if (std.mem.eql(u8, fmt, "j1")) {
|
if (std.mem.eql(u8, fmt, "j1")) {
|
||||||
res.content_type = .JSON; // reset to json
|
res.content_type = .JSON;
|
||||||
break :blk try json.render(req_alloc, weather);
|
break :blk try json.render(req_alloc, weather);
|
||||||
}
|
}
|
||||||
if (std.mem.eql(u8, fmt, "v2"))
|
if (std.mem.eql(u8, fmt, "v2"))
|
||||||
break :blk try v2.render(req_alloc, weather, use_imperial);
|
break :blk try v2.render(req_alloc, weather, use_imperial);
|
||||||
if (std.mem.startsWith(u8, fmt, "%"))
|
if (std.mem.startsWith(u8, fmt, "%"))
|
||||||
break :blk try custom.render(req_alloc, weather, fmt, use_imperial);
|
break :blk try custom.render(req_alloc, weather, fmt, use_imperial);
|
||||||
// fall back to line if we don't understand the format parameter
|
// fall back to line if we don't understant the format parameter
|
||||||
break :blk try line.render(req_alloc, weather, fmt, use_imperial);
|
break :blk try line.render(req_alloc, weather, fmt, use_imperial);
|
||||||
} else {
|
} else try formatted.render(req_alloc, weather, .{ .use_imperial = use_imperial });
|
||||||
const format: formatted.Format = determineFormat(params, req.headers.get("user-agent"));
|
|
||||||
log.debug(
|
|
||||||
"Format: {}. params.ansi {}, params.text {}, user agent: {?s}",
|
|
||||||
.{ format, params.ansi, params.text_only, req.headers.get("user-agent") },
|
|
||||||
);
|
|
||||||
if (format != .html) res.content_type = .TEXT else res.content_type = .HTML;
|
|
||||||
break :blk try formatted.render(req_alloc, weather, .{ .use_imperial = use_imperial, .format = format });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn determineFormat(params: QueryParams, user_agent: ?[]const u8) formatted.Format {
|
// Add coordinates header using response allocator
|
||||||
if (params.ansi or params.text_only) {
|
const coords_header = try std.fmt.allocPrint(res.arena, "{d:.4},{d:.4}", .{ location.coords.latitude, location.coords.longitude });
|
||||||
// user explicitly requested something. If both are set, text will win
|
res.headers.add("X-Location-Coordinates", coords_header);
|
||||||
if (params.text_only) return .plain_text;
|
|
||||||
return .ansi;
|
if (res.content_type != .JSON)
|
||||||
}
|
res.content_type = .TEXT;
|
||||||
const ua = user_agent orelse "";
|
|
||||||
// https://github.com/chubin/wttr.in/blob/master/lib/globals.py#L82C1-L97C2
|
res.body = output;
|
||||||
const plain_text_agents = &[_][]const u8{
|
|
||||||
"curl",
|
|
||||||
"httpie",
|
|
||||||
"lwp-request",
|
|
||||||
"wget",
|
|
||||||
"python-requests",
|
|
||||||
"python-httpx",
|
|
||||||
"openbsd ftp",
|
|
||||||
"powershell",
|
|
||||||
"fetch",
|
|
||||||
"aiohttp",
|
|
||||||
"http_get",
|
|
||||||
"xh",
|
|
||||||
"nushell",
|
|
||||||
"zig",
|
|
||||||
};
|
|
||||||
for (plain_text_agents) |agent|
|
|
||||||
if (std.mem.indexOf(u8, ua, agent)) |_|
|
|
||||||
return .ansi;
|
|
||||||
return .html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseXForwardedFor extracts first IP" {
|
test "parseXForwardedFor extracts first IP" {
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,11 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
///Units:
|
|
||||||
///
|
|
||||||
/// m # metric (SI) (used by default everywhere except US)
|
|
||||||
/// u # USCS (used by default in US)
|
|
||||||
/// M # show wind speed in m/s
|
|
||||||
///
|
|
||||||
///View options:
|
|
||||||
///
|
|
||||||
/// 0 # only current weather
|
|
||||||
/// 1 # current weather + today's forecast
|
|
||||||
/// 2 # current weather + today's + tomorrow's forecast
|
|
||||||
/// A # ignore User-Agent and force ANSI output format (terminal)
|
|
||||||
/// d # restrict output to standard console font glyphs
|
|
||||||
/// F # do not show the "Follow" line
|
|
||||||
/// n # narrow version (only day and night)
|
|
||||||
/// q # quiet version (no "Weather report" text)
|
|
||||||
/// Q # superquiet version (no "Weather report", no city name)
|
|
||||||
/// T # switch terminal sequences off (no colors)
|
|
||||||
pub const QueryParams = struct {
|
pub const QueryParams = struct {
|
||||||
format: ?[]const u8 = null,
|
format: ?[]const u8 = null,
|
||||||
lang: ?[]const u8 = null,
|
lang: ?[]const u8 = null,
|
||||||
location: ?[]const u8 = null,
|
location: ?[]const u8 = null,
|
||||||
units: ?Units = null,
|
units: ?Units = null,
|
||||||
transparency: ?u8 = null,
|
transparency: ?u8 = null,
|
||||||
/// A: Ignore user agent and force ansi mode
|
|
||||||
ansi: bool = false,
|
|
||||||
/// T: Avoid terminal sequences and just output plain text
|
|
||||||
text_only: bool = false,
|
|
||||||
|
|
||||||
pub const Units = enum {
|
pub const Units = enum {
|
||||||
metric,
|
metric,
|
||||||
|
|
@ -45,22 +23,16 @@ pub const QueryParams = struct {
|
||||||
const key = kv.next() orelse continue;
|
const key = kv.next() orelse continue;
|
||||||
const value = kv.next();
|
const value = kv.next();
|
||||||
|
|
||||||
if (key.len == 1) {
|
|
||||||
switch (key[0]) {
|
|
||||||
'u' => params.units = .uscs,
|
|
||||||
'm' => params.units = .metric,
|
|
||||||
'A' => params.ansi = true,
|
|
||||||
'T' => params.text_only = true,
|
|
||||||
't' => params.transparency = 150,
|
|
||||||
else => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (std.mem.eql(u8, key, "format")) {
|
if (std.mem.eql(u8, key, "format")) {
|
||||||
params.format = if (value) |v| try allocator.dupe(u8, v) else null;
|
params.format = if (value) |v| try allocator.dupe(u8, v) else null;
|
||||||
} else if (std.mem.eql(u8, key, "lang")) {
|
} else if (std.mem.eql(u8, key, "lang")) {
|
||||||
params.lang = if (value) |v| try allocator.dupe(u8, v) else null;
|
params.lang = if (value) |v| try allocator.dupe(u8, v) else null;
|
||||||
} else if (std.mem.eql(u8, key, "location")) {
|
} else if (std.mem.eql(u8, key, "location")) {
|
||||||
params.location = if (value) |v| try allocator.dupe(u8, v) else null;
|
params.location = if (value) |v| try allocator.dupe(u8, v) else null;
|
||||||
|
} else if (std.mem.eql(u8, key, "u")) {
|
||||||
|
params.units = .uscs;
|
||||||
|
} else if (std.mem.eql(u8, key, "m")) {
|
||||||
|
params.units = .metric;
|
||||||
} else if (std.mem.eql(u8, key, "use_imperial")) {
|
} else if (std.mem.eql(u8, key, "use_imperial")) {
|
||||||
params.units = .uscs;
|
params.units = .uscs;
|
||||||
} else if (std.mem.eql(u8, key, "use_metric")) {
|
} else if (std.mem.eql(u8, key, "use_metric")) {
|
||||||
|
|
@ -69,6 +41,8 @@ pub const QueryParams = struct {
|
||||||
if (value) |v| {
|
if (value) |v| {
|
||||||
params.transparency = try std.fmt.parseInt(u8, v, 10);
|
params.transparency = try std.fmt.parseInt(u8, v, 10);
|
||||||
}
|
}
|
||||||
|
} else if (std.mem.eql(u8, key, "t")) {
|
||||||
|
params.transparency = 150;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,7 +108,6 @@ test "parse multiple parameters" {
|
||||||
test "parse transparency" {
|
test "parse transparency" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
const params_t = try QueryParams.parse(allocator, "t");
|
const params_t = try QueryParams.parse(allocator, "t");
|
||||||
try std.testing.expect(params_t.transparency != null);
|
|
||||||
try std.testing.expectEqual(@as(u8, 150), params_t.transparency.?);
|
try std.testing.expectEqual(@as(u8, 150), params_t.transparency.?);
|
||||||
|
|
||||||
const params_custom = try QueryParams.parse(allocator, "transparency=200");
|
const params_custom = try QueryParams.parse(allocator, "transparency=200");
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,11 @@ const GeoIP = @This();
|
||||||
const log = std.log.scoped(.geoip);
|
const log = std.log.scoped(.geoip);
|
||||||
|
|
||||||
mmdb: *c.MMDB_s,
|
mmdb: *c.MMDB_s,
|
||||||
ip2location_client: *Ip2location,
|
ip2location_client: ?*Ip2location,
|
||||||
|
ip2location_cache: ?*Ip2location.Cache,
|
||||||
allocator: std.mem.Allocator,
|
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);
|
const path_z = try std.heap.c_allocator.dupeZ(u8, db_path);
|
||||||
defer std.heap.c_allocator.free(path_z);
|
defer std.heap.c_allocator.free(path_z);
|
||||||
|
|
||||||
|
|
@ -21,25 +22,32 @@ pub fn init(allocator: std.mem.Allocator, db_path: []const u8, api_key: ?[]const
|
||||||
errdefer allocator.destroy(mmdb);
|
errdefer allocator.destroy(mmdb);
|
||||||
|
|
||||||
const status = c.MMDB_open(path_z.ptr, c.MMDB_MODE_MMAP, 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;
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std.log.info(
|
var client: ?*Ip2location = null;
|
||||||
"IP2Location fallback: {s} (cache: {s})",
|
var cache: ?*Ip2location.Cache = null;
|
||||||
.{ if (api_key) |_| "key provided, 50k/mo limit" else "no key, 1k/day limit", cache_path },
|
|
||||||
);
|
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)", .{});
|
||||||
|
}
|
||||||
|
|
||||||
return GeoIP{
|
return GeoIP{
|
||||||
.mmdb = mmdb,
|
.mmdb = mmdb,
|
||||||
.ip2location_client = client,
|
.ip2location_client = client,
|
||||||
|
.ip2location_cache = cache,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -47,9 +55,14 @@ pub fn init(allocator: std.mem.Allocator, db_path: []const u8, api_key: ?[]const
|
||||||
pub fn deinit(self: *GeoIP) void {
|
pub fn deinit(self: *GeoIP) void {
|
||||||
c.MMDB_close(self.mmdb);
|
c.MMDB_close(self.mmdb);
|
||||||
self.allocator.destroy(self.mmdb);
|
self.allocator.destroy(self.mmdb);
|
||||||
self.ip2location_client.deinit();
|
if (self.ip2location_client) |client| {
|
||||||
log.debug("destroying client", .{});
|
client.deinit();
|
||||||
self.allocator.destroy(self.ip2location_client);
|
self.allocator.destroy(client);
|
||||||
|
}
|
||||||
|
if (self.ip2location_cache) |cache| {
|
||||||
|
cache.deinit();
|
||||||
|
self.allocator.destroy(cache);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lookup(self: *GeoIP, ip: []const u8) ?Coordinates {
|
pub fn lookup(self: *GeoIP, ip: []const u8) ?Coordinates {
|
||||||
|
|
@ -61,8 +74,11 @@ pub fn lookup(self: *GeoIP, ip: []const u8) ?Coordinates {
|
||||||
if (self.extractCoordinates(ip, result)) |coords|
|
if (self.extractCoordinates(ip, result)) |coords|
|
||||||
return coords;
|
return coords;
|
||||||
|
|
||||||
// Fallback to IP2Location
|
// Fallback to IP2Location if configured
|
||||||
return self.ip2location_client.lookup(ip);
|
if (self.ip2location_client) |client|
|
||||||
|
return client.lookupWithCache(ip, self.ip2location_cache);
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lookupInternal(mmdb: *c.MMDB_s, ip: []const u8) !c.MMDB_lookup_result_s {
|
fn lookupInternal(mmdb: *c.MMDB_s, ip: []const u8) !c.MMDB_lookup_result_s {
|
||||||
|
|
@ -145,11 +161,11 @@ test "MMDB functions are callable" {
|
||||||
}
|
}
|
||||||
|
|
||||||
test "GeoIP init with invalid path fails" {
|
test "GeoIP init with invalid path fails" {
|
||||||
const result = GeoIP.init(std.testing.allocator, "/nonexistent/path.mmdb", null, "");
|
const result = GeoIP.init(std.testing.allocator, "/nonexistent/path.mmdb", null, null);
|
||||||
try std.testing.expectError(error.CannotOpenDatabase, result);
|
try std.testing.expectError(error.CannotOpenDatabase, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "isUSIp detects US IPs" {
|
test "isUSIP detects US IPs" {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = std.testing.allocator;
|
||||||
const Config = @import("../Config.zig");
|
const Config = @import("../Config.zig");
|
||||||
const config = try Config.load(allocator);
|
const config = try Config.load(allocator);
|
||||||
|
|
@ -162,8 +178,9 @@ test "isUSIp detects US IPs" {
|
||||||
try GeoLite2.ensureDatabase(std.testing.allocator, db_path);
|
try GeoLite2.ensureDatabase(std.testing.allocator, db_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
var geoip = GeoIP.init(std.testing.allocator, db_path, null, config.ip2location_cache_file) catch
|
var geoip = GeoIP.init(std.testing.allocator, db_path, null, null) catch {
|
||||||
return error.SkipZigTest;
|
return error.SkipZigTest;
|
||||||
|
};
|
||||||
defer geoip.deinit();
|
defer geoip.deinit();
|
||||||
|
|
||||||
// Test that the function doesn't crash with various IPs
|
// Test that the function doesn't crash with various IPs
|
||||||
|
|
@ -186,8 +203,9 @@ test "lookup works" {
|
||||||
try GeoLite2.ensureDatabase(std.testing.allocator, db_path);
|
try GeoLite2.ensureDatabase(std.testing.allocator, db_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
var geoip = GeoIP.init(std.testing.allocator, db_path, null, config.ip2location_cache_file) catch
|
var geoip = GeoIP.init(std.testing.allocator, db_path, null, null) catch {
|
||||||
return error.SkipZigTest;
|
return error.SkipZigTest;
|
||||||
|
};
|
||||||
defer geoip.deinit();
|
defer geoip.deinit();
|
||||||
|
|
||||||
// Test that the function doesn't crash with various IPs
|
// Test that the function doesn't crash with various IPs
|
||||||
|
|
|
||||||
|
|
@ -7,31 +7,23 @@ const Self = @This();
|
||||||
const log = std.log.scoped(.ip2location);
|
const log = std.log.scoped(.ip2location);
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
api_key: ?[]const u8,
|
api_key: []const u8,
|
||||||
http_client: std.http.Client,
|
http_client: std.http.Client,
|
||||||
cache: *Cache,
|
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, api_key: ?[]const u8, cache_path: []const u8) !Self {
|
pub fn init(allocator: Allocator, api_key: []const u8) !Self {
|
||||||
const cache = try allocator.create(Cache);
|
|
||||||
errdefer allocator.destroy(cache);
|
|
||||||
cache.* = try .init(allocator, cache_path);
|
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.api_key = if (api_key) |k| try allocator.dupe(u8, k) else null,
|
.api_key = try allocator.dupe(u8, api_key),
|
||||||
.http_client = std.http.Client{ .allocator = allocator },
|
.http_client = std.http.Client{ .allocator = allocator },
|
||||||
.cache = cache,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.cache.deinit();
|
|
||||||
self.allocator.destroy(self.cache);
|
|
||||||
self.http_client.deinit();
|
self.http_client.deinit();
|
||||||
if (self.api_key) |k|
|
self.allocator.free(self.api_key);
|
||||||
self.allocator.free(k);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lookup(self: *Self, ip_str: []const u8) ?Coordinates {
|
pub fn lookupWithCache(self: *Self, ip_str: []const u8, cache: ?*Cache) ?Coordinates {
|
||||||
// Parse IP to u128 for cache lookup
|
// Parse IP to u128 for cache lookup
|
||||||
const addr = std.net.Address.parseIp(ip_str, 0) catch return null;
|
const addr = std.net.Address.parseIp(ip_str, 0) catch return null;
|
||||||
const ip_u128: u128 = switch (addr.any.family) {
|
const ip_u128: u128 = switch (addr.any.family) {
|
||||||
|
|
@ -42,39 +34,46 @@ pub fn lookup(self: *Self, ip_str: []const u8) ?Coordinates {
|
||||||
const family: u8 = if (addr.any.family == std.posix.AF.INET) 4 else 6;
|
const family: u8 = if (addr.any.family == std.posix.AF.INET) 4 else 6;
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
if (self.cache.get(ip_u128)) |coords|
|
if (cache) |c| {
|
||||||
|
if (c.get(ip_u128)) |coords| {
|
||||||
return coords;
|
return coords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch from API
|
// Fetch from API
|
||||||
const coords = self.fetch(ip_str) catch |err| {
|
const coords = self.lookup(ip_str) catch |err| {
|
||||||
log.err("API lookup failed: {}", .{err});
|
log.err("API lookup failed: {}", .{err});
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store in cache
|
// Store in cache
|
||||||
self.cache.put(ip_u128, family, coords) catch |err| {
|
if (cache) |c| {
|
||||||
|
c.put(ip_u128, family, coords) catch |err| {
|
||||||
log.warn("Failed to cache result: {}", .{err});
|
log.warn("Failed to cache result: {}", .{err});
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return coords;
|
return coords;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch(self: *Self, ip_str: []const u8) !Coordinates {
|
pub fn lookup(self: *Self, ip_str: []const u8) !Coordinates {
|
||||||
log.info("Fetching geolocation for IP {s}", .{ip_str});
|
log.info("Fetching geolocation for IP {s}", .{ip_str});
|
||||||
|
|
||||||
if (@import("builtin").is_test) return error.LookupUnavailableInUnitTest;
|
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
|
// Build URL: https://api.ip2location.io/?key=XXX&ip=1.2.3.4
|
||||||
try w.writeAll("https://api.ip2location.io/?ip=");
|
const url = try std.fmt.allocPrint(
|
||||||
try w.writeAll(ip_str);
|
self.allocator,
|
||||||
if (self.api_key) |key|
|
"https://api.ip2location.io/?key={s}&ip={s}",
|
||||||
try w.print("&key={s}", .{key});
|
.{ self.api_key, ip_str },
|
||||||
|
);
|
||||||
|
defer self.allocator.free(url);
|
||||||
|
|
||||||
|
const uri = try std.Uri.parse(url);
|
||||||
|
|
||||||
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 self.http_client.fetch(.{
|
||||||
.location = .{ .url = w.buffered() },
|
.location = .{ .uri = uri },
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
.response_writer = &writer,
|
.response_writer = &writer,
|
||||||
});
|
});
|
||||||
|
|
@ -98,18 +97,7 @@ fn fetch(self: *Self, ip_str: []const u8) !Coordinates {
|
||||||
const obj = parsed.value.object;
|
const obj = parsed.value.object;
|
||||||
const lat = obj.get("latitude") orelse return error.MissingLatitude;
|
const lat = obj.get("latitude") orelse return error.MissingLatitude;
|
||||||
const lon = obj.get("longitude") orelse return error.MissingLongitude;
|
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{
|
return Coordinates{
|
||||||
.latitude = @floatCast(lat.float),
|
.latitude = @floatCast(lat.float),
|
||||||
.longitude = @floatCast(lon.float),
|
.longitude = @floatCast(lon.float),
|
||||||
|
|
@ -143,7 +131,6 @@ pub const Cache = struct {
|
||||||
.entries = std.AutoHashMap(u128, Coordinates).init(allocator),
|
.entries = std.AutoHashMap(u128, Coordinates).init(allocator),
|
||||||
.file = null,
|
.file = null,
|
||||||
};
|
};
|
||||||
errdefer allocator.free(cache.path);
|
|
||||||
|
|
||||||
// Try to open existing cache file
|
// Try to open existing cache file
|
||||||
if (std.fs.openFileAbsolute(path, .{ .mode = .read_write })) |file| {
|
if (std.fs.openFileAbsolute(path, .{ .mode = .read_write })) |file| {
|
||||||
|
|
|
||||||
|
|
@ -254,8 +254,9 @@ test "resolve IP address with GeoIP" {
|
||||||
try GeoLite2.ensureDatabase(allocator, config.geolite_path);
|
try GeoLite2.ensureDatabase(allocator, config.geolite_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
var geoip = GeoIp.init(allocator, config.geolite_path, null, config.ip2location_cache_file) catch
|
var geoip = GeoIp.init(allocator, config.geolite_path, null, null) catch {
|
||||||
return error.SkipZigTest;
|
return error.SkipZigTest;
|
||||||
|
};
|
||||||
defer geoip.deinit();
|
defer geoip.deinit();
|
||||||
|
|
||||||
var geocache = try GeoCache.init(allocator, null);
|
var geocache = try GeoCache.init(allocator, null);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ pub fn main() !u8 {
|
||||||
allocator,
|
allocator,
|
||||||
cfg.geolite_path,
|
cfg.geolite_path,
|
||||||
cfg.ip2location_api_key,
|
cfg.ip2location_api_key,
|
||||||
cfg.ip2location_cache_file,
|
if (cfg.ip2location_api_key != null) cfg.ip2location_cache_file else null,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
std.log.err("Failed to load GeoIP database from {s}: {}", .{ cfg.geolite_path, err });
|
std.log.err("Failed to load GeoIP database from {s}: {}", .{ cfg.geolite_path, err });
|
||||||
return err;
|
return err;
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,6 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, options: Re
|
||||||
defer output.deinit();
|
defer output.deinit();
|
||||||
|
|
||||||
const w = &output.writer;
|
const w = &output.writer;
|
||||||
if (options.format == .html) try w.writeAll("<pre>");
|
|
||||||
if (!options.no_caption)
|
if (!options.no_caption)
|
||||||
try w.print("Weather report: {s}\n\n", .{data.locationDisplayName()});
|
try w.print("Weather report: {s}\n\n", .{data.locationDisplayName()});
|
||||||
|
|
||||||
|
|
@ -123,7 +122,6 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, options: Re
|
||||||
try renderForecastDay(w, day, options);
|
try renderForecastDay(w, day, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options.format == .html) try w.writeAll("</pre>");
|
|
||||||
|
|
||||||
return output.toOwnedSlice();
|
return output.toOwnedSlice();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue