mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-09 21:02:19 +00:00
Merge branch 'main' into feat/transit-more-cities
This commit is contained in:
8 files changed
+109
-1
No files matched your search
@@ -0,0 +1,4 @@
|
||||
- Answer `405 Method Not Allowed` with `Allow: GET` to anything but `GET` on
|
||||
`/api/flights`, `/api/military` and `/api/route`, as the other query proxies
|
||||
do. They answered every verb with the same body. The refusal comes before
|
||||
rate limiting and any upstream call (daikaginza, #794).
|
||||
@@ -6491,6 +6491,7 @@ apply to `public/` assets or built-preview serving.
|
||||
- `/api/overpass` is bounded by body/response caps, per-client/global rate limits, concurrency limits, operator-configured upstreams, in-flight dedupe, cache bounds, and static validation that every selector is spatially bounded.
|
||||
- `/api/military-installations` uses an independent limiter with the same 90-per-client/300-global one-minute bounds, so viewport installation refreshes never consume `/api/overpass` annotation capacity.
|
||||
- `/api/route` proxies bounded OSRM route requests for annotation routes, with profile allowlisting, distance caps, response caps, caching, and sanitized "no route found" errors.
|
||||
- `/api/flights`, `/api/military`, and `/api/route` answer `405 Method Not Allowed` with `Allow: GET` to anything but `GET`, before rate limiting and any upstream call.
|
||||
- `/api/flights` negotiates gzip. The cached global snapshot and the regional adsb.lol fallback are each gzipped once per refresh (about 860 KB becomes about 250 KB). Clients that offer gzip get that frame; other clients, and bodies under 1 KiB or that gzip cannot shrink, get identity bytes. Both answers carry `Vary: Accept-Encoding`. Uncached bodies, such as auth failures, are never sent under a cached frame's encoding.
|
||||
- Track endpoints: `/api/vessels/track?mmsi=` (server-accumulated ring buffers; sub-route handled before the rows snapshot), `/api/flights/track?icao24=` (OAuth, 60s cache, sanitized errors, independent OpenSky credit bucket), `/api/military/track?hex=` (400 for an invalid hex, otherwise `503 history_unavailable` with `Retry-After: 1800` and `Cache-Control: no-store`, no upstream request; military tracking uses its locally accumulated trail).
|
||||
- Realtime debug logs redact API keys, bearer tokens, client secrets, and image data URLs before writing to disk; request bodies are size-capped.
|
||||
|
||||
@@ -83,6 +83,15 @@ export function adsbLolProxy() {
|
||||
// This mount also matches its /track route, served by its own handler.
|
||||
if (new URL(req.url || '/', 'http://localhost').pathname !== '/')
|
||||
return next();
|
||||
// After the /track hand-off, so /track keeps its own method handling.
|
||||
if (req.method !== 'GET') {
|
||||
res.writeHead(405, {
|
||||
'Content-Type': 'application/json',
|
||||
Allow: 'GET',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (_cache && now - _cacheAt < CACHE_MS) {
|
||||
serve(res, 200, _cache, 'HIT');
|
||||
|
||||
@@ -496,6 +496,15 @@ export function openSkyProxy() {
|
||||
// This mount also matches its /track route, served by its own handler.
|
||||
if (new URL(req.url || '/', 'http://localhost').pathname !== '/')
|
||||
return next();
|
||||
// After the /track hand-off, so /track keeps its own method handling.
|
||||
if (req.method !== 'GET') {
|
||||
res.writeHead(405, {
|
||||
'Content-Type': 'application/json',
|
||||
Allow: 'GET',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
||||
return;
|
||||
}
|
||||
res.setHeader('X-Feed-Source', 'OpenSky Network');
|
||||
const requestedMode = normalizeOpenSkyAuthMode(
|
||||
process.env.OPENSKY_AUTH_MODE,
|
||||
|
||||
@@ -227,6 +227,11 @@ export function installRouteMiddleware(
|
||||
// only shape that asks the upstream for them. A response WITHOUT steps is
|
||||
// byte for byte what this endpoint has always returned.
|
||||
middlewares.use('/api/route', async (req, res) => {
|
||||
if (req.method !== 'GET') {
|
||||
res.writeHead(405, { 'Content-Type': 'application/json', Allow: 'GET' });
|
||||
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
||||
return;
|
||||
}
|
||||
const fail = (msg) => {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, error: msg }));
|
||||
|
||||
@@ -209,7 +209,11 @@ test('routing middleware uses configured OSRM servers while retaining request bo
|
||||
const request = async (query) => {
|
||||
let payload;
|
||||
await handler(
|
||||
{ url: `/?${query}`, socket: { remoteAddress: '127.0.0.1' } },
|
||||
{
|
||||
url: `/?${query}`,
|
||||
method: 'GET',
|
||||
socket: { remoteAddress: '127.0.0.1' },
|
||||
},
|
||||
{
|
||||
writeHead() {},
|
||||
end(body) {
|
||||
|
||||
@@ -585,6 +585,63 @@ test('track backfill proxy returns 502 on an oversized upstream body and caches
|
||||
assert.equal(callCount, 1);
|
||||
});
|
||||
|
||||
test('the aircraft snapshot routes decline a non-GET before any upstream work', async (t) => {
|
||||
// A fetch that throws proves the refusal comes before any upstream call.
|
||||
t.mock.method(globalThis, 'fetch', async (url) => {
|
||||
throw new Error(`upstream must not be reached: ${url}`);
|
||||
});
|
||||
const cases = [
|
||||
[install(providers.openSkyProxy()), '/api/flights'],
|
||||
[install(providers.adsbLolProxy()), '/api/military'],
|
||||
];
|
||||
for (const [request, route] of cases) {
|
||||
for (const method of ['POST', 'PUT', 'DELETE', 'PATCH']) {
|
||||
const response = await request(route, '/', method);
|
||||
assert.equal(response.statusCode, 405, `${method} ${route}`);
|
||||
// RFC 9110 section 15.5.6: a 405 names the methods it accepts.
|
||||
assert.equal(response.headers.allow, 'GET', `${method} ${route}`);
|
||||
assert.deepEqual(JSON.parse(response.body), {
|
||||
error: 'Method Not Allowed',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('the snapshot method check hands each /track route on to its own handler', async () => {
|
||||
// The snapshot mount also matches /track, so it must hand /track on
|
||||
// before it checks the method.
|
||||
for (const [plugin, route] of [
|
||||
[providers.openSkyProxy(), '/api/flights'],
|
||||
[providers.adsbLolProxy(), '/api/military'],
|
||||
]) {
|
||||
const routes = new Map();
|
||||
plugin.configureServer({
|
||||
middlewares: { use: (path, handler) => routes.set(path, handler) },
|
||||
});
|
||||
const res = {
|
||||
statusCode: 200,
|
||||
writeHead(s) {
|
||||
this.statusCode = s;
|
||||
},
|
||||
end() {},
|
||||
};
|
||||
let handedOn = false;
|
||||
await routes.get(route)(
|
||||
{ url: '/track?icao24=abc123', method: 'POST' },
|
||||
res,
|
||||
() => {
|
||||
handedOn = true;
|
||||
},
|
||||
);
|
||||
assert.ok(handedOn, `${route}/track reaches next()`);
|
||||
assert.equal(
|
||||
res.statusCode,
|
||||
200,
|
||||
`${route}/track is not answered by the snapshot handler`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the OpenSky snapshot is compressed for clients that ask, and an error body never inherits the cached frame', async (t) => {
|
||||
environment(t, {
|
||||
OPENSKY_AUTH_MODE: 'anon',
|
||||
|
||||
@@ -501,6 +501,25 @@ test('the upstream host is pinned: a redirect is refused, not followed', async (
|
||||
assert.deepEqual(seen, ['error'], 'a redirect would escape the pinned host');
|
||||
});
|
||||
|
||||
test('the routing proxy declines a non-GET before rate limiting or upstream', async (t) => {
|
||||
// A fetch that throws proves the refusal comes before any upstream call.
|
||||
const request = install(installRouteMiddleware);
|
||||
t.mock.method(globalThis, 'fetch', async () => {
|
||||
throw new Error('upstream must not be reached');
|
||||
});
|
||||
for (const method of ['POST', 'PUT', 'DELETE', 'PATCH']) {
|
||||
const response = await request(
|
||||
'/api/route',
|
||||
'?profile=car&coords=-97,30;-97.01,30.01',
|
||||
method,
|
||||
);
|
||||
assert.equal(response.statusCode, 405, method);
|
||||
// RFC 9110 section 15.5.6: a 405 names the methods it accepts.
|
||||
assert.equal(response.headers.allow, 'GET', method);
|
||||
assert.deepEqual(response.body, { error: 'Method Not Allowed' });
|
||||
}
|
||||
});
|
||||
|
||||
test('both paid Google routes refuse cross-site requests before calling Google', async (t) => {
|
||||
const plugin = googlePlacesContextProxy({ resolveApiKey: () => 'key' });
|
||||
const routes = new Map();
|
||||
|
||||
Reference in new issue
Block a user