AI implemented client IP logic
This commit is contained in:
parent
68dbe7d7dd
commit
75e5e88c31
5 changed files with 130 additions and 51 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(""));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue