mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix: register data providers for local preview
Adapt the preview middleware contribution from PR #324 to the extracted provider modules, including the existing OpenSky route.
This commit is contained in:
1 parent
276bb84904
commit
a0f07b752f
10 files changed
+1100
-1089
No files matched your search
+152
-150
@@ -21,161 +21,163 @@ const GBFS_PROXY_TIMEOUT_MS = 12000;
|
||||
* @returns {import('vite').Plugin}
|
||||
*/
|
||||
export function gbfsProxy() {
|
||||
return {
|
||||
name: 'gbfs-proxy',
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/api/gbfs', async (req, res) => {
|
||||
try {
|
||||
if (req.method !== 'GET') {
|
||||
res.writeHead(405, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url || '/', 'http://localhost');
|
||||
const encodedTarget = url.pathname.replace(/^\/+/, '');
|
||||
if (!encodedTarget) {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Missing GBFS upstream target' }));
|
||||
return;
|
||||
}
|
||||
|
||||
let decodedTarget = '';
|
||||
try {
|
||||
decodedTarget = decodeURIComponent(encodedTarget);
|
||||
} catch {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Invalid GBFS target encoding' }));
|
||||
return;
|
||||
}
|
||||
|
||||
let upstreamUrl = null;
|
||||
try {
|
||||
upstreamUrl = new URL(decodedTarget);
|
||||
} catch {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Invalid GBFS upstream URL' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (upstreamUrl.protocol !== 'https:') {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(
|
||||
JSON.stringify({ error: 'Only https GBFS targets are allowed' }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAllowedGbfsHost(upstreamUrl.hostname)) {
|
||||
res.writeHead(403, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'GBFS host not allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAllowedGbfsPath(upstreamUrl.pathname)) {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error:
|
||||
'Only station_information/station_status endpoints are allowed',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
GBFS_PROXY_TIMEOUT_MS,
|
||||
);
|
||||
let upstream;
|
||||
try {
|
||||
upstream = await fetch(upstreamUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'gods-eye-view-gbfs-proxy/1.0',
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
// Limit response size to prevent memory exhaustion from malicious upstream
|
||||
const GBFS_MAX_BODY_BYTES = 5 * 1024 * 1024; // 5 MB
|
||||
const contentLength = Number(upstream.headers.get('content-length'));
|
||||
if (
|
||||
Number.isFinite(contentLength) &&
|
||||
contentLength > GBFS_MAX_BODY_BYTES
|
||||
) {
|
||||
res.writeHead(502, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(
|
||||
JSON.stringify({ error: 'GBFS upstream response too large' }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const body = await upstream.text();
|
||||
if (Buffer.byteLength(body, 'utf8') > GBFS_MAX_BODY_BYTES) {
|
||||
res.writeHead(502, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(
|
||||
JSON.stringify({ error: 'GBFS upstream response too large' }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const contentType =
|
||||
upstream.headers.get('content-type') || 'application/json';
|
||||
res.writeHead(upstream.status, {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': gbfsCacheControl(upstreamUrl.pathname),
|
||||
'X-GBFS-Upstream': upstreamUrl.hostname,
|
||||
'X-GBFS-Cache': 'MISS',
|
||||
const installMiddleware = (server) => {
|
||||
server.middlewares.use('/api/gbfs', async (req, res) => {
|
||||
try {
|
||||
if (req.method !== 'GET') {
|
||||
res.writeHead(405, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(body);
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
res.writeHead(504, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'GBFS upstream timeout' }));
|
||||
return;
|
||||
}
|
||||
console.error('[GBFS Proxy]', error?.message || String(error));
|
||||
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url || '/', 'http://localhost');
|
||||
const encodedTarget = url.pathname.replace(/^\/+/, '');
|
||||
if (!encodedTarget) {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Missing GBFS upstream target' }));
|
||||
return;
|
||||
}
|
||||
|
||||
let decodedTarget = '';
|
||||
try {
|
||||
decodedTarget = decodeURIComponent(encodedTarget);
|
||||
} catch {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Invalid GBFS target encoding' }));
|
||||
return;
|
||||
}
|
||||
|
||||
let upstreamUrl = null;
|
||||
try {
|
||||
upstreamUrl = new URL(decodedTarget);
|
||||
} catch {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'Invalid GBFS upstream URL' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (upstreamUrl.protocol !== 'https:') {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(
|
||||
JSON.stringify({ error: 'Only https GBFS targets are allowed' }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAllowedGbfsHost(upstreamUrl.hostname)) {
|
||||
res.writeHead(403, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'GBFS host not allowed' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAllowedGbfsPath(upstreamUrl.pathname)) {
|
||||
res.writeHead(400, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
error:
|
||||
'Only station_information/station_status endpoints are allowed',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
GBFS_PROXY_TIMEOUT_MS,
|
||||
);
|
||||
let upstream;
|
||||
try {
|
||||
upstream = await fetch(upstreamUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'gods-eye-view-gbfs-proxy/1.0',
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
// Limit response size to prevent memory exhaustion from malicious upstream
|
||||
const GBFS_MAX_BODY_BYTES = 5 * 1024 * 1024; // 5 MB
|
||||
const contentLength = Number(upstream.headers.get('content-length'));
|
||||
if (
|
||||
Number.isFinite(contentLength) &&
|
||||
contentLength > GBFS_MAX_BODY_BYTES
|
||||
) {
|
||||
res.writeHead(502, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'GBFS proxy error' }));
|
||||
res.end(
|
||||
JSON.stringify({ error: 'GBFS upstream response too large' }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
const body = await upstream.text();
|
||||
if (Buffer.byteLength(body, 'utf8') > GBFS_MAX_BODY_BYTES) {
|
||||
res.writeHead(502, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(
|
||||
JSON.stringify({ error: 'GBFS upstream response too large' }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const contentType =
|
||||
upstream.headers.get('content-type') || 'application/json';
|
||||
res.writeHead(upstream.status, {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': gbfsCacheControl(upstreamUrl.pathname),
|
||||
'X-GBFS-Upstream': upstreamUrl.hostname,
|
||||
'X-GBFS-Cache': 'MISS',
|
||||
});
|
||||
res.end(body);
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
res.writeHead(504, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'GBFS upstream timeout' }));
|
||||
return;
|
||||
}
|
||||
console.error('[GBFS Proxy]', error?.message || String(error));
|
||||
res.writeHead(502, {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store',
|
||||
});
|
||||
res.end(JSON.stringify({ error: 'GBFS proxy error' }));
|
||||
}
|
||||
});
|
||||
};
|
||||
return {
|
||||
name: 'gbfs-proxy',
|
||||
configureServer: installMiddleware,
|
||||
configurePreviewServer: installMiddleware,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user