feat: authenticate OpenSky via OAuth2 (4000/day vs 400 anonymous)
serve.py now exchanges the gitignored openskycredentials.json (clientId/ clientSecret) for a 30-min bearer token (cached, refreshed ~1min before expiry) and adds it to the /states/all upstream fetch — lifting the aircraft quota to 4000 credits/day. Graceful fallback to anonymous if the file is absent or auth fails. Token exchange only happens on a cache miss (the 120s shared cache still applies first). Verified: dev proxy fetch shows X-Rate-Limit-Remaining 3992 (authenticated tier) vs the anonymous ~400. Key stays server-side (gitignored, never in the browser). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0bbdb30719
commit
594a0437e4
45
serve.py
45
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=<epoch_s> 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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user