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)}")