Spaces:
Running on Zero
Running on Zero
File size: 5,206 Bytes
9b715b4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | """Coarse, server-side geolocation of a visitor's IP, for analytics.
Nothing here touches the browser: no navigator.geolocation, no permission
prompt, no consent dialog to dismiss. The caller hands over the IP it
already has from the request and gets back city-level detail or nothing.
`lookup` is deliberately total -- it returns {} for anything it cannot
answer rather than raising, because a failed analytics lookup must never
break an image generation.
"""
import ipaddress
import json
import os
import ssl
import urllib.error
import urllib.request
# ipwho.is: free, no API key, HTTPS, ~10k lookups/month, and it takes the
# target IP in the path so no query string or signup is involved. Picked
# over ipapi.co (1k/day and already returning 429 unauthenticated), ip-api
# .com (HTTPS is paid-only) and freeipapi.com (redirects away from its
# documented endpoint). Failures are signalled in-band as {"success": false}
# with HTTP 200, so the status code alone is not enough to trust a response.
API_URL = os.environ.get("GEO_API_URL", "https://ipwho.is").rstrip("/")
# Runs inside a user-facing request, so the budget is "cheap enough to be
# invisible". Two seconds is already generous for a single GET.
TIMEOUT = float(os.environ.get("GEO_TIMEOUT", "2"))
ENABLED = os.environ.get("GEO_LOOKUP", "on").strip().lower() != "off"
# Visitors arrive in bursts from the same address, and geolocation of an IP
# does not meaningfully change during one container's lifetime. Only hits
# are cached: a timeout or a rate-limit is transient and caching {} would
# blind the Space for as long as the process lives.
_cache: dict[str, dict] = {}
_CACHE_MAX = 4096
_HEADERS = {"User-Agent": "diffusiondb-sd15-lora-space/1.0"}
# The Space image trusts the system CA store, but a python.org interpreter on
# macOS ships none and every HTTPS call there fails verification. certifi is
# already installed as a transitive dependency of huggingface_hub, so prefer
# its bundle when it is importable; never fall back to an unverified context.
try:
import certifi
_SSL = ssl.create_default_context(cafile=certifi.where())
except Exception:
_SSL = ssl.create_default_context()
def _is_public(ip: str) -> bool:
"""True only for addresses worth spending a network call on."""
try:
address = ipaddress.ip_address(ip)
except ValueError: # not an IP at all, or carries a %scope suffix
return False
return not (
address.is_private
or address.is_loopback
or address.is_link_local
or address.is_reserved
or address.is_multicast
or address.is_unspecified
)
def _fetch(ip: str) -> dict:
request = urllib.request.Request(f"{API_URL}/{ip}", headers=_HEADERS)
with urllib.request.urlopen(
request, timeout=TIMEOUT, context=_SSL
) as response:
# Cap the read: a wrong GEO_API_URL could point at anything.
return json.loads(response.read(64_000).decode("utf-8"))
def _parse(payload: dict) -> dict:
if not isinstance(payload, dict) or payload.get("success") is False:
return {}
# Nested under "timezone" as an object, unlike every other field.
timezone = payload.get("timezone")
if isinstance(timezone, dict):
timezone = timezone.get("id")
result = {
"country": payload.get("country"),
"country_code": payload.get("country_code"),
"region": payload.get("region"),
"city": payload.get("city"),
"latitude": float(payload["latitude"]),
"longitude": float(payload["longitude"]),
"timezone": timezone,
}
# An answer with no coordinates or no country is not an answer; better
# to report "unknown" than to record a row of Nones.
if result["country"] is None:
return {}
return result
def lookup(ip: str) -> dict:
"""Return {} on any failure, or a dict with these exact keys:
'country' e.g. 'United States'
'country_code' e.g. 'US'
'region' the state/province, e.g. 'California'
'city' e.g. 'San Francisco'
'latitude' float
'longitude' float
'timezone' e.g. 'America/Los_Angeles'
"""
# isinstance, not just falsiness: "never raises" has to hold even when a
# caller passes whatever its framework handed it rather than a str.
if not ENABLED or not ip or not isinstance(ip, str):
return {}
ip = ip.strip()
if not _is_public(ip):
return {}
if ip in _cache:
return _cache[ip]
try:
result = _parse(_fetch(ip))
except Exception:
# Timeout, DNS failure, HTTP error, truncated body, non-JSON,
# missing or non-numeric coordinates -- all of it means the same
# thing to the caller, and none of it is worth a traceback.
return {}
if result:
if len(_cache) >= _CACHE_MAX:
_cache.clear() # crude, but bounded and never wrong
_cache[ip] = result
return result
if __name__ == "__main__":
for probe in ("8.8.8.8", "1.1.1.1", "2606:4700:4700::1111",
"192.168.1.1", "127.0.0.1", "", "not-an-ip"):
print(f"{probe or '(empty)':>22} {lookup(probe)}")
|