add server settings UI and enforcement

This commit is contained in:
Nystik
2026-06-06 17:05:26 +02:00
parent b43d12f702
commit a7824ac284
13 changed files with 497 additions and 33 deletions

View File

@@ -213,7 +213,6 @@ const wss = setupWebSocket(server, {
getVaultPath: config.getVaultPath,
originAllowlist: settings.get("wsOrigins"),
});
app.set("wss", wss);
wireDemoWebSocket(server);
async function gracefulShutdown(signal) {

View File

@@ -100,16 +100,20 @@ router.post("/", async (req, res) => {
return res.status(400).json({ error: "Missing url" });
}
const proxyMode = settings.get("proxyMode");
if (proxyMode === "disabled") {
return res.status(403).json({ error: "Proxy is disabled" });
}
try {
await assertPublicUrl(url);
} catch (e) {
return res.status(e.statusCode || 400).json({ error: e.message });
}
// When a host allowlist is defined , the proxy only reaches those hosts.
const allowlist = settings.get("proxyAllowlist");
if (allowlist.length > 0) {
if (proxyMode === "allowlist") {
const allowlist = settings.get("proxyAllowlist");
const host = new URL(url).hostname;
if (!allowlist.includes(host)) {

View File

@@ -12,11 +12,21 @@ const NUMBER_KEYS = [
"writeCoalesceMs",
"maxBodyBytes",
];
const LIST_KEYS = ["proxyAllowlist", "wsOrigins"];
const LIST_KEYS = ["proxyAllowlist"];
function validate(body) {
const clean = {};
if (body.proxyMode !== undefined) {
if (!settings.PROXY_MODES.includes(body.proxyMode)) {
throw new Error(
`proxyMode must be one of: ${settings.PROXY_MODES.join(", ")}`,
);
}
clean.proxyMode = body.proxyMode;
}
for (const key of NUMBER_KEYS) {
if (body[key] === undefined) {
continue;
@@ -57,14 +67,8 @@ function validate(body) {
return clean;
}
function applySettings(effective, req) {
function applySettings(effective) {
writeCoalescer.configure({ writeCoalesceMs: effective.writeCoalesceMs });
const wss = req.app.get("wss");
if (wss && typeof wss.setOriginAllowlist === "function") {
wss.setOriginAllowlist(effective.wsOrigins);
}
}
router.get("/", (req, res) => {
@@ -81,7 +85,7 @@ router.post("/", (req, res) => {
}
const effective = settings.update(clean);
applySettings(effective, req);
applySettings(effective);
// Cache sizes ride in the bootstrap response; clear it so the next page load picks up new values.
bootstrapRoutes.invalidateAll();

View File

@@ -12,13 +12,20 @@ const DEFAULTS = {
inputCacheTtlMs: 5 * 60 * 1000,
writeCoalesceMs: 5000,
maxBodyBytes: 50 * 1024 * 1024,
// Empty arrays mean "no restriction": any proxy host, any WS origin.
// "any" reaches any public host, "allowlist" restricts to proxyAllowlist, "disabled" blocks all proxying.
proxyMode: "any",
// Empty allows any public host.
proxyAllowlist: [],
wsOrigins: [],
};
const PROXY_MODES = ["any", "allowlist", "disabled"];
const KEYS = Object.keys(DEFAULTS);
// Env vars only; never persisted to the settings file.
const ENV_ONLY_KEYS = ["wsOrigins"];
// Hard ceiling for request bodies.
const MAX_BODY_BACKSTOP = 500 * 1024 * 1024;
@@ -56,6 +63,10 @@ function loadFile() {
const clean = {};
for (const key of KEYS) {
if (ENV_ONLY_KEYS.includes(key)) {
continue;
}
if (parsed[key] !== undefined) {
clean[key] = parsed[key];
}
@@ -80,7 +91,7 @@ function get(key) {
// Merge validated changes into the persisted file and return the new effective settings.
function update(partial) {
for (const [key, value] of Object.entries(partial)) {
if (KEYS.includes(key)) {
if (KEYS.includes(key) && !ENV_ONLY_KEYS.includes(key)) {
fileOverrides[key] = value;
}
}
@@ -91,4 +102,13 @@ function update(partial) {
return getAll();
}
module.exports = { DEFAULTS, KEYS, MAX_BODY_BACKSTOP, getAll, get, update };
module.exports = {
DEFAULTS,
KEYS,
ENV_ONLY_KEYS,
PROXY_MODES,
MAX_BODY_BACKSTOP,
getAll,
get,
update,
};