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.
|
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
|
## Documentation Files
|
||||||
|
|
||||||
### [TARGET_ARCHITECTURE.md](TARGET_ARCHITECTURE.md) ⭐ NEW
|
### [TARGET_ARCHITECTURE.md](TARGET_ARCHITECTURE.md) ⭐ NEW
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ const std = @import("std");
|
||||||
const httpz = @import("httpz");
|
const httpz = @import("httpz");
|
||||||
const Cache = @import("../cache/cache.zig").Cache;
|
const Cache = @import("../cache/cache.zig").Cache;
|
||||||
const WeatherProvider = @import("../weather/provider.zig").WeatherProvider;
|
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 ansi = @import("../render/ansi.zig");
|
||||||
const line = @import("../render/line.zig");
|
const line = @import("../render/line.zig");
|
||||||
const json = @import("../render/json.zig");
|
const json = @import("../render/json.zig");
|
||||||
|
|
@ -12,6 +14,7 @@ const help = @import("help.zig");
|
||||||
pub const HandleWeatherOptions = struct {
|
pub const HandleWeatherOptions = struct {
|
||||||
cache: *Cache,
|
cache: *Cache,
|
||||||
provider: WeatherProvider,
|
provider: WeatherProvider,
|
||||||
|
resolver: *Resolver,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn handleWeather(
|
pub fn handleWeather(
|
||||||
|
|
@ -19,7 +22,9 @@ pub fn handleWeather(
|
||||||
req: *httpz.Request,
|
req: *httpz.Request,
|
||||||
res: *httpz.Response,
|
res: *httpz.Response,
|
||||||
) !void {
|
) !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(
|
pub fn handleWeatherLocation(
|
||||||
|
|
@ -45,15 +50,39 @@ pub fn handleWeatherLocation(
|
||||||
try handleWeatherInternal(opts, req, res, location);
|
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(
|
fn handleWeatherInternal(
|
||||||
opts: *HandleWeatherOptions,
|
opts: *HandleWeatherOptions,
|
||||||
req: *httpz.Request,
|
req: *httpz.Request,
|
||||||
res: *httpz.Response,
|
res: *httpz.Response,
|
||||||
location: ?[]const u8,
|
location_query: ?[]const u8,
|
||||||
) !void {
|
) !void {
|
||||||
const allocator = req.arena;
|
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| {
|
if (opts.cache.get(cache_key)) |cached| {
|
||||||
res.content_type = .TEXT;
|
res.content_type = .TEXT;
|
||||||
|
|
@ -61,8 +90,28 @@ fn handleWeatherInternal(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loc = location orelse "London";
|
// Resolve location
|
||||||
const weather = opts.provider.fetch(allocator, loc) catch |err| {
|
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) {
|
switch (err) {
|
||||||
error.LocationNotFound => {
|
error.LocationNotFound => {
|
||||||
res.status = 404;
|
res.status = 404;
|
||||||
|
|
@ -117,3 +166,18 @@ fn generateCacheKey(
|
||||||
format,
|
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 latitude_data: MMDBEntryData = undefined;
|
||||||
var longitude_data: MMDBEntryData = undefined;
|
var longitude_data: MMDBEntryData = undefined;
|
||||||
|
|
||||||
const lat_path = [_][*:0]const u8{ "location", "latitude", null };
|
const lat_status = MMDB_get_value(&entry_mut, &latitude_data, "location", "latitude", @as([*:0]const u8, @ptrCast(&[_]u8{0})));
|
||||||
const lon_path = [_][*:0]const u8{ "location", "longitude", null };
|
const lon_status = MMDB_get_value(&entry_mut, &longitude_data, "location", "longitude", @as([*:0]const u8, @ptrCast(&[_]u8{0})));
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
if (lat_status != 0 or lon_status != 0 or !latitude_data.has_data or !longitude_data.has_data) {
|
if (lat_status != 0 or lon_status != 0 or !latitude_data.has_data or !longitude_data.has_data) {
|
||||||
return error.CoordinatesNotFound;
|
return error.CoordinatesNotFound;
|
||||||
|
|
|
||||||
|
|
@ -81,47 +81,11 @@ pub const Resolver = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolveGeocoded(self: *Resolver, name: []const u8) !Location {
|
fn resolveGeocoded(self: *Resolver, name: []const u8) !Location {
|
||||||
// Call external geocoding service
|
// TODO: Call external geocoding service
|
||||||
const client = std.http.Client{ .allocator = self.allocator };
|
// For now, return a placeholder error
|
||||||
defer client.deinit();
|
_ = self;
|
||||||
|
_ = name;
|
||||||
const url = try std.fmt.allocPrint(
|
return error.GeocodingNotImplemented;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolveAirport(self: *Resolver, code: []const u8) !Location {
|
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 Server = @import("http/server.zig").Server;
|
||||||
const RateLimiter = @import("http/rate_limiter.zig").RateLimiter;
|
const RateLimiter = @import("http/rate_limiter.zig").RateLimiter;
|
||||||
const GeoIP = @import("location/geoip.zig").GeoIP;
|
const GeoIP = @import("location/geoip.zig").GeoIP;
|
||||||
|
const Resolver = @import("location/resolver.zig").Resolver;
|
||||||
|
|
||||||
pub const std_options: std.Options = .{
|
pub const std_options: std.Options = .{
|
||||||
.log_level = .info,
|
.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 size: {d}\n", .{cfg.cache_size});
|
||||||
try stdout.print("Cache dir: {s}\n", .{cfg.cache_dir});
|
try stdout.print("Cache dir: {s}\n", .{cfg.cache_dir});
|
||||||
try stdout.print("GeoLite2 path: {s}\n", .{cfg.geolite_path});
|
try stdout.print("GeoLite2 path: {s}\n", .{cfg.geolite_path});
|
||||||
|
try stdout.print("Geolocator URL: {s}\n", .{cfg.geolocator_url});
|
||||||
try stdout.flush();
|
try stdout.flush();
|
||||||
|
|
||||||
// Initialize GeoIP database
|
// Initialize GeoIP database
|
||||||
|
|
@ -37,6 +39,9 @@ pub fn main() !void {
|
||||||
};
|
};
|
||||||
defer geoip.deinit();
|
defer geoip.deinit();
|
||||||
|
|
||||||
|
// Initialize location resolver
|
||||||
|
var resolver = Resolver.init(allocator, &geoip, cfg.geolocator_url);
|
||||||
|
|
||||||
var cache = try Cache.init(allocator, .{
|
var cache = try Cache.init(allocator, .{
|
||||||
.max_entries = cfg.cache_size,
|
.max_entries = cfg.cache_size,
|
||||||
.cache_dir = cfg.cache_dir,
|
.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, .{
|
var server = try Server.init(allocator, cfg.listen_host, cfg.listen_port, .{
|
||||||
.cache = &cache,
|
.cache = &cache,
|
||||||
.provider = metno.provider(),
|
.provider = metno.provider(),
|
||||||
|
.resolver = &resolver,
|
||||||
}, &rate_limiter);
|
}, &rate_limiter);
|
||||||
|
|
||||||
try server.listen();
|
try server.listen();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue