Spaces:
Sleeping
Sleeping
Fix geolocation to resolve public IP for localhost/LAN clients
Browse filesWhen the client IP is private (127.0.0.1, 192.168.x.x, etc.), the
geolocation endpoint now discovers the server's public IP via
api.ipify.org before querying ip-api.com. This fixes the issue where
local development always fell back to New York.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- backend/api/geolocation.py +29 -2
backend/api/geolocation.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import math
|
| 6 |
|
| 7 |
import httpx
|
|
@@ -12,7 +13,7 @@ from ..data_loader import get_route_graph
|
|
| 12 |
|
| 13 |
router = APIRouter(prefix="/api/geolocation", tags=["geolocation"])
|
| 14 |
|
| 15 |
-
# Fallback for
|
| 16 |
_FALLBACK = {
|
| 17 |
"country_code": "US",
|
| 18 |
"city": "New York",
|
|
@@ -22,6 +23,14 @@ _FALLBACK = {
|
|
| 22 |
}
|
| 23 |
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
| 26 |
"""Great-circle distance in km between two lat/lon points."""
|
| 27 |
r = 6371.0
|
|
@@ -52,6 +61,21 @@ def _nearest_airports(lat: float, lon: float, limit: int = 3) -> list[dict]:
|
|
| 52 |
]
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
@router.get("")
|
| 56 |
async def geolocate(request: Request):
|
| 57 |
"""Resolve client IP to location, currency, and nearest airports."""
|
|
@@ -60,10 +84,13 @@ async def geolocate(request: Request):
|
|
| 60 |
if not client_ip:
|
| 61 |
client_ip = request.client.host if request.client else "127.0.0.1"
|
| 62 |
|
|
|
|
|
|
|
|
|
|
| 63 |
# Try ip-api.com (free, no key, 45 req/min)
|
| 64 |
try:
|
| 65 |
async with httpx.AsyncClient(timeout=3.0) as client:
|
| 66 |
-
resp = await client.get(f"http://ip-api.com/json/{
|
| 67 |
data = resp.json()
|
| 68 |
if data.get("status") == "success":
|
| 69 |
country_code = data.get("countryCode", "US")
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
+
import ipaddress
|
| 6 |
import math
|
| 7 |
|
| 8 |
import httpx
|
|
|
|
| 13 |
|
| 14 |
router = APIRouter(prefix="/api/geolocation", tags=["geolocation"])
|
| 15 |
|
| 16 |
+
# Fallback for truly unresolvable cases
|
| 17 |
_FALLBACK = {
|
| 18 |
"country_code": "US",
|
| 19 |
"city": "New York",
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
|
| 26 |
+
def _is_private_ip(ip_str: str) -> bool:
|
| 27 |
+
"""Check if an IP address is private/loopback/reserved."""
|
| 28 |
+
try:
|
| 29 |
+
return ipaddress.ip_address(ip_str).is_private
|
| 30 |
+
except ValueError:
|
| 31 |
+
return True
|
| 32 |
+
|
| 33 |
+
|
| 34 |
def _haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
| 35 |
"""Great-circle distance in km between two lat/lon points."""
|
| 36 |
r = 6371.0
|
|
|
|
| 61 |
]
|
| 62 |
|
| 63 |
|
| 64 |
+
async def _resolve_ip(client_ip: str) -> str:
|
| 65 |
+
"""If client_ip is private (localhost, LAN), discover the public IP."""
|
| 66 |
+
if not _is_private_ip(client_ip):
|
| 67 |
+
return client_ip
|
| 68 |
+
# Ask ip-api.com without an IP — it returns the server's public IP info
|
| 69 |
+
# Or use a lightweight service to get the public IP first
|
| 70 |
+
try:
|
| 71 |
+
async with httpx.AsyncClient(timeout=3.0) as client:
|
| 72 |
+
resp = await client.get("https://api.ipify.org?format=json")
|
| 73 |
+
data = resp.json()
|
| 74 |
+
return data.get("ip", client_ip)
|
| 75 |
+
except Exception:
|
| 76 |
+
return client_ip
|
| 77 |
+
|
| 78 |
+
|
| 79 |
@router.get("")
|
| 80 |
async def geolocate(request: Request):
|
| 81 |
"""Resolve client IP to location, currency, and nearest airports."""
|
|
|
|
| 84 |
if not client_ip:
|
| 85 |
client_ip = request.client.host if request.client else "127.0.0.1"
|
| 86 |
|
| 87 |
+
# If IP is private (localhost/LAN), resolve to public IP
|
| 88 |
+
lookup_ip = await _resolve_ip(client_ip)
|
| 89 |
+
|
| 90 |
# Try ip-api.com (free, no key, 45 req/min)
|
| 91 |
try:
|
| 92 |
async with httpx.AsyncClient(timeout=3.0) as client:
|
| 93 |
+
resp = await client.get(f"http://ip-api.com/json/{lookup_ip}?fields=status,country,countryCode,city,lat,lon")
|
| 94 |
data = resp.json()
|
| 95 |
if data.get("status") == "success":
|
| 96 |
country_code = data.get("countryCode", "US")
|