evOo-local/evoo-platform/server-config.js
Cursor Agent 797d73145e fix(cockpit): replace blanket CORS * with origin allowlist
Mission Control previously set Access-Control-Allow-Origin: * on every
route. That widens cross-origin browser access when Cockpit binds beyond
loopback, even with bearer auth.

Add server-config helpers compatible with PR #28 bind/token patterns:
- loopback binds default to local web/API origins for the Heute shell
- privileged /api/* routes never receive *; only reflected allowlist hits
- non-loopback binds require explicit EVOO_COCKPIT_CORS_ORIGIN
- default bind moves to 127.0.0.1 with token gate off-loopback

Document env vars in .env.example and cover config/CORS in unit tests.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
2026-09-22 06:23:52 +00:00

136 lines
4.3 KiB
JavaScript

const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
/** Default web/API origins allowed on loopback binds for the Heute iframe shell. */
const DEFAULT_LOOPBACK_WEB_ORIGINS = [
"http://127.0.0.1:5173",
"http://localhost:5173",
"http://127.0.0.1:3100",
"http://localhost:3100",
];
export function isLoopbackHost(host) {
return LOOPBACK_HOSTS.has(host.trim());
}
export function timingSafeStringEqual(supplied, expected) {
const actual = Buffer.from(expected);
const candidate = Buffer.from(supplied ?? "");
if (actual.length !== candidate.length) return false;
let difference = 0;
for (let index = 0; index < actual.length; index += 1) {
difference |= actual[index] ^ candidate[index];
}
return difference === 0;
}
export function hasValidBearerToken(authorization, expectedToken) {
const supplied = authorization?.startsWith("Bearer ") ? authorization.slice(7) : "";
return timingSafeStringEqual(supplied, expectedToken);
}
export function isPublicCockpitRoute(pathname) {
return pathname === "/health" || pathname === "/api/health";
}
/** Privileged API routes must never receive Access-Control-Allow-Origin: *. */
export function isPrivilegedCockpitRoute(pathname) {
if (isPublicCockpitRoute(pathname)) return false;
return pathname.startsWith("/api/");
}
export function parseCorsOriginAllowlist(value) {
if (!value?.trim()) return [];
return [...new Set(value.split(",").map((entry) => entry.trim()).filter(Boolean))];
}
/**
* Cockpit bind/auth settings. Defaults to loopback-only with no token so local
* Mission Control keeps working out of the box.
*/
export function resolveCockpitConfig(env) {
const port = Number.parseInt(env.PORT?.trim() || "3088", 10);
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
throw new Error("PORT is not a valid port number.");
}
const host = env.EVOO_COCKPIT_HOST?.trim() || "127.0.0.1";
const loopback = isLoopbackHost(host);
const token = env.EVOO_COCKPIT_TOKEN?.trim() || "";
if (!loopback && token === "") {
throw new Error(
"Set EVOO_COCKPIT_TOKEN when EVOO_COCKPIT_HOST is not loopback (127.0.0.1, localhost, or ::1).",
);
}
return {
port,
host,
token: loopback ? null : token,
requiresAuth: !loopback,
};
}
/**
* CORS allowlist. Loopback binds default to local web/API origins; non-loopback
* binds require an explicit EVOO_COCKPIT_CORS_ORIGIN comma-separated list.
*/
export function resolveCockpitCorsConfig(env, cockpitConfig) {
const explicit = parseCorsOriginAllowlist(env.EVOO_COCKPIT_CORS_ORIGIN);
const loopbackBind = isLoopbackHost(cockpitConfig.host);
if (!loopbackBind && explicit.length === 0) {
return {
loopbackBind: false,
allowedOrigins: [],
usesDefaultLoopbackOrigins: false,
};
}
const usesDefaultLoopbackOrigins = loopbackBind && explicit.length === 0;
const allowedOrigins = usesDefaultLoopbackOrigins ? DEFAULT_LOOPBACK_WEB_ORIGINS : explicit;
return {
loopbackBind,
allowedOrigins,
usesDefaultLoopbackOrigins,
};
}
/**
* Resolve the Access-Control-Allow-Origin value for a request.
* Returns a reflected origin string, null when CORS headers should be omitted,
* or false when the Origin is present but not allowed.
*/
export function resolveAllowedCorsOrigin(requestOrigin, corsConfig, pathname) {
const origin = requestOrigin?.trim() ?? "";
if (!origin) return null;
if (isPrivilegedCockpitRoute(pathname)) {
return corsConfig.allowedOrigins.includes(origin) ? origin : false;
}
if (corsConfig.loopbackBind && corsConfig.usesDefaultLoopbackOrigins) {
return corsConfig.allowedOrigins.includes(origin) ? origin : false;
}
return corsConfig.allowedOrigins.includes(origin) ? origin : false;
}
export function applyCockpitCorsHeaders(req, res, pathname, corsConfig) {
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
const allowed = resolveAllowedCorsOrigin(req.headers.origin, corsConfig, pathname);
if (typeof allowed === "string") {
res.setHeader("Access-Control-Allow-Origin", allowed);
res.setHeader("Vary", "Origin");
}
return allowed;
}
export function formatCockpitListenUrl(host, port) {
const displayHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
return `http://${displayHost}:${port}`;
}