wttr/zig/src/http/handler.zig

183 lines
5.6 KiB
Zig

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(""));
}