mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
A foreign Origin or Sec-Fetch-Site: cross-site request was answered and counted against the client's 60-a-minute quota, and could start a refresh. Run the keyless same-site checks the other map routes use before rate limiting and cache acquisition; reverse-proxy forwarding headers stay accepted. List the route with the keyless map routes in SECURITY.md.
158 lines
5.5 KiB
JavaScript
158 lines
5.5 KiB
JavaScript
import { normalizeDroneAirspaceSnapshot } from '../../src/layers/droneAirspace/records.js';
|
|
import { readResponseJsonCapped, coalesceProxyRequest } from './common/http.js';
|
|
import { makeRateLimiter, clientKey } from './common/rate-limit.js';
|
|
import { admitSameSiteKeyless } from './common/same-site.js';
|
|
|
|
// FAA Special Use Airspace (public, keyless). TYPE_CODE P (prohibited) and
|
|
// R (restricted) only — the small, always-in-force slice of the national
|
|
// SUA table. maxAllowableOffset trades ~100 m of boundary fidelity for a
|
|
// payload small enough to refresh (~330 KB for the current P+R set).
|
|
const API_URL =
|
|
'https://services6.arcgis.com/ssFJjBXIUyZDrSYZ/arcgis/rest/services/' +
|
|
'Special_Use_Airspace/FeatureServer/0/query?' +
|
|
new URLSearchParams({
|
|
where: "TYPE_CODE IN ('P','R')",
|
|
outFields: [
|
|
'NAME',
|
|
'TYPE_CODE',
|
|
'UPPER_VAL',
|
|
'UPPER_UOM',
|
|
'UPPER_CODE',
|
|
'LOWER_VAL',
|
|
'LOWER_UOM',
|
|
'LOWER_CODE',
|
|
'TIMESOFUSE',
|
|
'CONT_AGENT',
|
|
'CITY',
|
|
'STATE',
|
|
'OBJECTID',
|
|
].join(','),
|
|
maxAllowableOffset: '0.001',
|
|
outSR: '4326',
|
|
f: 'geojson',
|
|
}).toString();
|
|
|
|
const MAX_PAGES = 5;
|
|
const MIB = 1024 * 1024;
|
|
const SNAPSHOT_TTL_MS = 3_600_000;
|
|
/** Longest a failed refresh keeps serving the last snapshot, marked stale. */
|
|
const STALE_MAX_MS = 24 * 60 * 60 * 1000;
|
|
|
|
const exceededTransferLimit = (payload) =>
|
|
payload?.exceededTransferLimit === true ||
|
|
payload?.properties?.exceededTransferLimit === true;
|
|
|
|
/** Fixed-origin, bounded FAA SUA routes for dev and preview. */
|
|
export function droneAirspaceProxy({
|
|
fetchImpl = (...args) => globalThis.fetch(...args),
|
|
now = () => Date.now(),
|
|
} = {}) {
|
|
const cache = new Map();
|
|
const inFlight = new Map();
|
|
const allow = makeRateLimiter({ windowMs: 60_000, max: 60, globalMax: 1200 });
|
|
|
|
async function upstream(url, cap, timeout) {
|
|
const signal = AbortSignal.timeout(timeout);
|
|
const response = await fetchImpl(url, {
|
|
signal,
|
|
redirect: 'error',
|
|
});
|
|
if (!response.ok) {
|
|
await response.body?.cancel();
|
|
throw new Error('upstream_unavailable');
|
|
}
|
|
return readResponseJsonCapped(response, cap, signal);
|
|
}
|
|
|
|
// A snapshot is accepted only once the service says it is complete. A page
|
|
// limit reached while still flagged, a flagged page that adds nothing, or
|
|
// no valid zone at all is a failed refresh, so the last good snapshot stays.
|
|
async function fetchZones() {
|
|
const features = [];
|
|
let complete = false;
|
|
for (let page = 0; page < MAX_PAGES; page++) {
|
|
const url =
|
|
page === 0 ? API_URL : `${API_URL}&resultOffset=${features.length}`;
|
|
const payload = await upstream(url, 8 * MIB, 30_000);
|
|
if (!Array.isArray(payload?.features))
|
|
throw new Error('invalid_snapshot');
|
|
features.push(...payload.features);
|
|
if (!exceededTransferLimit(payload)) {
|
|
complete = true;
|
|
break;
|
|
}
|
|
if (!payload.features.length) break;
|
|
}
|
|
if (!complete) throw new Error('incomplete_snapshot');
|
|
const rows = normalizeDroneAirspaceSnapshot({ features });
|
|
if (!rows?.length) throw new Error('invalid_snapshot');
|
|
return { fetchedAt: now(), rows };
|
|
}
|
|
|
|
async function acquire(key, ttl, load) {
|
|
const previous = cache.get(key);
|
|
if (previous && now() - previous.savedAt < ttl)
|
|
return { value: previous.value, stale: false };
|
|
if (!inFlight.has(key) && inFlight.size >= 256)
|
|
throw Object.assign(new Error('busy'), { status: 429 });
|
|
try {
|
|
const { promise } = coalesceProxyRequest(inFlight, key, async () => {
|
|
const value = await load();
|
|
cache.set(key, { value, savedAt: now() });
|
|
return value;
|
|
});
|
|
return { value: await promise, stale: false };
|
|
} catch (error) {
|
|
// A failed refresh serves the last snapshot, marked stale, for a day.
|
|
if (previous && now() - previous.savedAt <= STALE_MAX_MS)
|
|
return { value: previous.value, stale: true };
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function handler(req, res) {
|
|
const json = (status, value, stale = false) => {
|
|
if (res.destroyed) return;
|
|
res.writeHead(status, {
|
|
'Content-Type': 'application/json',
|
|
'Cache-Control': 'no-store',
|
|
...(status === 405 ? { Allow: 'GET' } : {}),
|
|
...(status === 429 ? { 'Retry-After': '60' } : {}),
|
|
...(stale ? { 'X-Data-Stale': 'true' } : {}),
|
|
});
|
|
res.end(JSON.stringify(value));
|
|
};
|
|
// Keyless and needed to render the layer: refuse cross-site browsers
|
|
// before they spend a client's quota or start a refresh, but keep the
|
|
// documented reverse-proxy deployment working.
|
|
if (admitSameSiteKeyless(req, res)) return;
|
|
if (req.method !== 'GET') return json(405, { error: 'method_not_allowed' });
|
|
const path = (req.url || '/').split('?')[0];
|
|
if (path !== '/' && path !== '')
|
|
return json(404, { error: 'unknown_route' });
|
|
if (!allow(clientKey(req))) return json(429, { error: 'rate_limited' });
|
|
try {
|
|
const { value, stale } = await acquire(
|
|
'zones',
|
|
SNAPSHOT_TTL_MS,
|
|
fetchZones,
|
|
);
|
|
json(200, stale ? { ...value, stale: true } : value, stale);
|
|
} catch (error) {
|
|
json(error.status === 429 ? 429 : 502, {
|
|
error: 'drone_airspace_unavailable',
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
name: 'drone-airspace',
|
|
configureServer({ middlewares }) {
|
|
middlewares.use('/api/drone-airspace', handler);
|
|
},
|
|
configurePreviewServer({ middlewares }) {
|
|
middlewares.use('/api/drone-airspace', handler);
|
|
},
|
|
};
|
|
}
|