add server settings api
This commit is contained in:
parent
938a698795
commit
b43d12f702
6 changed files with 141 additions and 2 deletions
|
|
@ -70,6 +70,11 @@ function setupDemo(app) {
|
||||||
// Hide server-side plugins (headless-sync) from the demo UI
|
// Hide server-side plugins (headless-sync) from the demo UI
|
||||||
app.use("/api/plugins", pluginsBlocker);
|
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
|
// Cleanup timer
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
cleanupExpired().catch((e) =>
|
cleanupExpired().catch((e) =>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,18 @@ const ANSI_RESET = "\x1b[0m";
|
||||||
|
|
||||||
const app = express();
|
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());
|
app.use(compression());
|
||||||
|
|
||||||
// logger middleware
|
// logger middleware
|
||||||
|
|
@ -67,6 +78,7 @@ const fsRoutes = require("./routes/fs");
|
||||||
const vaultRoutes = require("./routes/vault");
|
const vaultRoutes = require("./routes/vault");
|
||||||
const proxyRoutes = require("./routes/proxy");
|
const proxyRoutes = require("./routes/proxy");
|
||||||
const versionRoutes = require("./routes/version");
|
const versionRoutes = require("./routes/version");
|
||||||
|
const settingsRoutes = require("./routes/settings");
|
||||||
const bootstrapRoutes = require("./routes/bootstrap");
|
const bootstrapRoutes = require("./routes/bootstrap");
|
||||||
|
|
||||||
app.use("/assets", express.static(path.join(__dirname, "assets")));
|
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/vault", vaultRoutes);
|
||||||
app.use("/api/proxy", proxyRoutes);
|
app.use("/api/proxy", proxyRoutes);
|
||||||
app.use("/api/version", versionRoutes);
|
app.use("/api/version", versionRoutes);
|
||||||
|
app.use("/api/settings", settingsRoutes);
|
||||||
app.use("/api/plugins", pluginRoutes);
|
app.use("/api/plugins", pluginRoutes);
|
||||||
app.use("/api/bootstrap", bootstrapRoutes);
|
app.use("/api/bootstrap", bootstrapRoutes);
|
||||||
|
|
||||||
|
|
@ -200,6 +213,7 @@ const wss = setupWebSocket(server, {
|
||||||
getVaultPath: config.getVaultPath,
|
getVaultPath: config.getVaultPath,
|
||||||
originAllowlist: settings.get("wsOrigins"),
|
originAllowlist: settings.get("wsOrigins"),
|
||||||
});
|
});
|
||||||
|
app.set("wss", wss);
|
||||||
wireDemoWebSocket(server);
|
wireDemoWebSocket(server);
|
||||||
|
|
||||||
async function gracefulShutdown(signal) {
|
async function gracefulShutdown(signal) {
|
||||||
|
|
|
||||||
11
apps/ignis-server/server/routes/bootstrap.js
vendored
11
apps/ignis-server/server/routes/bootstrap.js
vendored
|
|
@ -14,6 +14,7 @@ const {
|
||||||
getVirtualPluginsForVault,
|
getVirtualPluginsForVault,
|
||||||
} = require("../plugin-system/manager");
|
} = require("../plugin-system/manager");
|
||||||
const { getVersion } = require("../version");
|
const { getVersion } = require("../version");
|
||||||
|
const settings = require("../settings");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -140,6 +141,11 @@ async function buildEntry(vaultId) {
|
||||||
// In demo mode, hide server-side plugins from the client.
|
// In demo mode, hide server-side plugins from the client.
|
||||||
plugins: config.demoMode ? [] : getDiscoveredPlugins(),
|
plugins: config.demoMode ? [] : getDiscoveredPlugins(),
|
||||||
virtualPlugins: getVirtualPluginsForVault(vaultId, getVersion()),
|
virtualPlugins: getVirtualPluginsForVault(vaultId, getVersion()),
|
||||||
|
settings: {
|
||||||
|
contentCacheBytes: settings.get("contentCacheBytes"),
|
||||||
|
inputCacheBytes: settings.get("inputCacheBytes"),
|
||||||
|
inputCacheTtlMs: settings.get("inputCacheTtlMs"),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const jsonBuf = Buffer.from(JSON.stringify(response));
|
const jsonBuf = Buffer.from(JSON.stringify(response));
|
||||||
|
|
@ -185,6 +191,10 @@ function invalidateVault(vaultId) {
|
||||||
cache.delete(vaultId);
|
cache.delete(vaultId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function invalidateAll() {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
async function warmUp() {
|
async function warmUp() {
|
||||||
const ids = Object.keys(config.vaults);
|
const ids = Object.keys(config.vaults);
|
||||||
|
|
||||||
|
|
@ -251,4 +261,5 @@ router.get("/", async (req, res) => {
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
module.exports.invalidateVault = invalidateVault;
|
module.exports.invalidateVault = invalidateVault;
|
||||||
|
module.exports.invalidateAll = invalidateAll;
|
||||||
module.exports.warmUp = warmUp;
|
module.exports.warmUp = warmUp;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const dns = require("dns").promises;
|
const dns = require("dns").promises;
|
||||||
const net = require("net");
|
const net = require("net");
|
||||||
|
const settings = require("../settings");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
@ -105,6 +106,19 @@ router.post("/", async (req, res) => {
|
||||||
return res.status(e.statusCode || 400).json({ error: e.message });
|
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 {
|
try {
|
||||||
// Forward the caller's headers as-is.
|
// Forward the caller's headers as-is.
|
||||||
const fetchOpts = {
|
const fetchOpts = {
|
||||||
|
|
|
||||||
92
apps/ignis-server/server/routes/settings.js
Normal file
92
apps/ignis-server/server/routes/settings.js
Normal file
|
|
@ -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;
|
||||||
|
|
@ -19,6 +19,9 @@ const DEFAULTS = {
|
||||||
|
|
||||||
const KEYS = Object.keys(DEFAULTS);
|
const KEYS = Object.keys(DEFAULTS);
|
||||||
|
|
||||||
|
// Hard ceiling for request bodies.
|
||||||
|
const MAX_BODY_BACKSTOP = 500 * 1024 * 1024;
|
||||||
|
|
||||||
function parseList(raw) {
|
function parseList(raw) {
|
||||||
return raw
|
return raw
|
||||||
.split(",")
|
.split(",")
|
||||||
|
|
@ -88,4 +91,4 @@ function update(partial) {
|
||||||
return getAll();
|
return getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { DEFAULTS, KEYS, getAll, get, update };
|
module.exports = { DEFAULTS, KEYS, MAX_BODY_BACKSTOP, getAll, get, update };
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue