diff --git a/serve.py b/serve.py index fe1b5b0..943db3b 100644 --- a/serve.py +++ b/serve.py @@ -36,6 +36,15 @@ UPSTREAMS = { # may serve a stale cache rather than nothing. OPENSKY_CACHE_FRESH_SEC = 120 +# OpenSky OAuth2 (client-credentials) → 4000 credits/day vs 400 anonymous. The +# clientId/clientSecret live in gitignored openskycredentials.json; a 30-min +# bearer token is fetched on demand and cached. If the file is absent or auth +# fails, we simply fetch anonymously (graceful fallback). +OPENSKY_CREDS = Path(__file__).resolve().parent / "openskycredentials.json" +OPENSKY_TOKEN_URL = ("https://auth.opensky-network.org/auth/realms/" + "opensky-network/protocol/openid-connect/token") +_opensky_token = {"value": None, "exp": 0.0} + # ---- Historical replay (record.py writes this db; dev-only) ----------------- # GET history/aircraft?t= returns the nearest recorded snapshot within # ±10 min, or 404. Prod has no recorder → the file is absent → graceful 404. @@ -64,6 +73,35 @@ def opensky_cache_file(): return Path(override if override else "~/.cache/godverse/opensky-states.json").expanduser() +def opensky_bearer(): + # A valid access token (refreshed ~1 min before expiry), or None if creds are + # missing/auth fails → caller fetches anonymously. + try: + d = json.loads(OPENSKY_CREDS.read_text()) + cid, csec = d.get("clientId"), d.get("clientSecret") + except (OSError, ValueError): + return None + if not cid or not csec: + return None + now = time.time() + if _opensky_token["value"] and now < _opensky_token["exp"]: + return _opensky_token["value"] + try: + data = urllib.parse.urlencode({ + "grant_type": "client_credentials", "client_id": cid, "client_secret": csec, + }).encode() + req = urllib.request.Request( + OPENSKY_TOKEN_URL, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}) + with urllib.request.urlopen(req, timeout=15) as r: + tok = json.loads(r.read()) + _opensky_token["value"] = tok["access_token"] + _opensky_token["exp"] = now + max(60, int(tok.get("expires_in", 1800)) - 60) + return _opensky_token["value"] + except Exception: + return None + + def has_bbox(query): # A global states/all request has no bounding-box params; only those bypass the cache. q = query.lower() @@ -150,8 +188,13 @@ class Handler(SimpleHTTPRequestHandler): if hdrs.get(k) is not None: passthrough[k] = hdrs.get(k) + req_headers = {"User-Agent": "godsigh-dev/0.1"} + if name == "opensky": + tok = opensky_bearer() # authenticated → 4000/day; None → anonymous 400/day + if tok: + req_headers["Authorization"] = "Bearer " + tok try: - req = urllib.request.Request(url, headers={"User-Agent": "godsigh-dev/0.1"}) + req = urllib.request.Request(url, headers=req_headers) with urllib.request.urlopen(req, timeout=45) as r: body = r.read() status, ctype = r.status, r.headers.get("Content-Type", "text/plain")