Files
MineGeo/lib/request-origin.ts

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");
}
}