import type { IncomingMessage } from "node:http"; import { isIP } from "node:net"; import { argon2Verify } from "hash-wasm"; import { RateLimiterMemory } from "rate-limiter-flexible"; import { getSearchToken } from "./searchToken.ts"; import { addVerifiedToken, isVerifiedToken } from "./verifiedTokens.ts"; const rateLimiter = new RateLimiterMemory({ points: 10, duration: 10, }); /** Whether to trust proxy-set forwarding headers. Off unless `TRUST_PROXY` is `true`/`1`. */ function isProxyTrusted(): boolean { const value = process.env.TRUST_PROXY?.trim().toLowerCase(); return value === "true" || value === "1"; } /** * Resolves the client IP used as the rate-limit key. * * `X-Forwarded-For` / `X-Real-IP` are only honored when `TRUST_PROXY` is * enabled. On a directly-exposed instance those headers are fully * client-controlled, so trusting them would let a caller forge a fresh IP per * request and evade rate limiting entirely. When `TRUST_PROXY` is off (the * default) we use the real TCP peer address, which cannot be spoofed. * * Enable `TRUST_PROXY` only when MiniSearch runs behind a reverse proxy that * sets the rightmost `X-Forwarded-For` entry (e.g. nginx's * `$proxy_add_x_forwarded_for`). */ export function getClientIp(request: IncomingMessage): string { if (isProxyTrusted()) { const forwarded = request.headers["x-forwarded-for"]; const xff = Array.isArray(forwarded) ? forwarded.join(",") : forwarded; if (typeof xff === "string" && xff.length > 0) { const parts = xff .split(",") .map((p) => p.trim()) .filter(Boolean); const ip = parts[parts.length - 1]; if (ip && isIP(ip)) { return ip; } } const realIp = request.headers["x-real-ip"]; if (typeof realIp === "string" && realIp.length > 0 && isIP(realIp)) { return realIp; } } return request.socket.remoteAddress || "unknown"; } export async function verifyTokenAndRateLimit( token: string | null, request?: IncomingMessage, ): Promise<{ isAuthorized: boolean; statusCode?: number; error?: string; }> { if (!token) { return { isAuthorized: false, statusCode: 400, error: "Missing token.", }; } if (!isVerifiedToken(token)) { let isValidToken = false; try { isValidToken = await argon2Verify({ password: getSearchToken(), hash: token, }); } catch (error) { void error; } if (!isValidToken) { return { isAuthorized: false, statusCode: 401, error: "Invalid token.", }; } } // Records a new session or refreshes an active one's last-seen time. addVerifiedToken(token); const rateLimitKey = request ? getClientIp(request) : token; try { await rateLimiter.consume(rateLimitKey); } catch { return { isAuthorized: false, statusCode: 429, error: "Too many requests.", }; } return { isAuthorized: true }; }