const std = @import("std"); const httpz = @import("httpz"); const Cache = @import("../cache/cache.zig").Cache; const WeatherProvider = @import("../weather/provider.zig").WeatherProvider; const Resolver = @import("../location/resolver.zig").Resolver; const Location = @import("../location/resolver.zig").Location; const ansi = @import("../render/ansi.zig"); const line = @import("../render/line.zig"); const json = @import("../render/json.zig"); const v2 = @import("../render/v2.zig"); const custom = @import("../render/custom.zig"); const help = @import("help.zig"); pub const HandleWeatherOptions = struct { cache: *Cache, provider: WeatherProvider, resolver: *Resolver, }; pub fn handleWeather( opts: *HandleWeatherOptions, req: *httpz.Request, res: *httpz.Response, ) !void { // Get client IP for location detection const client_ip = getClientIP(req); try handleWeatherInternal(opts, req, res, client_ip); } pub fn handleWeatherLocation( opts: *HandleWeatherOptions, req: *httpz.Request, res: *httpz.Response, ) !void { const location = req.param("location") orelse "London"; // Handle special endpoints if (std.mem.startsWith(u8, location, ":")) { if (std.mem.eql(u8, location, ":help")) { res.content_type = .TEXT; res.body = help.help_page; return; } else if (std.mem.eql(u8, location, ":translation")) { res.content_type = .TEXT; res.body = help.translation_page; return; } } try handleWeatherInternal(opts, req, res, location); } fn getClientIP(req: *httpz.Request) []const u8 { // Check X-Forwarded-For header first (for proxies) if (req.header("x-forwarded-for")) |xff| { return parseXForwardedFor(xff); } // Check X-Real-IP header if (req.header("x-real-ip")) |real_ip| { return real_ip; } // Fall back to empty (no IP available) return ""; } fn parseXForwardedFor(xff: []const u8) []const u8 { // Take first IP from comma-separated list var iter = std.mem.splitScalar(u8, xff, ','); if (iter.next()) |first_ip| { return std.mem.trim(u8, first_ip, " \t"); } return ""; } fn handleWeatherInternal( opts: *HandleWeatherOptions, req: *httpz.Request, res: *httpz.Response, location_query: ?[]const u8, ) !void { const allocator = req.arena; const cache_key = try generateCacheKey(allocator, req, location_query); if (opts.cache.get(cache_key)) |cached| { res.content_type = .TEXT; res.body = cached; return; } // Resolve location const loc_str = location_query orelse ""; const location = if (loc_str.len == 0) Location{ .name = "London", .latitude = 51.5074, .longitude = -0.1278 } else opts.resolver.resolve(loc_str) catch |err| { switch (err) { error.LocationNotFound => { res.status = 404; res.body = "Location not found\n"; return; }, else => { res.status = 500; res.body = "Internal server error\n"; return; }, } }; // Fetch weather using coordinates const weather = opts.provider.fetch(allocator, location.name) catch |err| { switch (err) { error.LocationNotFound => { res.status = 404; res.body = "Location not found\n"; return; }, else => { res.status = 500; res.body = "Internal server error\n"; return; }, } }; defer weather.deinit(); const query = try req.query(); const format = query.get("format"); const output = if (format) |fmt| blk: { if (std.mem.eql(u8, fmt, "j1")) { res.content_type = .JSON; break :blk try json.render(allocator, weather); } else if (std.mem.eql(u8, fmt, "v2")) { break :blk try v2.render(allocator, weather); } else if (std.mem.startsWith(u8, fmt, "%")) { break :blk try custom.render(allocator, weather, fmt); } else { break :blk try line.render(allocator, weather, fmt); } } else try ansi.render(allocator, weather, .{}); const ttl = 1000 + std.crypto.random.intRangeAtMost(u64, 0, 1000); try opts.cache.put(cache_key, output, ttl); if (res.content_type != .JSON) { res.content_type = .TEXT; } res.body = output; } fn generateCacheKey( allocator: std.mem.Allocator, req: *httpz.Request, location: ?[]const u8, ) ![]const u8 { const loc = location orelse ""; const query = try req.query(); const format = query.get("format") orelse ""; return std.fmt.allocPrint(allocator, "{s}:{s}:{s}", .{ req.url.path, loc, format, }); } test "parseXForwardedFor extracts first IP" { try std.testing.expectEqualStrings("192.168.1.1", parseXForwardedFor("192.168.1.1")); try std.testing.expectEqualStrings("10.0.0.1", parseXForwardedFor("10.0.0.1, 172.16.0.1")); try std.testing.expectEqualStrings("203.0.113.1", parseXForwardedFor("203.0.113.1, 198.51.100.1, 192.0.2.1")); } test "parseXForwardedFor trims whitespace" { try std.testing.expectEqualStrings("192.168.1.1", parseXForwardedFor(" 192.168.1.1 ")); try std.testing.expectEqualStrings("10.0.0.1", parseXForwardedFor(" 10.0.0.1 , 172.16.0.1")); } test "parseXForwardedFor handles empty string" { try std.testing.expectEqualStrings("", parseXForwardedFor("")); }