136 lines · 5.3 KB
import { env } from "cloudflare:workers";
import { getRequestHeader } from "@tanstack/react-start/server";
import { HttpError } from "./errors";
import type { Viewer } from "~/lib/types";

const HTTP_TOO_MANY = 429;

type LimiterName = "AUTH_LIMITER" | "WRITE_LIMITER" | "HEAVY_LIMITER" | "GIT_LIMITER";

/** The exact client address from Cloudflare; use for logs and Turnstile, not for rate-limit keys. */
export function rawRequestIp(request?: Request): string {
  const header = request ? request.headers.get("cf-connecting-ip") : getRequestHeader("cf-connecting-ip");
  return header ?? "unknown";
}

/**
 * An IPv6 client typically holds a whole /64, so keying on the full address lets it rotate past every
 * per-IP limit. IPv6 collapses to its /64 prefix; IPv4 stays exact.
 */
const IPV6_GROUPS = 8;
const IPV6_PREFIX_GROUPS = 4;

export function ipBucket(ip: string): string {
  if (!ip.includes(":") || ip.includes(".")) return ip;
  const halves = ip.toLowerCase().split("::");
  if (halves.length > 2) return ip;
  const [head, tail = ""] = halves;
  const left = head ? head.split(":") : [];
  const right = tail ? tail.split(":") : [];
  if ([...left, ...right].some((group) => !/^[0-9a-f]{1,4}$/.test(group))) return ip;
  if (!ip.includes("::") && left.length !== IPV6_GROUPS) return ip;
  const fill = Math.max(0, IPV6_GROUPS - left.length - right.length);
  const groups = [...left, ...Array<string>(fill).fill("0"), ...right];
  if (groups.length !== IPV6_GROUPS) return ip;
  return `${groups
    .slice(0, IPV6_PREFIX_GROUPS)
    .map((group) => group.replace(/^0+(?=.)/, ""))
    .join(":")}::/64`;
}