Initial commit

This commit is contained in:
admin
2026-05-26 22:46:00 +02:00
commit 7e2c2ff89c
256 changed files with 51523 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
import "server-only";
import { NextRequest, NextResponse } from "next/server";
const LOCAL_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]);
function bareHost(value: string | null): string {
if (!value) return "";
const trimmed = value.trim().toLowerCase();
if (trimmed === "::1") return trimmed;
if (trimmed.startsWith("[") && trimmed.includes("]")) {
return trimmed.slice(1, trimmed.indexOf("]"));
}
return trimmed.split(":")[0];
}
function isLocalHost(value: string | null): boolean {
return LOCAL_HOSTS.has(bareHost(value));
}
function trustedLanEnabled(): boolean {
return process.env.PINKUDEX_TRUSTED_LAN === "1";
}
function trustedHostnames(): Set<string> {
const raw = process.env.PINKUDEX_TRUSTED_HOSTNAMES;
if (!raw) return new Set();
return new Set(
raw
.split(",")
.map((v) => v.trim().toLowerCase())
.filter(Boolean),
);
}
function parseIPv4(value: string): number[] | null {
const parts = value.split(".");
if (parts.length !== 4) return null;
const octets: number[] = [];
for (const p of parts) {
if (!/^\d{1,3}$/.test(p)) return null;
const n = Number(p);
if (n < 0 || n > 255) return null;
octets.push(n);
}
return octets;
}
function isPrivateIPv4(value: string): boolean {
const o = parseIPv4(value);
if (!o) return false;
// 127.0.0.0/8 (loopback)
if (o[0] === 127) return true;
// 10.0.0.0/8
if (o[0] === 10) return true;
// 172.16.0.0/12
if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return true;
// 192.168.0.0/16
if (o[0] === 192 && o[1] === 168) return true;
// 100.64.0.0/10 (CGNAT / Tailscale)
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return true;
return false;
}
function isPrivateIPv6(value: string): boolean {
const v = value.toLowerCase();
if (v === "::1") return true;
// fc00::/7 (ULA): first byte 0xfc or 0xfd
if (v.startsWith("fc") || v.startsWith("fd")) return true;
// fe80::/10 (link-local)
if (v.startsWith("fe8") || v.startsWith("fe9") || v.startsWith("fea") || v.startsWith("feb")) return true;
return false;
}
function isTrustedHost(value: string | null): boolean {
const host = bareHost(value);
if (!host) return false;
if (LOCAL_HOSTS.has(host)) return true;
if (!trustedLanEnabled()) return false;
if (host.includes(":") || /^[0-9a-f:]+$/i.test(host)) {
if (isPrivateIPv6(host)) return true;
}
if (isPrivateIPv4(host)) return true;
if (trustedHostnames().has(host)) return true;
return false;
}
function forwardedFor(req: NextRequest): string[] {
const out: string[] = [];
const xff = req.headers.get("x-forwarded-for");
if (xff) out.push(...xff.split(",").map((v) => v.trim()).filter(Boolean));
const realIp = req.headers.get("x-real-ip");
if (realIp) out.push(realIp.trim());
const forwarded = req.headers.get("forwarded");
if (forwarded) {
for (const part of forwarded.split(",")) {
const m = part.match(/(?:^|;)\s*for="?(\[?[^\]";,]+]?)"?/i);
if (m) out.push(m[1].trim());
}
}
return out;
}
function sameOriginHeaderIsTrusted(req: NextRequest, header: "origin" | "referer"): boolean {
const raw = req.headers.get(header);
if (!raw) return true;
try {
return isTrustedHost(new URL(raw).hostname);
} catch {
return false;
}
}
export function assertLocalRequest(req: NextRequest): NextResponse | null {
const hostIsLocal = isTrustedHost(req.headers.get("host")) && isTrustedHost(req.nextUrl.hostname);
const forwarded = forwardedFor(req);
const forwardedIsLocal = forwarded.length === 0 || forwarded.every(isTrustedHost);
const originIsLocal =
sameOriginHeaderIsTrusted(req, "origin") && sameOriginHeaderIsTrusted(req, "referer");
if (hostIsLocal && forwardedIsLocal && originIsLocal) return null;
return NextResponse.json(
{ error: "This endpoint is only available from the local machine." },
{ status: 403 },
);
}
export { isLocalHost };
Binary file not shown.