From b43d12f702e41fa5eba42a20a2dab3b3a8d568cf Mon Sep 17 00:00:00 2001 From: Nystik <236107-Nystik@users.noreply.gitlab.com> Date: Sat, 6 Jun 2026 13:04:34 +0200 Subject: [PATCH] add server settings api --- apps/ignis-server/server/demo/index.js | 5 ++ apps/ignis-server/server/index.js | 16 +++- apps/ignis-server/server/routes/bootstrap.js | 11 +++ apps/ignis-server/server/routes/proxy.js | 14 +++ apps/ignis-server/server/routes/settings.js | 92 ++++++++++++++++++++ apps/ignis-server/server/settings.js | 5 +- 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 apps/ignis-server/server/routes/settings.js diff --git a/apps/ignis-server/server/demo/index.js b/apps/ignis-server/server/demo/index.js index ab0b85d..11f1b3a 100644 --- a/apps/ignis-server/server/demo/index.js +++ b/apps/ignis-server/server/demo/index.js @@ -70,6 +70,11 @@ function setupDemo(app) { // Hide server-side plugins (headless-sync) from the demo UI app.use("/api/plugins", pluginsBlocker); + // Server settings are-fixed in demo mode. + app.use("/api/settings", (req, res) => { + res.status(403).json({ error: "Settings are disabled in demo mode" }); + }); + // Cleanup timer const interval = setInterval(() => { cleanupExpired().catch((e) => diff --git a/apps/ignis-server/server/index.js b/apps/ignis-server/server/index.js index dc94591..1ad05ec 100644 --- a/apps/ignis-server/server/index.js +++ b/apps/ignis-server/server/index.js @@ -33,7 +33,18 @@ const ANSI_RESET = "\x1b[0m"; const app = express(); -app.use(express.json({ limit: "50mb" })); +// Reject oversized requests by Content-Length before parsing. +app.use((req, res, next) => { + const declared = Number(req.headers["content-length"]); + + if (Number.isFinite(declared) && declared > settings.get("maxBodyBytes")) { + return res.status(413).json({ error: "Request body too large" }); + } + + next(); +}); + +app.use(express.json({ limit: settings.MAX_BODY_BACKSTOP })); app.use(compression()); // logger middleware @@ -67,6 +78,7 @@ const fsRoutes = require("./routes/fs"); const vaultRoutes = require("./routes/vault"); const proxyRoutes = require("./routes/proxy"); const versionRoutes = require("./routes/version"); +const settingsRoutes = require("./routes/settings"); const bootstrapRoutes = require("./routes/bootstrap"); app.use("/assets", express.static(path.join(__dirname, "assets"))); @@ -79,6 +91,7 @@ app.use("/api/fs", fsRoutes); app.use("/api/vault", vaultRoutes); app.use("/api/proxy", proxyRoutes); app.use("/api/version", versionRoutes); +app.use("/api/settings", settingsRoutes); app.use("/api/plugins", pluginRoutes); app.use("/api/bootstrap", bootstrapRoutes); @@ -200,6 +213,7 @@ const wss = setupWebSocket(server, { getVaultPath: config.getVaultPath, originAllowlist: settings.get("wsOrigins"), }); +app.set("wss", wss); wireDemoWebSocket(server); async function gracefulShutdown(signal) { diff --git a/apps/ignis-server/server/routes/bootstrap.js b/apps/ignis-server/server/routes/bootstrap.js index 04fdc99..11e97cf 100644 --- a/apps/ignis-server/server/routes/bootstrap.js +++ b/apps/ignis-server/server/routes/bootstrap.js @@ -14,6 +14,7 @@ const { getVirtualPluginsForVault, } = require("../plugin-system/manager"); const { getVersion } = require("../version"); +const settings = require("../settings"); const router = express.Router(); @@ -140,6 +141,11 @@ async function buildEntry(vaultId) { // In demo mode, hide server-side plugins from the client. plugins: config.demoMode ? [] : getDiscoveredPlugins(), virtualPlugins: getVirtualPluginsForVault(vaultId, getVersion()), + settings: { + contentCacheBytes: settings.get("contentCacheBytes"), + inputCacheBytes: settings.get("inputCacheBytes"), + inputCacheTtlMs: settings.get("inputCacheTtlMs"), + }, }; const jsonBuf = Buffer.from(JSON.stringify(response)); @@ -185,6 +191,10 @@ function invalidateVault(vaultId) { cache.delete(vaultId); } +function invalidateAll() { + cache.clear(); +} + async function warmUp() { const ids = Object.keys(config.vaults); @@ -251,4 +261,5 @@ router.get("/", async (req, res) => { module.exports = router; module.exports.invalidateVault = invalidateVault; +module.exports.invalidateAll = invalidateAll; module.exports.warmUp = warmUp; diff --git a/apps/ignis-server/server/routes/proxy.js b/apps/ignis-server/server/routes/proxy.js index a55f19b..ac562b2 100644 --- a/apps/ignis-server/server/routes/proxy.js +++ b/apps/ignis-server/server/routes/proxy.js @@ -1,6 +1,7 @@ const express = require("express"); const dns = require("dns").promises; const net = require("net"); +const settings = require("../settings"); const router = express.Router(); @@ -105,6 +106,19 @@ router.post("/", async (req, res) => { 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) { + const host = new URL(url).hostname; + + if (!allowlist.includes(host)) { + return res + .status(403) + .json({ error: `Host not in proxy allowlist: ${host}` }); + } + } + try { // Forward the caller's headers as-is. const fetchOpts = { diff --git a/apps/ignis-server/server/routes/settings.js b/apps/ignis-server/server/routes/settings.js new file mode 100644 index 0000000..540e83d --- /dev/null +++ b/apps/ignis-server/server/routes/settings.js @@ -0,0 +1,92 @@ +const express = require("express"); +const { writeCoalescer } = require("@ignis/server-core"); +const settings = require("../settings"); +const bootstrapRoutes = require("./bootstrap"); + +const router = express.Router(); + +const NUMBER_KEYS = [ + "contentCacheBytes", + "inputCacheBytes", + "inputCacheTtlMs", + "writeCoalesceMs", + "maxBodyBytes", +]; +const LIST_KEYS = ["proxyAllowlist", "wsOrigins"]; + +function validate(body) { + const clean = {}; + + for (const key of NUMBER_KEYS) { + if (body[key] === undefined) { + continue; + } + + const n = body[key]; + + if (!Number.isInteger(n) || n < 0) { + throw new Error(`${key} must be a non-negative integer`); + } + + if (key === "maxBodyBytes" && (n < 1 || n > settings.MAX_BODY_BACKSTOP)) { + throw new Error( + `maxBodyBytes must be between 1 and ${settings.MAX_BODY_BACKSTOP}`, + ); + } + + clean[key] = n; + } + + for (const key of LIST_KEYS) { + if (body[key] === undefined) { + continue; + } + + const list = body[key]; + + if ( + !Array.isArray(list) || + list.some((v) => typeof v !== "string" || !v.trim()) + ) { + throw new Error(`${key} must be an array of non-empty strings`); + } + + clean[key] = list.map((v) => v.trim()); + } + + return clean; +} + +function applySettings(effective, req) { + 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) => { + res.json(settings.getAll()); +}); + +router.post("/", (req, res) => { + let clean; + + try { + clean = validate(req.body || {}); + } catch (e) { + return res.status(400).json({ error: e.message }); + } + + const effective = settings.update(clean); + applySettings(effective, req); + + // Cache sizes ride in the bootstrap response; clear it so the next page load picks up new values. + bootstrapRoutes.invalidateAll(); + + res.json(effective); +}); + +module.exports = router; diff --git a/apps/ignis-server/server/settings.js b/apps/ignis-server/server/settings.js index 682cd18..036e952 100644 --- a/apps/ignis-server/server/settings.js +++ b/apps/ignis-server/server/settings.js @@ -19,6 +19,9 @@ const DEFAULTS = { const KEYS = Object.keys(DEFAULTS); +// Hard ceiling for request bodies. +const MAX_BODY_BACKSTOP = 500 * 1024 * 1024; + function parseList(raw) { return raw .split(",") @@ -88,4 +91,4 @@ function update(partial) { return getAll(); } -module.exports = { DEFAULTS, KEYS, getAll, get, update }; +module.exports = { DEFAULTS, KEYS, MAX_BODY_BACKSTOP, getAll, get, update };