mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
style: format extracted live provider modules
This commit is contained in:
1 parent
0e221441b4
commit
f9a85528fe
14 files changed
+778
-362
No files matched your search
@@ -20,7 +20,11 @@ export function adsbLolProxy() {
|
||||
try {
|
||||
const now = Date.now();
|
||||
if (_cache && now - _cacheAt < CACHE_MS) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-ADS-B-Cache': 'HIT' });
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
'X-ADS-B-Cache': 'HIT',
|
||||
});
|
||||
res.end(_cache);
|
||||
return;
|
||||
}
|
||||
@@ -32,12 +36,19 @@ export function adsbLolProxy() {
|
||||
_cache = body;
|
||||
_cacheAt = now;
|
||||
}
|
||||
res.writeHead(upstream.status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-ADS-B-Cache': 'MISS' });
|
||||
res.writeHead(upstream.status, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
'X-ADS-B-Cache': 'MISS',
|
||||
});
|
||||
res.end(body);
|
||||
} catch (e) {
|
||||
console.error('[adsb.lol Proxy]', e.message);
|
||||
if (_cache) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'X-ADS-B-Cache': 'STALE' });
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/json',
|
||||
'X-ADS-B-Cache': 'STALE',
|
||||
});
|
||||
res.end(_cache);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,14 +21,18 @@ export function adsbdbProxy() {
|
||||
try {
|
||||
const parsed = JSON.parse(await fsp.readFile(CACHE_PATH, 'utf8'));
|
||||
cache = { routes: parsed.routes ?? {}, aircraft: parsed.aircraft ?? {} };
|
||||
} catch { /* first run */ }
|
||||
} catch {
|
||||
/* first run */
|
||||
}
|
||||
setInterval(async () => {
|
||||
if (!dirty) return;
|
||||
dirty = false;
|
||||
try {
|
||||
await fsp.mkdir(path.dirname(CACHE_PATH), { recursive: true });
|
||||
await fsp.writeFile(CACHE_PATH, JSON.stringify(cache), 'utf8');
|
||||
} catch { dirty = true; } // retry next tick
|
||||
} catch {
|
||||
dirty = true;
|
||||
} // retry next tick
|
||||
}, 15_000).unref?.();
|
||||
}
|
||||
|
||||
@@ -43,7 +47,11 @@ export function adsbdbProxy() {
|
||||
lat: Number.isFinite(a.latitude) ? a.latitude : null,
|
||||
lon: Number.isFinite(a.longitude) ? a.longitude : null,
|
||||
});
|
||||
return { airline: fr.airline?.name || null, origin: airport(fr.origin), destination: airport(fr.destination) };
|
||||
return {
|
||||
airline: fr.airline?.name || null,
|
||||
origin: airport(fr.origin),
|
||||
destination: airport(fr.destination),
|
||||
};
|
||||
}
|
||||
|
||||
function parseAircraft(json) {
|
||||
@@ -51,7 +59,10 @@ export function adsbdbProxy() {
|
||||
if (!a) return null;
|
||||
return {
|
||||
typeCode: a.icao_type || null, // ICAO designator, e.g. "B738" — feeds classifyAircraft
|
||||
typeName: a.manufacturer && a.type ? `${a.manufacturer} ${a.type}` : (a.type || null),
|
||||
typeName:
|
||||
a.manufacturer && a.type
|
||||
? `${a.manufacturer} ${a.type}`
|
||||
: a.type || null,
|
||||
registration: a.registration || null,
|
||||
};
|
||||
}
|
||||
@@ -61,30 +72,37 @@ export function adsbdbProxy() {
|
||||
if (fresh(store[key])) return Promise.resolve(store[key].data);
|
||||
const ik = `${kind}:${key}`;
|
||||
if (!inflight.has(ik)) {
|
||||
inflight.set(ik, (async () => {
|
||||
try {
|
||||
const url = kind === 'route'
|
||||
? `https://api.adsbdb.com/v0/callsign/${encodeURIComponent(key)}`
|
||||
: `https://api.adsbdb.com/v0/aircraft/${encodeURIComponent(key)}`;
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
||||
if (res.ok) {
|
||||
const data = kind === 'route' ? parseRoute(await res.json()) : parseAircraft(await res.json());
|
||||
store[key] = { at: Date.now(), data }; // data may be null — negative cache
|
||||
dirty = true;
|
||||
return data;
|
||||
inflight.set(
|
||||
ik,
|
||||
(async () => {
|
||||
try {
|
||||
const url =
|
||||
kind === 'route'
|
||||
? `https://api.adsbdb.com/v0/callsign/${encodeURIComponent(key)}`
|
||||
: `https://api.adsbdb.com/v0/aircraft/${encodeURIComponent(key)}`;
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
||||
if (res.ok) {
|
||||
const data =
|
||||
kind === 'route'
|
||||
? parseRoute(await res.json())
|
||||
: parseAircraft(await res.json());
|
||||
store[key] = { at: Date.now(), data }; // data may be null — negative cache
|
||||
dirty = true;
|
||||
return data;
|
||||
}
|
||||
if (res.status === 404) {
|
||||
store[key] = { at: Date.now(), data: null }; // known-missing — cache the miss
|
||||
dirty = true;
|
||||
}
|
||||
// other statuses: leave uncached so we retry later
|
||||
return fresh(store[key]) ? store[key].data : null;
|
||||
} catch {
|
||||
return fresh(store[key]) ? store[key].data : null; // network error → stale if any
|
||||
} finally {
|
||||
inflight.delete(ik);
|
||||
}
|
||||
if (res.status === 404) {
|
||||
store[key] = { at: Date.now(), data: null }; // known-missing — cache the miss
|
||||
dirty = true;
|
||||
}
|
||||
// other statuses: leave uncached so we retry later
|
||||
return fresh(store[key]) ? store[key].data : null;
|
||||
} catch {
|
||||
return fresh(store[key]) ? store[key].data : null; // network error → stale if any
|
||||
} finally {
|
||||
inflight.delete(ik);
|
||||
}
|
||||
})());
|
||||
})(),
|
||||
);
|
||||
}
|
||||
return inflight.get(ik);
|
||||
}
|
||||
@@ -99,18 +117,28 @@ export function adsbdbProxy() {
|
||||
res.end(JSON.stringify(obj));
|
||||
};
|
||||
try {
|
||||
const [, kind, rawKey] = String(req.url || '').split('?')[0].split('/');
|
||||
const [, kind, rawKey] = String(req.url || '')
|
||||
.split('?')[0]
|
||||
.split('/');
|
||||
if (kind === 'route') {
|
||||
const cs = String(rawKey || '').toUpperCase();
|
||||
if (!/^[A-Z0-9]{2,8}$/.test(cs)) return send(400, { error: 'invalid callsign' });
|
||||
if (!/^[A-Z0-9]{2,8}$/.test(cs))
|
||||
return send(400, { error: 'invalid callsign' });
|
||||
const data = await lookup('route', cs);
|
||||
return send(200, data ? { found: true, ...data } : { found: false });
|
||||
return send(
|
||||
200,
|
||||
data ? { found: true, ...data } : { found: false },
|
||||
);
|
||||
}
|
||||
if (kind === 'type') {
|
||||
const hex = String(rawKey || '').toLowerCase();
|
||||
if (!/^[0-9a-f]{6}$/.test(hex)) return send(400, { error: 'invalid hex' });
|
||||
if (!/^[0-9a-f]{6}$/.test(hex))
|
||||
return send(400, { error: 'invalid hex' });
|
||||
const data = await lookup('aircraft', hex);
|
||||
return send(200, data ? { found: true, ...data } : { found: false });
|
||||
return send(
|
||||
200,
|
||||
data ? { found: true, ...data } : { found: false },
|
||||
);
|
||||
}
|
||||
return send(404, { error: 'unknown endpoint' });
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { normalizeAdsbLolPointResponse } from '../../../src/data/adsbLolFallback.js';
|
||||
import { coalesceProxyRequest, readResponseJsonCapped } from '../common/http.js';
|
||||
import {
|
||||
coalesceProxyRequest,
|
||||
readResponseJsonCapped,
|
||||
} from '../common/http.js';
|
||||
import { requiredFiniteQueryNumber } from '../common/query.js';
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenSky OAuth2 token + response cache state
|
||||
@@ -105,7 +108,7 @@ export async function getOpenSkyToken() {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `grant_type=client_credentials&client_id=${encodeURIComponent(clientId)}&client_secret=${encodeURIComponent(clientSecret)}`,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let data = null;
|
||||
@@ -119,7 +122,8 @@ export async function getOpenSkyToken() {
|
||||
const expiresIn = Number(data?.expires_in);
|
||||
if (!res.ok || !accessToken) {
|
||||
if (!_openskyAuthWarned) {
|
||||
const detail = data?.error_description || data?.error || `HTTP ${res.status}`;
|
||||
const detail =
|
||||
data?.error_description || data?.error || `HTTP ${res.status}`;
|
||||
console.warn('[OpenSky] OAuth client_credentials failed:', detail);
|
||||
_openskyAuthWarned = true;
|
||||
}
|
||||
@@ -130,13 +134,21 @@ export async function getOpenSkyToken() {
|
||||
|
||||
_openskyToken = accessToken;
|
||||
// Default to 1800 s (30 min) if expires_in is missing or non-finite
|
||||
_openskyTokenExpiry = Date.now() + (Number.isFinite(expiresIn) ? expiresIn : 1800) * 1000;
|
||||
console.log('[OpenSky] OAuth token refreshed, expires in', Number.isFinite(expiresIn) ? expiresIn : 1800, 's');
|
||||
_openskyTokenExpiry =
|
||||
Date.now() + (Number.isFinite(expiresIn) ? expiresIn : 1800) * 1000;
|
||||
console.log(
|
||||
'[OpenSky] OAuth token refreshed, expires in',
|
||||
Number.isFinite(expiresIn) ? expiresIn : 1800,
|
||||
's',
|
||||
);
|
||||
_openskyAuthWarned = false;
|
||||
return _openskyToken;
|
||||
} catch (err) {
|
||||
if (!_openskyAuthWarned) {
|
||||
console.warn('[OpenSky] OAuth token request failed:', err?.message || String(err));
|
||||
console.warn(
|
||||
'[OpenSky] OAuth token request failed:',
|
||||
err?.message || String(err),
|
||||
);
|
||||
_openskyAuthWarned = true;
|
||||
}
|
||||
_openskyToken = null;
|
||||
@@ -158,12 +170,14 @@ export async function getOpenSkyToken() {
|
||||
* @returns {string} One of the valid mode strings, or the default ('oauth').
|
||||
*/
|
||||
function normalizeOpenSkyAuthMode(value) {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
const raw = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!raw) return OPENSKY_AUTH_MODE_DEFAULT;
|
||||
if (OPENSKY_AUTH_MODE_SET.has(raw)) return raw;
|
||||
if (!_openskyAuthModeWarned) {
|
||||
console.warn(
|
||||
`[OpenSky] Invalid OPENSKY_AUTH_MODE="${raw}", defaulting to "${OPENSKY_AUTH_MODE_DEFAULT}"`
|
||||
`[OpenSky] Invalid OPENSKY_AUTH_MODE="${raw}", defaulting to "${OPENSKY_AUTH_MODE_DEFAULT}"`,
|
||||
);
|
||||
_openskyAuthModeWarned = true;
|
||||
}
|
||||
@@ -183,7 +197,14 @@ function normalizeOpenSkyAuthMode(value) {
|
||||
* @param {string} opts.reason - Human-readable reason string for diagnostics.
|
||||
* @returns {Record<string,string>} Header object.
|
||||
*/
|
||||
function buildOpenSkyHeaders({ cacheStatus, requestedMode, usedMode, reason, staleSeconds, retryAfterSeconds }) {
|
||||
function buildOpenSkyHeaders({
|
||||
cacheStatus,
|
||||
requestedMode,
|
||||
usedMode,
|
||||
reason,
|
||||
staleSeconds,
|
||||
retryAfterSeconds,
|
||||
}) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
@@ -195,8 +216,12 @@ function buildOpenSkyHeaders({ cacheStatus, requestedMode, usedMode, reason, sta
|
||||
};
|
||||
// Credit-governor extras (field-test fix 2026-07-06): the client can show a
|
||||
// STALE cue / countdown without parsing the body.
|
||||
if (Number.isFinite(staleSeconds)) headers['X-OpenSky-Stale-Seconds'] = String(Math.round(staleSeconds));
|
||||
if (Number.isFinite(retryAfterSeconds)) headers['X-OpenSky-Retry-After-Seconds'] = String(Math.round(retryAfterSeconds));
|
||||
if (Number.isFinite(staleSeconds))
|
||||
headers['X-OpenSky-Stale-Seconds'] = String(Math.round(staleSeconds));
|
||||
if (Number.isFinite(retryAfterSeconds))
|
||||
headers['X-OpenSky-Retry-After-Seconds'] = String(
|
||||
Math.round(retryAfterSeconds),
|
||||
);
|
||||
return headers;
|
||||
}
|
||||
|
||||
@@ -204,8 +229,10 @@ export function adsbLolFallbackAnchor(req) {
|
||||
const incoming = new URL(req?.url || '', 'http://localhost');
|
||||
const latitude = requiredFiniteQueryNumber(incoming.searchParams, 'lat');
|
||||
const longitude = requiredFiniteQueryNumber(incoming.searchParams, 'lon');
|
||||
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) return null;
|
||||
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) return null;
|
||||
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90)
|
||||
return null;
|
||||
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180)
|
||||
return null;
|
||||
return { latitude, longitude };
|
||||
}
|
||||
|
||||
@@ -221,38 +248,45 @@ async function fetchAdsbLolPointFallback(req) {
|
||||
return { ...cached, cacheStatus: 'HIT' };
|
||||
}
|
||||
|
||||
const request = coalesceProxyRequest(_adsbLolPointInFlight, cacheKey, async () => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
try {
|
||||
const upstream = await fetch(
|
||||
`https://api.adsb.lol/v2/lat/${roundedLat}/lon/${roundedLon}/dist/${ADSBLOL_POINT_RADIUS_NM}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'gods-eye-view-adsblol-regional-fallback/1.0',
|
||||
const request = coalesceProxyRequest(
|
||||
_adsbLolPointInFlight,
|
||||
cacheKey,
|
||||
async () => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
try {
|
||||
const upstream = await fetch(
|
||||
`https://api.adsb.lol/v2/lat/${roundedLat}/lon/${roundedLon}/dist/${ADSBLOL_POINT_RADIUS_NM}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'gods-eye-view-adsblol-regional-fallback/1.0',
|
||||
},
|
||||
signal: controller.signal,
|
||||
},
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
if (!upstream.ok) throw new Error(`upstream HTTP ${upstream.status}`);
|
||||
const payload = await readResponseJsonCapped(upstream, ADSBLOL_POINT_MAX_RESPONSE_BYTES);
|
||||
const normalized = normalizeAdsbLolPointResponse(payload);
|
||||
const record = {
|
||||
body: JSON.stringify(normalized),
|
||||
cachedAt: Date.now(),
|
||||
count: normalized.states.length,
|
||||
};
|
||||
_adsbLolPointCache.delete(cacheKey);
|
||||
_adsbLolPointCache.set(cacheKey, record);
|
||||
while (_adsbLolPointCache.size > ADSBLOL_POINT_CACHE_MAX) {
|
||||
_adsbLolPointCache.delete(_adsbLolPointCache.keys().next().value);
|
||||
);
|
||||
if (!upstream.ok) throw new Error(`upstream HTTP ${upstream.status}`);
|
||||
const payload = await readResponseJsonCapped(
|
||||
upstream,
|
||||
ADSBLOL_POINT_MAX_RESPONSE_BYTES,
|
||||
);
|
||||
const normalized = normalizeAdsbLolPointResponse(payload);
|
||||
const record = {
|
||||
body: JSON.stringify(normalized),
|
||||
cachedAt: Date.now(),
|
||||
count: normalized.states.length,
|
||||
};
|
||||
_adsbLolPointCache.delete(cacheKey);
|
||||
_adsbLolPointCache.set(cacheKey, record);
|
||||
while (_adsbLolPointCache.size > ADSBLOL_POINT_CACHE_MAX) {
|
||||
_adsbLolPointCache.delete(_adsbLolPointCache.keys().next().value);
|
||||
}
|
||||
return record;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
return record;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
try {
|
||||
const record = await request.promise;
|
||||
return { ...record, cacheStatus: request.shared ? 'INFLIGHT' : 'MISS' };
|
||||
@@ -292,8 +326,10 @@ function openSkySourceEpochMs(body) {
|
||||
}
|
||||
|
||||
function openSkySourceIsStale(sourceEpochMs, now = Date.now()) {
|
||||
return Number.isFinite(sourceEpochMs)
|
||||
&& now - sourceEpochMs > OPENSKY_SOURCE_STALE_MS;
|
||||
return (
|
||||
Number.isFinite(sourceEpochMs) &&
|
||||
now - sourceEpochMs > OPENSKY_SOURCE_STALE_MS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -317,21 +353,26 @@ export function openSkyProxy() {
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/api/opensky', async (req, res) => {
|
||||
try {
|
||||
const requestedMode = normalizeOpenSkyAuthMode(process.env.OPENSKY_AUTH_MODE);
|
||||
const requestedMode = normalizeOpenSkyAuthMode(
|
||||
process.env.OPENSKY_AUTH_MODE,
|
||||
);
|
||||
const now = Date.now();
|
||||
const inCooldown = now < _openskyCooldownUntil;
|
||||
// Fresh-enough cache (adaptive TTL) OR any cache during a 429
|
||||
// cooldown: serve it without touching upstream. Stale-during-cooldown
|
||||
// is deliberate (credit governor): last-good planes beat a dead layer.
|
||||
if (_openskyCacheBody && (now - _openskyCacheTime < _openskyTtlMs || inCooldown)) {
|
||||
if (
|
||||
_openskyCacheBody &&
|
||||
(now - _openskyCacheTime < _openskyTtlMs || inCooldown)
|
||||
) {
|
||||
if (
|
||||
openSkySourceIsStale(_openskyCacheSourceEpochMs, now)
|
||||
&& await serveAdsbLolPointFallback(
|
||||
openSkySourceIsStale(_openskyCacheSourceEpochMs, now) &&
|
||||
(await serveAdsbLolPointFallback(
|
||||
req,
|
||||
res,
|
||||
requestedMode,
|
||||
'opensky_snapshot_stale_regional_fallback',
|
||||
)
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -347,10 +388,16 @@ export function openSkyProxy() {
|
||||
cacheStatus: isStale ? 'STALE' : 'HIT',
|
||||
requestedMode: cachedMeta.requestedMode || requestedMode,
|
||||
usedMode: cachedMeta.usedMode || 'unknown',
|
||||
reason: isStale ? 'rate_limited_serving_stale' : (cachedMeta.reason || 'cached'),
|
||||
staleSeconds: isStale ? (now - _openskyCacheTime) / 1000 : undefined,
|
||||
retryAfterSeconds: inCooldown ? (_openskyCooldownUntil - now) / 1000 : undefined,
|
||||
})
|
||||
reason: isStale
|
||||
? 'rate_limited_serving_stale'
|
||||
: cachedMeta.reason || 'cached',
|
||||
staleSeconds: isStale
|
||||
? (now - _openskyCacheTime) / 1000
|
||||
: undefined,
|
||||
retryAfterSeconds: inCooldown
|
||||
? (_openskyCooldownUntil - now) / 1000
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
res.end(_openskyCacheBody);
|
||||
return;
|
||||
@@ -359,22 +406,37 @@ export function openSkyProxy() {
|
||||
// synthesize the 429 locally — hammering upstream mid-cooldown can't
|
||||
// succeed and just burns goodwill.
|
||||
if (inCooldown) {
|
||||
if (await serveAdsbLolPointFallback(req, res, requestedMode, 'opensky_cooldown_regional_fallback')) return;
|
||||
res.writeHead(429, buildOpenSkyHeaders({
|
||||
cacheStatus: 'COOLDOWN',
|
||||
requestedMode,
|
||||
usedMode: 'none',
|
||||
reason: 'rate_limited',
|
||||
retryAfterSeconds: (_openskyCooldownUntil - now) / 1000,
|
||||
}));
|
||||
res.end(JSON.stringify({ error: 'OpenSky rate limited; proxy cooling down.' }));
|
||||
if (
|
||||
await serveAdsbLolPointFallback(
|
||||
req,
|
||||
res,
|
||||
requestedMode,
|
||||
'opensky_cooldown_regional_fallback',
|
||||
)
|
||||
)
|
||||
return;
|
||||
res.writeHead(
|
||||
429,
|
||||
buildOpenSkyHeaders({
|
||||
cacheStatus: 'COOLDOWN',
|
||||
requestedMode,
|
||||
usedMode: 'none',
|
||||
reason: 'rate_limited',
|
||||
retryAfterSeconds: (_openskyCooldownUntil - now) / 1000,
|
||||
}),
|
||||
);
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error: 'OpenSky rate limited; proxy cooling down.',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const basicUser = process.env.OPENSKY_USERNAME || '';
|
||||
const basicPass = process.env.OPENSKY_PASSWORD || '';
|
||||
const hasBasicCreds = Boolean(basicUser && basicPass);
|
||||
const headers = { 'Accept': 'application/json' };
|
||||
const headers = { Accept: 'application/json' };
|
||||
let usedMode = 'anon';
|
||||
let reason = 'forced_anonymous';
|
||||
|
||||
@@ -410,7 +472,10 @@ export function openSkyProxy() {
|
||||
}
|
||||
}
|
||||
|
||||
let upstream = await fetch('https://opensky-network.org/api/states/all?extended=1', { headers });
|
||||
let upstream = await fetch(
|
||||
'https://opensky-network.org/api/states/all?extended=1',
|
||||
{ headers },
|
||||
);
|
||||
// Auto-mode fallback: if OAuth was rejected, retry with Basic credentials
|
||||
if (
|
||||
(upstream.status === 401 || upstream.status === 403) &&
|
||||
@@ -422,7 +487,10 @@ export function openSkyProxy() {
|
||||
Accept: 'application/json',
|
||||
Authorization: `Basic ${Buffer.from(`${basicUser}:${basicPass}`).toString('base64')}`,
|
||||
};
|
||||
upstream = await fetch('https://opensky-network.org/api/states/all?extended=1', { headers: retryHeaders });
|
||||
upstream = await fetch(
|
||||
'https://opensky-network.org/api/states/all?extended=1',
|
||||
{ headers: retryHeaders },
|
||||
);
|
||||
usedMode = 'basic';
|
||||
reason = 'oauth_rejected_fallback_basic';
|
||||
}
|
||||
@@ -430,14 +498,14 @@ export function openSkyProxy() {
|
||||
let body = await upstream.text();
|
||||
const sourceEpochMs = upstream.ok ? openSkySourceEpochMs(body) : null;
|
||||
if (
|
||||
upstream.ok
|
||||
&& openSkySourceIsStale(sourceEpochMs, now)
|
||||
&& await serveAdsbLolPointFallback(
|
||||
upstream.ok &&
|
||||
openSkySourceIsStale(sourceEpochMs, now) &&
|
||||
(await serveAdsbLolPointFallback(
|
||||
req,
|
||||
res,
|
||||
requestedMode,
|
||||
'opensky_snapshot_stale_regional_fallback',
|
||||
)
|
||||
))
|
||||
) {
|
||||
// Keep the last global snapshot available as a fail-soft cache,
|
||||
// but do not label or render it as a fresh live result.
|
||||
@@ -452,23 +520,31 @@ export function openSkyProxy() {
|
||||
reason = 'rate_limited';
|
||||
// Credit governor: honor OpenSky's retry-after (bounded 30 s … 30 min;
|
||||
// 2 min when the header is absent) — no upstream attempts until then.
|
||||
const retryAfterSec = Number(upstream.headers.get('x-rate-limit-retry-after-seconds'));
|
||||
const retryAfterSec = Number(
|
||||
upstream.headers.get('x-rate-limit-retry-after-seconds'),
|
||||
);
|
||||
const cooldownMs = Math.min(
|
||||
Math.max(Number.isFinite(retryAfterSec) ? retryAfterSec * 1000 : 120_000, 30_000),
|
||||
30 * 60_000
|
||||
Math.max(
|
||||
Number.isFinite(retryAfterSec) ? retryAfterSec * 1000 : 120_000,
|
||||
30_000,
|
||||
),
|
||||
30 * 60_000,
|
||||
);
|
||||
_openskyCooldownUntil = now + cooldownMs;
|
||||
// Serve the last-good body instead of the 429 when we have one —
|
||||
// the layer keeps rendering (STALE-cued) instead of dying.
|
||||
if (_openskyCacheBody && _openskyCacheStatus === 200) {
|
||||
res.writeHead(200, buildOpenSkyHeaders({
|
||||
cacheStatus: 'STALE',
|
||||
requestedMode,
|
||||
usedMode,
|
||||
reason: 'rate_limited_serving_stale',
|
||||
staleSeconds: (now - _openskyCacheTime) / 1000,
|
||||
retryAfterSeconds: cooldownMs / 1000,
|
||||
}));
|
||||
res.writeHead(
|
||||
200,
|
||||
buildOpenSkyHeaders({
|
||||
cacheStatus: 'STALE',
|
||||
requestedMode,
|
||||
usedMode,
|
||||
reason: 'rate_limited_serving_stale',
|
||||
staleSeconds: (now - _openskyCacheTime) / 1000,
|
||||
retryAfterSeconds: cooldownMs / 1000,
|
||||
}),
|
||||
);
|
||||
res.end(_openskyCacheBody);
|
||||
return;
|
||||
}
|
||||
@@ -487,12 +563,14 @@ export function openSkyProxy() {
|
||||
if (upstream.status === 401 || upstream.status === 403) {
|
||||
if (requestedMode === 'basic' && !hasBasicCreds) {
|
||||
body = JSON.stringify({
|
||||
error: 'OpenSky auth missing. Basic mode requires OPENSKY_USERNAME and OPENSKY_PASSWORD.',
|
||||
error:
|
||||
'OpenSky auth missing. Basic mode requires OPENSKY_USERNAME and OPENSKY_PASSWORD.',
|
||||
});
|
||||
reason = 'missing_basic_creds';
|
||||
} else if (requestedMode === 'oauth' && usedMode !== 'oauth') {
|
||||
body = JSON.stringify({
|
||||
error: 'OpenSky auth invalid. OAuth mode requires valid OPENSKY_CLIENT_ID and OPENSKY_CLIENT_SECRET.',
|
||||
error:
|
||||
'OpenSky auth invalid. OAuth mode requires valid OPENSKY_CLIENT_ID and OPENSKY_CLIENT_SECRET.',
|
||||
});
|
||||
reason = 'oauth_invalid_or_missing';
|
||||
} else if (usedMode === 'basic') {
|
||||
@@ -502,12 +580,14 @@ export function openSkyProxy() {
|
||||
reason = 'basic_invalid_credentials';
|
||||
} else if (usedMode === 'oauth') {
|
||||
body = JSON.stringify({
|
||||
error: 'OpenSky auth invalid. OAuth client credentials were rejected.',
|
||||
error:
|
||||
'OpenSky auth invalid. OAuth client credentials were rejected.',
|
||||
});
|
||||
reason = 'oauth_invalid_credentials';
|
||||
} else if (requestedMode === 'auto' && !hasBasicCreds) {
|
||||
body = JSON.stringify({
|
||||
error: 'OpenSky auth missing. Provide basic credentials or valid OAuth client credentials.',
|
||||
error:
|
||||
'OpenSky auth missing. Provide basic credentials or valid OAuth client credentials.',
|
||||
});
|
||||
reason = 'missing_oauth_and_basic_creds';
|
||||
} else {
|
||||
@@ -521,9 +601,17 @@ export function openSkyProxy() {
|
||||
// Refine the reason string to reflect the actual outcome
|
||||
if (upstream.ok && reason === 'forced_anonymous') {
|
||||
reason = 'anonymous_ok';
|
||||
} else if (upstream.ok && usedMode === 'basic' && reason === 'basic_credentials') {
|
||||
} else if (
|
||||
upstream.ok &&
|
||||
usedMode === 'basic' &&
|
||||
reason === 'basic_credentials'
|
||||
) {
|
||||
reason = 'basic_ok';
|
||||
} else if (upstream.ok && usedMode === 'oauth' && reason === 'oauth_token') {
|
||||
} else if (
|
||||
upstream.ok &&
|
||||
usedMode === 'oauth' &&
|
||||
reason === 'oauth_token'
|
||||
) {
|
||||
reason = 'oauth_ok';
|
||||
}
|
||||
|
||||
@@ -542,7 +630,9 @@ export function openSkyProxy() {
|
||||
// Credit governor: adapt the cache TTL to the remaining daily
|
||||
// budget so a continuously-open app stretches its polls instead of
|
||||
// exhausting the quota mid-day. Success also clears any cooldown.
|
||||
const remaining = Number(upstream.headers.get('x-rate-limit-remaining'));
|
||||
const remaining = Number(
|
||||
upstream.headers.get('x-rate-limit-remaining'),
|
||||
);
|
||||
_openskyTtlMs = openskyAdaptiveTtlMs(remaining);
|
||||
_openskyCooldownUntil = 0;
|
||||
}
|
||||
@@ -554,14 +644,16 @@ export function openSkyProxy() {
|
||||
requestedMode,
|
||||
usedMode,
|
||||
reason,
|
||||
})
|
||||
}),
|
||||
);
|
||||
res.end(body);
|
||||
} catch (e) {
|
||||
console.error('[OpenSky Proxy]', e.message);
|
||||
if (_openskyCacheBody) {
|
||||
const cachedMeta = _openskyCacheMeta || {
|
||||
requestedMode: normalizeOpenSkyAuthMode(process.env.OPENSKY_AUTH_MODE),
|
||||
requestedMode: normalizeOpenSkyAuthMode(
|
||||
process.env.OPENSKY_AUTH_MODE,
|
||||
),
|
||||
usedMode: 'unknown',
|
||||
reason: 'cached_stale',
|
||||
};
|
||||
@@ -569,16 +661,27 @@ export function openSkyProxy() {
|
||||
_openskyCacheStatus || 200,
|
||||
buildOpenSkyHeaders({
|
||||
cacheStatus: 'STALE',
|
||||
requestedMode: cachedMeta.requestedMode || OPENSKY_AUTH_MODE_DEFAULT,
|
||||
requestedMode:
|
||||
cachedMeta.requestedMode || OPENSKY_AUTH_MODE_DEFAULT,
|
||||
usedMode: cachedMeta.usedMode || 'unknown',
|
||||
reason: cachedMeta.reason || 'cached_stale',
|
||||
})
|
||||
}),
|
||||
);
|
||||
res.end(_openskyCacheBody);
|
||||
return;
|
||||
}
|
||||
const requestedMode = normalizeOpenSkyAuthMode(process.env.OPENSKY_AUTH_MODE);
|
||||
if (await serveAdsbLolPointFallback(req, res, requestedMode, 'opensky_proxy_error_regional_fallback')) return;
|
||||
const requestedMode = normalizeOpenSkyAuthMode(
|
||||
process.env.OPENSKY_AUTH_MODE,
|
||||
);
|
||||
if (
|
||||
await serveAdsbLolPointFallback(
|
||||
req,
|
||||
res,
|
||||
requestedMode,
|
||||
'opensky_proxy_error_regional_fallback',
|
||||
)
|
||||
)
|
||||
return;
|
||||
res.writeHead(
|
||||
502,
|
||||
buildOpenSkyHeaders({
|
||||
@@ -586,7 +689,7 @@ export function openSkyProxy() {
|
||||
requestedMode,
|
||||
usedMode: 'error',
|
||||
reason: 'proxy_error',
|
||||
})
|
||||
}),
|
||||
);
|
||||
res.end(JSON.stringify({ error: 'OpenSky proxy error' }));
|
||||
}
|
||||
|
||||
@@ -36,8 +36,14 @@ export function trackBackfillProxies() {
|
||||
res.end(cached.body);
|
||||
return;
|
||||
}
|
||||
const upstream = await fetch(upstreamUrl, { headers, signal: AbortSignal.timeout(12000) });
|
||||
const { tooLarge, text } = await readCappedResponseText(upstream, RESPONSE_CAP_BYTES);
|
||||
const upstream = await fetch(upstreamUrl, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(12000),
|
||||
});
|
||||
const { tooLarge, text } = await readCappedResponseText(
|
||||
upstream,
|
||||
RESPONSE_CAP_BYTES,
|
||||
);
|
||||
let body;
|
||||
if (tooLarge) {
|
||||
body = JSON.stringify({ error: 'Upstream track response too large' });
|
||||
@@ -58,11 +64,15 @@ export function trackBackfillProxies() {
|
||||
middlewares.use('/api/opensky-track', async (req, res) => {
|
||||
try {
|
||||
const incoming = new URL(req.url || '', 'http://localhost');
|
||||
const icao24 = String(incoming.searchParams.get('icao24') || '').trim().toLowerCase();
|
||||
const icao24 = String(incoming.searchParams.get('icao24') || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!/^[0-9a-f]{6}$/.test(icao24)) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ error: 'icao24 must be a 6-char hex string' }));
|
||||
res.end(
|
||||
JSON.stringify({ error: 'icao24 must be a 6-char hex string' }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const token = await getOpenSkyToken();
|
||||
@@ -70,7 +80,7 @@ export function trackBackfillProxies() {
|
||||
res,
|
||||
`osky:${icao24}`,
|
||||
`https://opensky-network.org/api/tracks/all?icao24=${icao24}&time=0`,
|
||||
token ? { Authorization: `Bearer ${token}` } : {}
|
||||
token ? { Authorization: `Bearer ${token}` } : {},
|
||||
);
|
||||
} catch (error) {
|
||||
res.statusCode = 502;
|
||||
@@ -82,17 +92,21 @@ export function trackBackfillProxies() {
|
||||
middlewares.use('/api/adsblol/trace', async (req, res) => {
|
||||
try {
|
||||
const incoming = new URL(req.url || '', 'http://localhost');
|
||||
const hex = String(incoming.searchParams.get('hex') || '').trim().toLowerCase();
|
||||
const hex = String(incoming.searchParams.get('hex') || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!/^[0-9a-f~]{6,7}$/.test(hex)) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ error: 'hex must be a 6-7 char hex string' }));
|
||||
res.end(
|
||||
JSON.stringify({ error: 'hex must be a 6-7 char hex string' }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await proxyJson(
|
||||
res,
|
||||
`lol:${hex}`,
|
||||
`https://adsb.lol/data/traces/${hex.slice(-2)}/trace_full_${hex}.json`
|
||||
`https://adsb.lol/data/traces/${hex.slice(-2)}/trace_full_${hex}.json`,
|
||||
);
|
||||
} catch (error) {
|
||||
res.statusCode = 502;
|
||||
|
||||
Reference in new issue
Block a user