import { isIP } from 'node:net'; export const COOKIE = 'dng_access'; export function readCookie(req) { const matches = (req.headers.cookie || '').split(';').map(v => v.trim()).filter(v => v.startsWith(`${COOKIE}=`)); return matches.length === 1 ? matches[0].slice(COOKIE.length + 1) : ''; } export function sessionCookie(config, token, maxAge = 0) { return `${COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}${config.secureCookie ? '; Secure' : ''}`; } export function safeNext(value) { if (typeof value !== 'string' || value.length > 4096 || !value.startsWith('/') || value.startsWith('//') || /[\\\u0000-\u0020]/.test(value)) return '/'; try { const decoded = decodeURIComponent(value); if (/[\\\u0000-\u001f]/.test(decoded)) return '/'; const url = new URL(value, 'http://gateway.local'); if (url.origin !== 'http://gateway.local' || url.pathname.startsWith('/_access')) return '/'; return url.pathname + url.search + url.hash; } catch { return '/'; } } export function clientIp(req, config) { const forwarded = req.headers['x-forwarded-for']; // Accept a single IP only. Forwarded chains require explicit edge normalization. if (config.trustProxy && typeof forwarded === 'string' && isIP(forwarded.trim())) return forwarded.trim(); return req.socket.remoteAddress || 'unknown'; } export function sendJson(res, status, body, headers = {}) { res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff', ...headers }); res.end(JSON.stringify(body)); } export async function readBody(req, maximum = 4096) { if (Number(req.headers['content-length']) > maximum) { const error = new Error('Request is too large.'); error.status = 413; throw error; } const chunks = []; let size = 0; for await (const chunk of req) { size += chunk.length; if (size > maximum) { const error = new Error('Request is too large.'); error.status = 413; throw error; } chunks.push(chunk); } return Buffer.concat(chunks); }