mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
109 lines
3.9 KiB
JavaScript
109 lines
3.9 KiB
JavaScript
/**
|
|
* Read a fetch() Response body as text with a hard byte cap. Rejects early on an
|
|
* oversized Content-Length, then streams with a running cap so a chunked or
|
|
* length-omitted response cannot blow past the limit. Throws { code:'RESPONSE_TOO_LARGE' }.
|
|
*/
|
|
export async function readResponseTextCapped(response, maxBytes) {
|
|
const declared = Number(response.headers.get('content-length'));
|
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
const err = new Error('Upstream response too large');
|
|
err.code = 'RESPONSE_TOO_LARGE';
|
|
throw err;
|
|
}
|
|
const reader = response.body?.getReader?.();
|
|
if (!reader) {
|
|
const text = await response.text();
|
|
if (Buffer.byteLength(text) > maxBytes) {
|
|
const err = new Error('Upstream response too large');
|
|
err.code = 'RESPONSE_TOO_LARGE';
|
|
throw err;
|
|
}
|
|
return text;
|
|
}
|
|
const decoder = new TextDecoder();
|
|
let out = '';
|
|
let total = 0;
|
|
for (;;) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
total += value.byteLength;
|
|
if (total > maxBytes) {
|
|
try { await reader.cancel(); } catch { /* no-op */ }
|
|
const err = new Error('Upstream response too large');
|
|
err.code = 'RESPONSE_TOO_LARGE';
|
|
throw err;
|
|
}
|
|
out += decoder.decode(value, { stream: true });
|
|
}
|
|
out += decoder.decode();
|
|
return out;
|
|
}
|
|
|
|
/** Parse a fetch() JSON response only after enforcing a hard byte cap. */
|
|
export async function readResponseJsonCapped(response, maxBytes) {
|
|
return JSON.parse(await readResponseTextCapped(response, maxBytes));
|
|
}
|
|
|
|
/**
|
|
* Return the existing promise for a cache key, or create one and remove it
|
|
* only when that exact promise settles.
|
|
*/
|
|
export function coalesceProxyRequest(inFlight, key, create) {
|
|
const existing = inFlight.get(key);
|
|
if (existing) return { promise: existing, shared: true };
|
|
let promise;
|
|
promise = Promise.resolve()
|
|
.then(create)
|
|
.finally(() => {
|
|
if (inFlight.get(key) === promise) inFlight.delete(key);
|
|
});
|
|
inFlight.set(key, promise);
|
|
return { promise, shared: false };
|
|
}
|
|
|
|
/**
|
|
* Pipe an upstream fetch Response (image or video) to the client HTTP response.
|
|
*
|
|
* Forwards Content-Type, Content-Length, Content-Range, Accept-Ranges, and
|
|
* Cache-Control headers from the upstream. Falls back to buffered arrayBuffer
|
|
* if the body is not streamable.
|
|
*
|
|
* @param {import('http').ServerResponse} res
|
|
* @param {Response} upstream - fetch() Response object.
|
|
* @param {object} [opts]
|
|
* @param {string} [opts.sourceHeader='upstream'] - Value for X-CCTV-Source header.
|
|
*/
|
|
/**
|
|
* Read a fetch Response body as text while enforcing a hard byte cap during
|
|
* the read — so a malicious or buggy upstream that streams an unbounded body
|
|
* (no/oversized Content-Length, chunked) can't OOM the proxy. Returns
|
|
* { tooLarge, text }. Cancels the stream as soon as the cap is crossed.
|
|
* @param {Response} upstream - fetch() response.
|
|
* @param {number} maxBytes - hard ceiling on decoded bytes.
|
|
* @returns {Promise<{tooLarge: boolean, text: string}>}
|
|
*/
|
|
export async function readCappedResponseText(upstream, maxBytes) {
|
|
const declared = Number(upstream.headers.get('content-length'));
|
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
try { await upstream.body?.cancel(); } catch { /* no-op */ }
|
|
return { tooLarge: true, text: '' };
|
|
}
|
|
if (!upstream.body || typeof upstream.body[Symbol.asyncIterator] !== 'function') {
|
|
const text = await upstream.text();
|
|
return text.length > maxBytes ? { tooLarge: true, text: '' } : { tooLarge: false, text };
|
|
}
|
|
const decoder = new TextDecoder();
|
|
let text = '';
|
|
let total = 0;
|
|
for await (const chunk of upstream.body) {
|
|
total += chunk.length;
|
|
if (total > maxBytes) {
|
|
try { await upstream.body.cancel(); } catch { /* no-op */ }
|
|
return { tooLarge: true, text: '' };
|
|
}
|
|
text += decoder.decode(chunk, { stream: true });
|
|
}
|
|
text += decoder.decode();
|
|
return { tooLarge: false, text };
|
|
}
|