SPB Git

spb/autoscholar

Public
2.7 KB · 79 lines typescript
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/**24 * Source IP as seen by the HTTP layer. `next start` populates X-Forwarded-For with the socket address for direct25 * connections; the MacLustr gateway (Caddy) sets it to the public client address. Returns the first hop or null.26 */27export function sourceIp(req: Request): string | null {28  const xff = req.headers.get("x-forwarded-for");29  const first = xff?.split(",")[0]?.trim() ?? "";30  if (first && isIP(first)) return first;31  const xr = req.headers.get("x-real-ip");32  if (xr && isIP(xr)) return xr;33  return null;34}3536/** True when the request arrived through the public tunnel: the source address is not private/loopback. */37export function viaPublicProxy(req: Request): boolean {38  const ip = sourceIp(req);39  if (!ip) return false;40  return !ipInCidrs(41    ip,42    "127.0.0.0/8,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10,fd00::/8,fe80::/10",43  );44}4546function ipToInt(ip: string): bigint | null {47  if (isIP(ip) === 4) return ip.split(".").reduce((acc, o) => (acc << 8n) + BigInt(Number(o)), 0n);48  if (isIP(ip) === 6) {49    const parts = ip.split("::");50    const head = parts[0] ? parts[0].split(":") : [];51    const tail = parts[1] ? parts[1].split(":") : [];52    const fill = 8 - head.length - tail.length;53    const groups = [...head, ...Array(Math.max(fill, 0)).fill("0"), ...tail];54    return groups.reduce((acc, g) => (acc << 16n) + BigInt(Number.parseInt(g || "0", 16)), 0n);55  }56  return null;57}5859export function ipInCidrs(ip: string | null, cidrs: string): boolean {60  if (!ip) return false;61  const v = isIP(ip);62  const n = ipToInt(ip);63  if (!n) return false;64  for (const raw of cidrs65    .split(",")66    .map((s) => s.trim())67    .filter(Boolean)) {68    const [base, bitsStr] = raw.split("/");69    if (!base || isIP(base) !== v) continue;70    const bits = Number(bitsStr ?? (v === 4 ? 32 : 128));71    const total = v === 4 ? 32 : 128;72    const b = ipToInt(base);73    if (b === null) continue;74    const shift = BigInt(total - bits);75    if (n >> shift === b >> shift) return true;76  }77  return false;78}79
Files · 87b4e55c3b9fbcc2cd191a1e469b33de0a24ccc8
Menu

Theme