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.
|
# may serve a stale cache rather than nothing.
|
||||||
OPENSKY_CACHE_FRESH_SEC = 120
|
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) -----------------
|
# ---- Historical replay (record.py writes this db; dev-only) -----------------
|
||||||
# GET history/aircraft?t=<epoch_s> returns the nearest recorded snapshot within
|
# 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.
|
# ±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()
|
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):
|
def has_bbox(query):
|
||||||
# A global states/all request has no bounding-box params; only those bypass the cache.
|
# A global states/all request has no bounding-box params; only those bypass the cache.
|
||||||
q = query.lower()
|
q = query.lower()
|
||||||
@ -150,8 +188,13 @@ class Handler(SimpleHTTPRequestHandler):
|
|||||||
if hdrs.get(k) is not None:
|
if hdrs.get(k) is not None:
|
||||||
passthrough[k] = hdrs.get(k)
|
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:
|
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:
|
with urllib.request.urlopen(req, timeout=45) as r:
|
||||||
body = r.read()
|
body = r.read()
|
||||||
status, ctype = r.status, r.headers.get("Content-Type", "text/plain")
|
status, ctype = r.status, r.headers.get("Content-Type", "text/plain")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user