spb/autoscholar
Public
1import "server-only";2import { isIP } from "node:net";3import { getEnv } from "@autoscholar/core";45/** Client IP: honours X-Forwarded-For only when TRUST_PROXY=1 (behind the MacLustr Tunnel Caddy). */6export function clientIp(req: Request): string | null {7 const env = getEnv();8 const h = req.headers;9 if (env.TRUST_PROXY === 1) {10 const xff = h.get("x-forwarded-for");11 if (xff) {12 const first = xff.split(",")[0]?.trim() ?? "";13 if (isIP(first)) return first;14 }15 const xr = h.get("x-real-ip");16 if (xr && isIP(xr)) return xr;17 }18 const direct = h.get("x-autoscholar-remote-addr"); // set by nothing public; Next does not expose the socket address19 if (direct && isIP(direct)) return direct;20 return null;21}2223/** True when the request arrived through the public tunnel (Caddy adds X-Forwarded-*). */24export function viaPublicProxy(req: Request): boolean {25 return !!(26 req.headers.get("x-forwarded-for") ||27 req.headers.get("x-forwarded-proto") ||28 req.headers.get("x-forwarded-host")29 );30}3132function ipToInt(ip: string): bigint | null {33 if (isIP(ip) === 4) return ip.split(".").reduce((acc, o) => (acc << 8n) + BigInt(Number(o)), 0n);34 if (isIP(ip) === 6) {35 const parts = ip.split("::");36 const head = parts[0] ? parts[0].split(":") : [];37 const tail = parts[1] ? parts[1].split(":") : [];38 const fill = 8 - head.length - tail.length;39 const groups = [...head, ...Array(Math.max(fill, 0)).fill("0"), ...tail];40 return groups.reduce((acc, g) => (acc << 16n) + BigInt(Number.parseInt(g || "0", 16)), 0n);41 }42 return null;43}4445export function ipInCidrs(ip: string | null, cidrs: string): boolean {46 if (!ip) return false;47 const v = isIP(ip);48 const n = ipToInt(ip);49 if (!n) return false;50 for (const raw of cidrs51 .split(",")52 .map((s) => s.trim())53 .filter(Boolean)) {54 const [base, bitsStr] = raw.split("/");55 if (!base || isIP(base) !== v) continue;56 const bits = Number(bitsStr ?? (v === 4 ? 32 : 128));57 const total = v === 4 ? 32 : 128;58 const b = ipToInt(base);59 if (b === null) continue;60 const shift = BigInt(total - bits);61 if (n >> shift === b >> shift) return true;62 }63 return false;64}65