119 lines
2.7 KiB
TypeScript
119 lines
2.7 KiB
TypeScript
import { fail } from "@/lib/api";
|
|
import { getPublicBaseUrl } from "@/lib/public-url";
|
|
|
|
const DEFAULT_MAX_JSON_BYTES = 64 * 1024;
|
|
|
|
function parseContentLength(value: string | null): number | null {
|
|
if (!value) return null;
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
|
return Math.floor(parsed);
|
|
}
|
|
|
|
function mutationMethod(method: string) {
|
|
const normalized = method.toUpperCase();
|
|
return normalized === "POST" || normalized === "PUT" || normalized === "PATCH" || normalized === "DELETE";
|
|
}
|
|
|
|
function isAllowedOrigin(origin: string, req: Request) {
|
|
const allowedOrigins = new Set<string>();
|
|
|
|
try {
|
|
allowedOrigins.add(new URL(req.url).origin);
|
|
} catch {
|
|
// Ignore invalid request URL.
|
|
}
|
|
|
|
try {
|
|
allowedOrigins.add(new URL(getPublicBaseUrl()).origin);
|
|
} catch {
|
|
// Ignore invalid public URL config.
|
|
}
|
|
|
|
if (process.env.NEXTAUTH_URL) {
|
|
try {
|
|
allowedOrigins.add(new URL(process.env.NEXTAUTH_URL).origin);
|
|
} catch {
|
|
// Ignore invalid NEXTAUTH_URL config.
|
|
}
|
|
}
|
|
|
|
return allowedOrigins.has(origin);
|
|
}
|
|
|
|
export function validateMutationRequestOrigin(req: Request) {
|
|
if (!mutationMethod(req.method)) return null;
|
|
|
|
const fetchSite = (req.headers.get("sec-fetch-site") ?? "").toLowerCase();
|
|
if (fetchSite === "cross-site") {
|
|
return fail("Ungültige Anfrage-Herkunft", 403);
|
|
}
|
|
|
|
const originHeader = req.headers.get("origin");
|
|
if (!originHeader) return null;
|
|
|
|
let origin: string;
|
|
try {
|
|
origin = new URL(originHeader).origin;
|
|
} catch {
|
|
return fail("Ungültige Anfrage-Herkunft", 403);
|
|
}
|
|
|
|
if (!isAllowedOrigin(origin, req)) {
|
|
return fail("Ungültige Anfrage-Herkunft", 403);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function readJsonBody(
|
|
req: Request,
|
|
options?: { maxBytes?: number }
|
|
) {
|
|
const maxBytes = options?.maxBytes ?? DEFAULT_MAX_JSON_BYTES;
|
|
const contentLength = parseContentLength(req.headers.get("content-length"));
|
|
|
|
if (contentLength !== null && contentLength > maxBytes) {
|
|
return {
|
|
ok: false as const,
|
|
response: fail("Anfrage ist zu groß", 413)
|
|
};
|
|
}
|
|
|
|
let raw = "";
|
|
try {
|
|
raw = await req.text();
|
|
} catch {
|
|
return {
|
|
ok: false as const,
|
|
response: fail("Anfrage konnte nicht gelesen werden", 400)
|
|
};
|
|
}
|
|
|
|
if (Buffer.byteLength(raw, "utf8") > maxBytes) {
|
|
return {
|
|
ok: false as const,
|
|
response: fail("Anfrage ist zu groß", 413)
|
|
};
|
|
}
|
|
|
|
if (!raw.trim()) {
|
|
return {
|
|
ok: true as const,
|
|
data: {}
|
|
};
|
|
}
|
|
|
|
try {
|
|
return {
|
|
ok: true as const,
|
|
data: JSON.parse(raw) as unknown
|
|
};
|
|
} catch {
|
|
return {
|
|
ok: false as const,
|
|
response: fail("Ungültiges JSON", 400)
|
|
};
|
|
}
|
|
}
|