80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
type OriginRequest = Pick<Request, "url" | "headers" | "method">;
|
|
type OriginConfiguration = {
|
|
websiteUrl?: string;
|
|
additionalOrigins?: string;
|
|
allowAnyOrigin?: boolean;
|
|
};
|
|
|
|
function parseOrigin(value: string): URL {
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(value);
|
|
} catch {
|
|
throw new Error("Invalid request origin");
|
|
}
|
|
if (
|
|
!["http:", "https:"].includes(parsed.protocol) ||
|
|
parsed.username ||
|
|
parsed.password
|
|
) {
|
|
throw new Error("Invalid request origin");
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
const originOf = (value: string) => parseOrigin(value).origin;
|
|
|
|
/** Next may build request.url using the listening address rather than the LAN host. */
|
|
export function requestOrigin(
|
|
request: Pick<Request, "url" | "headers">,
|
|
websiteUrl?: string,
|
|
): string {
|
|
const url = new URL(request.url);
|
|
const host = request.headers.get("host");
|
|
if (!host) return originOf(url.origin);
|
|
// Use the direct request's authority, including its port. Forwarded hosts are
|
|
// intentionally excluded; proxy deployments can configure their public URL.
|
|
if (/[\s/\\?#@,]/.test(host)) throw new Error("Invalid request origin");
|
|
const direct = parseOrigin(`${url.protocol}//${host}`);
|
|
// Preserve HTTPS when a configured public host terminates TLS at a proxy.
|
|
// A different configured host must not redirect a LAN login to localhost.
|
|
if (websiteUrl) {
|
|
const configured = parseOrigin(websiteUrl);
|
|
if (configured.host === direct.host) return configured.origin;
|
|
}
|
|
return direct.origin;
|
|
}
|
|
|
|
export function verifyOrigin(
|
|
request: OriginRequest,
|
|
configuration: OriginConfiguration = {
|
|
websiteUrl: process.env.MINEGEO_URL,
|
|
additionalOrigins: process.env.MINEGEO_ALLOWED_ORIGINS,
|
|
allowAnyOrigin: process.env.MINEGEO_ALLOW_ANY_ORIGIN === "true",
|
|
},
|
|
) {
|
|
if (request.method === "GET" || request.method === "HEAD") return;
|
|
const origin = request.headers.get("origin");
|
|
if (origin !== null) {
|
|
if (configuration.allowAnyOrigin) return;
|
|
const parsed = parseOrigin(origin);
|
|
if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
throw new Error("Invalid request origin");
|
|
}
|
|
const allowed = new Set([requestOrigin(request)]);
|
|
for (const value of [
|
|
configuration.websiteUrl,
|
|
...(configuration.additionalOrigins?.split(",") ?? []),
|
|
]) {
|
|
if (value?.trim()) allowed.add(originOf(value.trim()));
|
|
}
|
|
if (!allowed.has(originOf(origin)))
|
|
throw new Error("Invalid request origin");
|
|
} else if (
|
|
!request.headers.get("authorization") &&
|
|
!request.headers.get("content-type")?.includes("application/json")
|
|
) {
|
|
throw new Error("JSON request required");
|
|
}
|
|
}
|