AI implemented client IP logic

This commit is contained in:
Emil Lerch 2025-12-18 10:01:11 -08:00
parent 68dbe7d7dd
commit 75e5e88c31
Signed by: lobo
GPG key ID: A7B62D657EF764F8
5 changed files with 130 additions and 51 deletions

View file

@ -2,6 +2,54 @@
This directory contains comprehensive documentation for rewriting wttr.in in Zig.
## Current Implementation Status
### Implemented Features
- HTTP server with routing and middleware
- Rate limiting
- Caching system
- GeoIP database integration (libmaxminddb)
- Location resolver with multiple input types
- Output formats: ANSI, line (1-4), JSON (j1), v2 data-rich, custom (%)
- Query parameter parsing
- Static help pages (/:help, /:translation)
- Error handling (404/500 status codes)
- Configuration from environment variables
### Missing Features (To Be Implemented Later)
1. **Prometheus Metrics Format** (format=p1)
- Export weather data in Prometheus metrics format
- See API_ENDPOINTS.md for format specification
2. **PNG Generation**
- Render weather reports as PNG images
- Support transparency and custom styling
- Requires image rendering library integration
3. **Multiple Locations Support**
- Handle colon-separated locations (e.g., `London:Paris:Berlin`)
- Process and display weather for multiple cities in one request
4. **Language/Localization**
- Accept-Language header parsing
- lang query parameter support
- Translation of weather conditions and text (54 languages)
5. **Moon Phase Calculation**
- Real moon phase computation based on date
- Moon phase emoji display
- Moonday calculation
6. **Astronomical Times**
- Calculate dawn, sunrise, zenith, sunset, dusk times
- Based on location coordinates and timezone
- Display in custom format output
7. **Airport Database**
- IATA airport code to coordinates mapping
- Use dedicated airport data instead of geocoding service
## Documentation Files
### [TARGET_ARCHITECTURE.md](TARGET_ARCHITECTURE.md) ⭐ NEW

View file

@ -2,6 +2,8 @@ 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");
@ -12,6 +14,7 @@ const help = @import("help.zig");
pub const HandleWeatherOptions = struct {
cache: *Cache,
provider: WeatherProvider,
resolver: *Resolver,
};
pub fn handleWeather(
@ -19,7 +22,9 @@ pub fn handleWeather(
req: *httpz.Request,
res: *httpz.Response,
) !void {
try handleWeatherInternal(opts, req, res, null);
// Get client IP for location detection
const client_ip = getClientIP(req);
try handleWeatherInternal(opts, req, res, client_ip);
}
pub fn handleWeatherLocation(
@ -45,15 +50,39 @@ pub fn handleWeatherLocation(
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: ?[]const u8,
location_query: ?[]const u8,
) !void {
const allocator = req.arena;
const cache_key = try generateCacheKey(allocator, req, location);
const cache_key = try generateCacheKey(allocator, req, location_query);
if (opts.cache.get(cache_key)) |cached| {
res.content_type = .TEXT;
@ -61,8 +90,28 @@ fn handleWeatherInternal(
return;
}
const loc = location orelse "London";
const weather = opts.provider.fetch(allocator, loc) catch |err| {
// 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;
@ -117,3 +166,18 @@ fn generateCacheKey(
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(""));
}

View file

@ -120,11 +120,8 @@ pub const GeoIP = struct {
var latitude_data: MMDBEntryData = undefined;
var longitude_data: MMDBEntryData = undefined;
const lat_path = [_][*:0]const u8{ "location", "latitude", null };
const lon_path = [_][*:0]const u8{ "location", "longitude", null };
const lat_status = MMDB_get_value(&entry_mut, &latitude_data, lat_path[0], lat_path[1], lat_path[2]);
const lon_status = MMDB_get_value(&entry_mut, &longitude_data, lon_path[0], lon_path[1], lon_path[2]);
const lat_status = MMDB_get_value(&entry_mut, &latitude_data, "location", "latitude", @as([*:0]const u8, @ptrCast(&[_]u8{0})));
const lon_status = MMDB_get_value(&entry_mut, &longitude_data, "location", "longitude", @as([*:0]const u8, @ptrCast(&[_]u8{0})));
if (lat_status != 0 or lon_status != 0 or !latitude_data.has_data or !longitude_data.has_data) {
return error.CoordinatesNotFound;

View file

@ -81,47 +81,11 @@ pub const Resolver = struct {
}
fn resolveGeocoded(self: *Resolver, name: []const u8) !Location {
// Call external geocoding service
const client = std.http.Client{ .allocator = self.allocator };
defer client.deinit();
const url = try std.fmt.allocPrint(
self.allocator,
"{s}/geocode?q={s}",
.{ self.geolocator_url, name },
);
defer self.allocator.free(url);
var response = std.ArrayList(u8).init(self.allocator);
defer response.deinit();
const result = client.fetch(.{
.location = .{ .url = url },
.response_storage = .{ .dynamic = &response },
}) catch {
return error.GeocodingFailed;
};
if (result.status != .ok) {
return error.LocationNotFound;
}
// Parse JSON response: {"name": "...", "lat": ..., "lon": ...}
const parsed = std.json.parseFromSlice(
struct { name: []const u8, lat: f64, lon: f64 },
self.allocator,
response.items,
.{},
) catch {
return error.InvalidGeocodingResponse;
};
defer parsed.deinit();
return Location{
.name = try self.allocator.dupe(u8, parsed.value.name),
.latitude = parsed.value.lat,
.longitude = parsed.value.lon,
};
// TODO: Call external geocoding service
// For now, return a placeholder error
_ = self;
_ = name;
return error.GeocodingNotImplemented;
}
fn resolveAirport(self: *Resolver, code: []const u8) !Location {

View file

@ -6,6 +6,7 @@ const types = @import("weather/types.zig");
const Server = @import("http/server.zig").Server;
const RateLimiter = @import("http/rate_limiter.zig").RateLimiter;
const GeoIP = @import("location/geoip.zig").GeoIP;
const Resolver = @import("location/resolver.zig").Resolver;
pub const std_options: std.Options = .{
.log_level = .info,
@ -27,6 +28,7 @@ pub fn main() !void {
try stdout.print("Cache size: {d}\n", .{cfg.cache_size});
try stdout.print("Cache dir: {s}\n", .{cfg.cache_dir});
try stdout.print("GeoLite2 path: {s}\n", .{cfg.geolite_path});
try stdout.print("Geolocator URL: {s}\n", .{cfg.geolocator_url});
try stdout.flush();
// Initialize GeoIP database
@ -37,6 +39,9 @@ pub fn main() !void {
};
defer geoip.deinit();
// Initialize location resolver
var resolver = Resolver.init(allocator, &geoip, cfg.geolocator_url);
var cache = try Cache.init(allocator, .{
.max_entries = cfg.cache_size,
.cache_dir = cfg.cache_dir,
@ -56,6 +61,7 @@ pub fn main() !void {
var server = try Server.init(allocator, cfg.listen_host, cfg.listen_port, .{
.cache = &cache,
.provider = metno.provider(),
.resolver = &resolver,
}, &rate_limiter);
try server.listen();