Files
Webserver/lib/request.ts

81 lines
2.1 KiB
TypeScript
Raw Permalink Normal View History

2026-05-25 09:49:40 -04:00
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
function sanitizeHeaderValue(value: string, maxLength: number) {
return value.replace(/[\u0000-\u001f\u007f]/g, '').trim().slice(0, maxLength) || 'unknown';
}
export function getClientAddress(headers: Headers) {
const forwarded = headers.get('x-forwarded-for');
if (forwarded) {
const addresses = forwarded
.split(',')
.map((value) => value.trim())
.filter(Boolean);
if (addresses.length > 0) {
// Use the nearest address. This avoids trusting a spoofed left-most value
// when Next or a reverse proxy appends the real peer address.
return sanitizeHeaderValue(addresses[addresses.length - 1], 128);
}
}
const realIp = headers.get('x-real-ip');
if (realIp) return sanitizeHeaderValue(realIp, 128);
return 'unknown';
}
export function getUserAgent(headers: Headers) {
return sanitizeHeaderValue(headers.get('user-agent') || 'unknown', 256);
}
export function normalizeVisitPath(value: unknown) {
if (typeof value !== 'string') return null;
const input = value.trim();
if (
input.length === 0 ||
input.length > 2048 ||
!input.startsWith('/') ||
input.startsWith('//') ||
CONTROL_CHARS.test(input)
) {
return null;
}
try {
const parsed = new URL(input, 'https://site.local');
if (parsed.origin !== 'https://site.local') return null;
return `${parsed.pathname}${parsed.search}`;
} catch {
return null;
}
}
export function isSameOriginRequest(request: Request) {
const host = request.headers.get('host');
if (!host) return false;
const requestHost = host.toLowerCase();
const origin = request.headers.get('origin');
if (origin) {
try {
return new URL(origin).host.toLowerCase() === requestHost;
} catch {
return false;
}
}
const referer = request.headers.get('referer');
if (referer) {
try {
return new URL(referer).host.toLowerCase() === requestHost;
} catch {
return false;
}
}
const fetchSite = request.headers.get('sec-fetch-site');
return fetchSite === 'same-origin' || fetchSite === 'none';
}