import type { CityResponse, Reader } from "maxmind"; import { open } from "maxmind"; export interface GeoInfo { ip: string | null; city: string | null; region: string | null; country: string | null; } /** Default path for GeoLite2-City database */ const DEFAULT_GEOIP_PATH = "/usr/share/GeoIP/GeoLite2-City.mmdb"; // Module-level reader instance let geoReader: Reader | null = null; let initialized = false; /** * Initialize the GeoIP database reader. * Uses GEOIP_DATABASE_PATH env var, defaults to /usr/share/GeoIP/GeoLite2-City.mmdb */ export const initGeoReader = async (): Promise => { if (initialized) { return; } const dbPath = Bun.env.GEOIP_DATABASE_PATH ?? DEFAULT_GEOIP_PATH; try { geoReader = await open(dbPath); } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; console.warn(`GeoIP database not available at ${dbPath}: ${message}`); } initialized = true; }; // ===== Test Helpers ===== // These are exported for testing only. Do not use in production code. /** @internal Reset state for testing */ export const _resetForTesting = (): void => { geoReader = null; initialized = false; }; /** @internal Set a mock reader for testing */ export const _setReaderForTesting = ( reader: Reader | null, ): void => { geoReader = reader; initialized = true; }; // ===== Public API ===== /** * Extract the client IP address from request headers. * Checks headers in priority order: * 1. CF-Connecting-IP (Cloudflare) * 2. True-Client-IP (Cloudflare Enterprise / Akamai) * 3. X-Real-IP (nginx, other proxies) * 4. X-Forwarded-For (first IP in chain) * 5. X-Client-IP (some proxies) */ export const extractClientIP = (headers: Headers): string | null => { const cfIP = headers.get("CF-Connecting-IP"); if (cfIP) { return cfIP.trim(); } const trueClientIP = headers.get("True-Client-IP"); if (trueClientIP) { return trueClientIP.trim(); } const realIP = headers.get("X-Real-IP"); if (realIP) { return realIP.trim(); } const forwardedFor = headers.get("X-Forwarded-For"); if (forwardedFor) { const firstIP = forwardedFor.split(",")[0]?.trim(); if (firstIP) { return firstIP; } } const clientIP = headers.get("X-Client-IP"); if (clientIP) { return clientIP.trim(); } return null; }; /** * Look up geo information from an IP address using the GeoIP database. * Returns null values if the database isn't initialized or IP is unknown. */ export const lookupGeoFromIP = ( ip: string, ): { city: string | null; region: string | null; country: string | null } => { if (!geoReader) { return { city: null, region: null, country: null }; } try { const result = geoReader.get(ip); if (!result) { return { city: null, region: null, country: null }; } return { city: result.city?.names.en ?? null, region: result.subdivisions?.[0]?.names.en ?? null, country: result.country?.iso_code ?? null, }; } catch { return { city: null, region: null, country: null }; } }; /** * Extract geolocation info from request headers. * Uses Cloudflare headers when available, falls back to GeoIP database lookup. */ export const getGeoInfo = (headers: Headers): GeoInfo => { const ip = extractClientIP(headers); // Try Cloudflare geo headers first const cfCountry = headers.get("CF-IPCountry"); const cfCity = headers.get("CF-IPCity"); const cfRegion = headers.get("CF-Region"); if (cfCountry || cfCity || cfRegion) { return { ip, city: cfCity ?? null, region: cfRegion ?? null, country: cfCountry ?? null, }; } // Fall back to GeoIP database lookup if (ip) { return { ip, ...lookupGeoFromIP(ip) }; } return { ip: null, city: null, region: null, country: null }; }; /** * Extract User-Agent from request headers. */ export const getUserAgent = (headers: Headers): string => { return headers.get("User-Agent") ?? "Unknown"; };