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>
This commit is contained in:
parent
beece5a383
commit
797d73145e
11
.env.example
11
.env.example
@ -206,6 +206,17 @@ RAKAZO_UPDATER_IMAGE_TAG=local
|
|||||||
RAKAZO_UPDATER_URL=
|
RAKAZO_UPDATER_URL=
|
||||||
RAKAZO_UPDATER_TOKEN=
|
RAKAZO_UPDATER_TOKEN=
|
||||||
|
|
||||||
|
# --- evOo Mission Control (Cockpit on port 3088) ---
|
||||||
|
PORT=3088
|
||||||
|
EVOO_COCKPIT_HOST=127.0.0.1
|
||||||
|
# Required when EVOO_COCKPIT_HOST binds beyond loopback (e.g. 0.0.0.0 for LAN/Tailscale).
|
||||||
|
# Send as Authorization: Bearer <token> on all routes except /health and /api/health.
|
||||||
|
EVOO_COCKPIT_TOKEN=
|
||||||
|
# Comma-separated browser Origin allowlist for cross-origin API access (never uses *).
|
||||||
|
# Loopback binds default to http://127.0.0.1:5173 and http://localhost:5173 when unset.
|
||||||
|
# Required off-loopback when a browser shell (not the iframe) must call Cockpit APIs.
|
||||||
|
EVOO_COCKPIT_CORS_ORIGIN=
|
||||||
|
|
||||||
# Optional default UI locale for the web SPA (en | de | ko | tr | hi | pt-BR | zh-CN).
|
# Optional default UI locale for the web SPA (en | de | ko | tr | hi | pt-BR | zh-CN).
|
||||||
# Overridden by localStorage key rakazo.uiLocale when the user picks a language in Settings.
|
# Overridden by localStorage key rakazo.uiLocale when the user picks a language in Settings.
|
||||||
# VITE_DEFAULT_UI_LOCALE=en
|
# VITE_DEFAULT_UI_LOCALE=en
|
||||||
|
|||||||
135
evoo-platform/server-config.js
Normal file
135
evoo-platform/server-config.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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}`;
|
||||||
|
}
|
||||||
207
evoo-platform/server-config.test.js
Normal file
207
evoo-platform/server-config.test.js
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
applyCockpitCorsHeaders,
|
||||||
|
formatCockpitListenUrl,
|
||||||
|
hasValidBearerToken,
|
||||||
|
isLoopbackHost,
|
||||||
|
isPrivilegedCockpitRoute,
|
||||||
|
isPublicCockpitRoute,
|
||||||
|
resolveAllowedCorsOrigin,
|
||||||
|
resolveCockpitConfig,
|
||||||
|
resolveCockpitCorsConfig,
|
||||||
|
} from "./server-config.js";
|
||||||
|
|
||||||
|
describe("resolveCockpitConfig", () => {
|
||||||
|
it("binds to loopback by default without requiring a token", () => {
|
||||||
|
expect(resolveCockpitConfig({})).toEqual({
|
||||||
|
port: 3088,
|
||||||
|
host: "127.0.0.1",
|
||||||
|
token: null,
|
||||||
|
requiresAuth: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("honours explicit loopback hosts", () => {
|
||||||
|
for (const host of ["127.0.0.1", "localhost", "::1"]) {
|
||||||
|
expect(resolveCockpitConfig({ EVOO_COCKPIT_HOST: host })).toMatchObject({
|
||||||
|
host,
|
||||||
|
token: null,
|
||||||
|
requiresAuth: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires a shared token before binding beyond loopback", () => {
|
||||||
|
expect(() => resolveCockpitConfig({ EVOO_COCKPIT_HOST: "0.0.0.0" })).toThrow(
|
||||||
|
/EVOO_COCKPIT_TOKEN/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows a non-loopback bind when a token is configured", () => {
|
||||||
|
expect(
|
||||||
|
resolveCockpitConfig({
|
||||||
|
EVOO_COCKPIT_HOST: "0.0.0.0",
|
||||||
|
EVOO_COCKPIT_TOKEN: "cockpit-review-token-000000000000",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
port: 3088,
|
||||||
|
host: "0.0.0.0",
|
||||||
|
token: "cockpit-review-token-000000000000",
|
||||||
|
requiresAuth: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid ports", () => {
|
||||||
|
expect(() => resolveCockpitConfig({ PORT: "0" })).toThrow(/PORT/);
|
||||||
|
expect(() => resolveCockpitConfig({ PORT: "abc" })).toThrow(/PORT/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cockpit auth helpers", () => {
|
||||||
|
const token = "cockpit-review-token-000000000000";
|
||||||
|
|
||||||
|
it("treats health routes as public when auth is enabled", () => {
|
||||||
|
expect(isPublicCockpitRoute("/health")).toBe(true);
|
||||||
|
expect(isPublicCockpitRoute("/api/health")).toBe(true);
|
||||||
|
expect(isPublicCockpitRoute("/api/status")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts only bearer tokens with constant-time comparison", () => {
|
||||||
|
expect(hasValidBearerToken(`Bearer ${token}`, token)).toBe(true);
|
||||||
|
expect(hasValidBearerToken(`Bearer ${`${token}x`}`, token)).toBe(false);
|
||||||
|
expect(hasValidBearerToken(token, token)).toBe(false);
|
||||||
|
expect(hasValidBearerToken(undefined, token)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats listen URLs with bracketed IPv6 hosts", () => {
|
||||||
|
expect(formatCockpitListenUrl("127.0.0.1", 3088)).toBe("http://127.0.0.1:3088");
|
||||||
|
expect(formatCockpitListenUrl("::1", 3088)).toBe("http://[::1]:3088");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isLoopbackHost", () => {
|
||||||
|
it("recognises loopback hosts only", () => {
|
||||||
|
expect(isLoopbackHost("127.0.0.1")).toBe(true);
|
||||||
|
expect(isLoopbackHost("localhost")).toBe(true);
|
||||||
|
expect(isLoopbackHost("::1")).toBe(true);
|
||||||
|
expect(isLoopbackHost("0.0.0.0")).toBe(false);
|
||||||
|
expect(isLoopbackHost("192.168.1.10")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isPrivilegedCockpitRoute", () => {
|
||||||
|
it("marks API routes except health as privileged", () => {
|
||||||
|
expect(isPrivilegedCockpitRoute("/health")).toBe(false);
|
||||||
|
expect(isPrivilegedCockpitRoute("/api/health")).toBe(false);
|
||||||
|
expect(isPrivilegedCockpitRoute("/api/status")).toBe(true);
|
||||||
|
expect(isPrivilegedCockpitRoute("/api/trading-bot/mode")).toBe(true);
|
||||||
|
expect(isPrivilegedCockpitRoute("/index.html")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveCockpitCorsConfig", () => {
|
||||||
|
const loopbackCockpit = resolveCockpitConfig({ EVOO_COCKPIT_HOST: "127.0.0.1" });
|
||||||
|
const publicCockpit = resolveCockpitConfig({
|
||||||
|
EVOO_COCKPIT_HOST: "0.0.0.0",
|
||||||
|
EVOO_COCKPIT_TOKEN: "cockpit-review-token-000000000000",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to local web origins on loopback binds", () => {
|
||||||
|
const cors = resolveCockpitCorsConfig({}, loopbackCockpit);
|
||||||
|
expect(cors.loopbackBind).toBe(true);
|
||||||
|
expect(cors.usesDefaultLoopbackOrigins).toBe(true);
|
||||||
|
expect(cors.allowedOrigins).toContain("http://127.0.0.1:5173");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires an explicit allowlist off loopback", () => {
|
||||||
|
const cors = resolveCockpitCorsConfig({}, publicCockpit);
|
||||||
|
expect(cors.allowedOrigins).toEqual([]);
|
||||||
|
expect(cors.usesDefaultLoopbackOrigins).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("honours EVOO_COCKPIT_CORS_ORIGIN when set", () => {
|
||||||
|
const cors = resolveCockpitCorsConfig(
|
||||||
|
{ EVOO_COCKPIT_CORS_ORIGIN: "https://ops.example.com, https://ops.example.com" },
|
||||||
|
publicCockpit,
|
||||||
|
);
|
||||||
|
expect(cors.allowedOrigins).toEqual(["https://ops.example.com"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveAllowedCorsOrigin", () => {
|
||||||
|
const loopbackCors = resolveCockpitCorsConfig({}, resolveCockpitConfig({}));
|
||||||
|
const publicCors = resolveCockpitCorsConfig(
|
||||||
|
{ EVOO_COCKPIT_CORS_ORIGIN: "https://ops.example.com" },
|
||||||
|
resolveCockpitConfig({
|
||||||
|
EVOO_COCKPIT_HOST: "0.0.0.0",
|
||||||
|
EVOO_COCKPIT_TOKEN: "cockpit-review-token-000000000000",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const blockedPublicCors = resolveCockpitCorsConfig(
|
||||||
|
{},
|
||||||
|
resolveCockpitConfig({
|
||||||
|
EVOO_COCKPIT_HOST: "0.0.0.0",
|
||||||
|
EVOO_COCKPIT_TOKEN: "cockpit-review-token-000000000000",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
it("never returns a wildcard and omits headers without an Origin", () => {
|
||||||
|
expect(resolveAllowedCorsOrigin(undefined, loopbackCors, "/api/status")).toBeNull();
|
||||||
|
expect(resolveAllowedCorsOrigin("", loopbackCors, "/api/status")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reflects allowlisted origins on privileged routes", () => {
|
||||||
|
expect(resolveAllowedCorsOrigin("http://127.0.0.1:5173", loopbackCors, "/api/status")).toBe(
|
||||||
|
"http://127.0.0.1:5173",
|
||||||
|
);
|
||||||
|
expect(resolveAllowedCorsOrigin("https://evil.example", loopbackCors, "/api/status")).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("blocks cross-origin privileged access off loopback without an allowlist", () => {
|
||||||
|
expect(
|
||||||
|
resolveAllowedCorsOrigin("https://ops.example.com", blockedPublicCors, "/api/status"),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows configured origins off loopback", () => {
|
||||||
|
expect(
|
||||||
|
resolveAllowedCorsOrigin("https://ops.example.com", publicCors, "/api/trading-bot"),
|
||||||
|
).toBe("https://ops.example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyCockpitCorsHeaders", () => {
|
||||||
|
it("sets reflected origins and never sets *", () => {
|
||||||
|
const cors = resolveCockpitCorsConfig({}, resolveCockpitConfig({}));
|
||||||
|
const headers = {};
|
||||||
|
const res = { setHeader: (key, value) => { headers[key] = value; } };
|
||||||
|
const allowed = applyCockpitCorsHeaders(
|
||||||
|
{ headers: { origin: "http://127.0.0.1:5173" } },
|
||||||
|
res,
|
||||||
|
"/api/status",
|
||||||
|
cors,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(allowed).toBe("http://127.0.0.1:5173");
|
||||||
|
expect(headers["Access-Control-Allow-Origin"]).toBe("http://127.0.0.1:5173");
|
||||||
|
expect(headers["Access-Control-Allow-Origin"]).not.toBe("*");
|
||||||
|
expect(headers["Access-Control-Allow-Headers"]).toContain("Authorization");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits Access-Control-Allow-Origin for disallowed cross-origin requests", () => {
|
||||||
|
const cors = resolveCockpitCorsConfig({}, resolveCockpitConfig({}));
|
||||||
|
const headers = {};
|
||||||
|
const res = { setHeader: (key, value) => { headers[key] = value; } };
|
||||||
|
const allowed = applyCockpitCorsHeaders(
|
||||||
|
{ headers: { origin: "https://evil.example" } },
|
||||||
|
res,
|
||||||
|
"/api/status",
|
||||||
|
cors,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(allowed).toBe(false);
|
||||||
|
expect(headers["Access-Control-Allow-Origin"]).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -11,11 +11,21 @@ import http from "node:http";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import {
|
||||||
|
applyCockpitCorsHeaders,
|
||||||
|
formatCockpitListenUrl,
|
||||||
|
hasValidBearerToken,
|
||||||
|
isPublicCockpitRoute,
|
||||||
|
resolveCockpitConfig,
|
||||||
|
resolveCockpitCorsConfig,
|
||||||
|
} from "./server-config.js";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || "3088", 10);
|
const cockpit = resolveCockpitConfig(process.env);
|
||||||
|
const corsConfig = resolveCockpitCorsConfig(process.env, cockpit);
|
||||||
|
const { port: PORT, host: COCKPIT_HOST } = cockpit;
|
||||||
const BASE_DIR = __dirname;
|
const BASE_DIR = __dirname;
|
||||||
const ROOT_DIR = path.resolve(BASE_DIR, "..");
|
const ROOT_DIR = path.resolve(BASE_DIR, "..");
|
||||||
|
|
||||||
@ -275,19 +285,30 @@ async function getLiveCapitalTelemetry() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
// CORS & Security Headers
|
const parsedUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
||||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
const urlPath = parsedUrl.pathname;
|
||||||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
const corsAllowed = applyCockpitCorsHeaders(req, res, urlPath, corsConfig);
|
||||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
if (req.method === "OPTIONS") {
|
||||||
|
if (corsAllowed === false && req.headers.origin) {
|
||||||
|
res.writeHead(403);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
res.writeHead(204);
|
res.writeHead(204);
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
if (
|
||||||
const urlPath = parsedUrl.pathname;
|
cockpit.requiresAuth &&
|
||||||
|
!isPublicCockpitRoute(urlPath) &&
|
||||||
|
!hasValidBearerToken(req.headers.authorization, cockpit.token)
|
||||||
|
) {
|
||||||
|
res.writeHead(401, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ error: "unauthorized" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. API Health Check
|
// 1. API Health Check
|
||||||
if (urlPath === "/health" || urlPath === "/api/health") {
|
if (urlPath === "/health" || urlPath === "/api/health") {
|
||||||
@ -690,12 +711,21 @@ const server = http.createServer(async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(PORT, "0.0.0.0", () => {
|
server.listen(PORT, COCKPIT_HOST, () => {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(path.join(ROOT_DIR, ".evoo_server.pid"), process.pid.toString(), "utf8");
|
fs.writeFileSync(path.join(ROOT_DIR, ".evoo_server.pid"), process.pid.toString(), "utf8");
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// Ignore if write fails
|
// Ignore if write fails
|
||||||
}
|
}
|
||||||
console.log(`[evOo Core] Server listening on http://127.0.0.1:${PORT} (PID: ${process.pid})`);
|
const listenUrl = formatCockpitListenUrl(COCKPIT_HOST, PORT);
|
||||||
console.log(`[evOo Core] Healthcheck available at http://127.0.0.1:${PORT}/health`);
|
console.log(`[evOo Core] Server listening on ${listenUrl} (PID: ${process.pid})`);
|
||||||
|
console.log(`[evOo Core] Healthcheck available at ${listenUrl}/health`);
|
||||||
|
if (cockpit.requiresAuth) {
|
||||||
|
console.log("[evOo Core] Non-loopback bind: Bearer token required except /health");
|
||||||
|
}
|
||||||
|
if (!corsConfig.loopbackBind && corsConfig.allowedOrigins.length === 0) {
|
||||||
|
console.log(
|
||||||
|
"[evOo Core] Non-loopback bind: set EVOO_COCKPIT_CORS_ORIGIN for browser cross-origin access",
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export default defineConfig({
|
|||||||
"apps/mobile/plugins/**/*.test.js",
|
"apps/mobile/plugins/**/*.test.js",
|
||||||
"apps/api/src/**/*.test.ts",
|
"apps/api/src/**/*.test.ts",
|
||||||
"apps/www/src/**/*.test.ts",
|
"apps/www/src/**/*.test.ts",
|
||||||
|
"evoo-platform/**/*.test.js",
|
||||||
],
|
],
|
||||||
testTimeout: 30_000,
|
testTimeout: 30_000,
|
||||||
hookTimeout: 60_000,
|
hookTimeout: 60_000,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user