Fix log token leakage, orphaned certs, hash routing, icon-reset label, docs/backup theme contrast, and version drift
- Redact sensitive query-string params (token/secret/password/etc.) before storing access logs - Clean up orphaned certificate files when a site/proxy/redirect is deleted, renamed, or has domains removed - Add hashchange listener so back/forward navigation and deep links correctly route the app - Fix activity log showing "undefined" instead of a username when resetting/updating a user's icon - Fix illegible dark-on-dark text in the Docs page hero banner and the Backup & Restore form fields in light theme - Give raw validation errors on proxy/site/redirect config routes the same "Gateway configuration rejected:" wording as other config errors - Serve index.html with the real package.json version baked into the cache-busting query string instead of a stale hardcoded one - Bump version to 0.11.31
This commit is contained in:
@@ -442,6 +442,15 @@ $("#password-form").addEventListener("submit", async event => {
|
||||
} catch (error) { $("#password-error").textContent = error.message; }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
window.addEventListener("hashchange", () => {
|
||||
if (!state.user) return; // Not logged in yet; boot() handles initial routing.
|
||||
const requestedHash = location.hash.slice(1);
|
||||
state.adminTab = requestedHash.startsWith("administration/") ? requestedHash.split("/")[1] || "users" : "users";
|
||||
if (requestedHash.startsWith("administration/")) history.replaceState(null, "", `${location.pathname}${location.search}#administration`);
|
||||
state.view = location.hash.slice(1) || "overview";
|
||||
render();
|
||||
loadFeatureView().catch(error => toast(error.message));
|
||||
});
|
||||
boot().catch(error => toast(error.message));
|
||||
|
||||
function syncUpstreamTlsControls(form) {
|
||||
|
||||
@@ -113,6 +113,7 @@ dialog{max-height:calc(100vh - 28px);overflow:auto}
|
||||
#backup-settings-form select{appearance:none!important}
|
||||
.settings-form .form-section{grid-column:1/-1;padding:4px 0 20px;border-bottom:1px solid var(--line)}.settings-form .form-section+.form-section{padding-top:20px}.settings-form .form-section:last-of-type{border-bottom:0}.settings-form .form-section .eyebrow{margin-bottom:12px}.settings-form .form-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0 18px}.settings-form .form-grid .check-control{align-self:end}.settings-form .form-section-wide .check-control{max-width:420px}.backup-settings .dialog-actions{grid-column:1/-1}.backup-settings code{color:var(--green);font-family:ui-monospace,monospace;font-size:.78rem}
|
||||
#backup-settings-form select,#backup-settings-form input[type="number"],#backup-settings-form input[type="password"]{display:block;width:100%;height:44px;min-height:44px;margin-top:7px;padding:0 12px;box-sizing:border-box;border:1px solid var(--line);border-radius:9px;background:#0b1525;color:var(--text);line-height:42px}#backup-settings-form select{appearance:auto}#backup-settings-form .check-control{display:flex;align-items:center;height:44px;min-height:44px;margin-top:16px;padding:0 12px}#backup-settings-form .check-control input{width:17px;height:17px;margin:0;line-height:normal}#backup-settings-form .dialog-actions{margin-top:18px;padding-top:16px}
|
||||
:root[data-theme="light"] #backup-settings-form select,:root[data-theme="light"] #backup-settings-form input[type="number"],:root[data-theme="light"] #backup-settings-form input[type="password"]{background:#fff}
|
||||
.backup-password-field .field-label{display:flex;align-items:center;justify-content:space-between;gap:10px}.backup-password-field .optional{float:none}.backup-password-field small{display:block;max-width:38rem;line-height:1.45}
|
||||
.event-filters{display:flex;gap:10px;margin:4px 0 15px}.event-filters label{min-width:180px;margin:0}.event-filters select{width:100%;height:44px;min-height:44px}.event-list{max-height:360px;overflow:auto}.event-row{display:flex;align-items:flex-start;gap:12px;padding:12px 3px;border-top:1px solid var(--line)}.event-row:first-child{border-top:0}.event-row>span:last-child{display:grid;gap:3px}.event-row small{text-transform:capitalize;color:var(--muted);font-size:.7rem}.activity-mark.warn{background:rgba(255,191,105,.12);color:var(--warning)}@media(max-width:600px){.event-filters{flex-direction:column}.event-filters label{min-width:0}}
|
||||
.dashboard-panel .panel-heading .text-button{white-space:nowrap;font-size:.75rem}
|
||||
@@ -129,7 +130,7 @@ dialog{max-height:calc(100vh - 28px);overflow:auto}
|
||||
#access-assignment-summary>strong{margin-top:20px;padding-top:16px;border-top:1px solid var(--line)}.access-create-guidance{margin-top:18px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--panel2)}.access-create-guidance strong{display:block}.access-create-guidance p{margin:6px 0 0;color:var(--muted);line-height:1.45}
|
||||
|
||||
.danger-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.danger-actions .button{margin:0}
|
||||
.docs-intro{margin-bottom:22px;padding:24px;border:1px solid var(--line);border-radius:16px;background:linear-gradient(145deg,rgba(20,33,54,.95),rgba(13,23,39,.95))}.docs-intro h2{margin:0 0 10px}.docs-intro p:last-child{margin:0;color:var(--muted);max-width:850px;line-height:1.6}
|
||||
.docs-intro{--text:#f4f7fb;--muted:#aebbd0;margin-bottom:22px;padding:24px;border:1px solid var(--line);border-radius:16px;background:linear-gradient(145deg,rgba(20,33,54,.95),rgba(13,23,39,.95));color:var(--text)}.docs-intro h2{margin:0 0 10px}.docs-intro p:last-child{margin:0;color:var(--muted);max-width:850px;line-height:1.6}
|
||||
.docs-layout{display:grid;grid-template-columns:210px minmax(0,1fr);gap:22px;align-items:start}.docs-nav{position:sticky;top:18px;height:calc(100vh - 170px);overflow:auto;display:grid;align-content:start;gap:6px;padding:14px;border:1px solid var(--line);border-radius:14px;background:rgba(16,26,43,.9)}.docs-nav .eyebrow{margin:4px 8px 8px}.docs-nav button{padding:9px 10px;border:0;border-radius:8px;background:transparent;color:var(--muted);text-align:left;cursor:pointer;font:inherit}.docs-nav button:hover{background:rgba(98,230,167,.1);color:var(--text)}.docs #docs-content{display:grid;gap:16px}.docs #docs-content article{scroll-margin-top:18px;padding:24px;border:1px solid var(--line);border-radius:16px;background:rgba(16,26,43,.76);line-height:1.6}.docs #docs-content article h2{margin:0 0 12px}.docs #docs-content article h3{margin:20px 0 5px;color:var(--green);font-size:.86rem}.docs #docs-content article p{color:var(--muted)}.docs .doc-search{display:block;margin-top:18px}.docs .doc-search input{margin-top:8px}@media(max-width:800px){.docs-layout{grid-template-columns:1fr}.docs-nav{position:static;display:flex;flex-wrap:wrap}.docs-nav .eyebrow{width:100%}}
|
||||
.docs-intro{padding:32px 34px}.docs-search-panel{max-width:880px;margin-top:24px;padding:18px 20px;border:1px solid var(--line);border-radius:14px;background:rgba(7,15,28,.42)}.docs-search-panel .doc-search{margin:0}.docs-search-panel .doc-search span{display:block;margin-bottom:8px;font-weight:750;color:var(--text)}.docs-search-panel small{display:block;margin-top:8px;color:var(--muted)}.docs #docs-content article ul{margin:10px 0 0;padding-left:22px;color:var(--muted)}.docs #docs-content article li{margin:8px 0}
|
||||
.docs-intro{width:100%;box-sizing:border-box;text-align:center}.docs-intro>p:not(.eyebrow){margin-left:auto;margin-right:auto}.docs-search-panel{max-width:none;text-align:left}.docs-search-panel .doc-search input{width:100%;box-sizing:border-box}
|
||||
@@ -169,6 +170,7 @@ dialog{max-height:calc(100vh - 28px);overflow:auto}
|
||||
.backup-settings .encryption-toggle{height:auto!important;min-height:0!important;margin-top:16px!important;padding:0!important;background:transparent!important;border:0!important;display:block!important}
|
||||
.backup-settings .encryption-toggle .field-label{display:flex;align-items:center;height:20px;margin:0 0 7px;color:#c7d0de;font-size:.82rem;font-weight:650}
|
||||
.backup-settings .encryption-toggle-box{display:flex;align-items:center;gap:10px;height:44px;min-height:44px;padding:0 12px;border:1px solid var(--line);border-radius:9px;background:#0b1525;box-sizing:border-box}
|
||||
:root[data-theme="light"] .backup-settings .encryption-toggle-box{background:#fff}
|
||||
.backup-settings .encryption-toggle-box input{width:17px!important;height:17px!important;margin:0!important;flex:0 0 auto}
|
||||
.backup-settings .encryption-toggle-box>span{color:var(--muted);font-size:.72rem;line-height:1.35}
|
||||
/* Authoritative backup-form field layout: labels sit above equal-height controls. */
|
||||
|
||||
+66
-8
@@ -469,6 +469,24 @@ async function certificateInventory() {
|
||||
return { checkedAt: new Date().toISOString(), thresholds: settings.certificateHealth, latestError, summary: { total: certificates.length, healthy: certificates.filter(item => item.status === "healthy").length, within30Days: certificates.filter(item => item.daysRemaining != null && item.daysRemaining <= 30 && item.daysRemaining > 0).length, within7Days: certificates.filter(item => item.daysRemaining != null && item.daysRemaining <= 7 && item.daysRemaining > 0).length, warning: certificates.filter(item => item.status === "warning").length, critical: certificates.filter(item => item.status === "critical").length, expired: certificates.filter(item => item.status === "expired").length, pending: certificates.filter(item => item.status === "pending").length, mismatch: certificates.filter(item => item.status === "mismatch").length }, certificates };
|
||||
}
|
||||
|
||||
async function pruneOrphanedCertificates(candidateDomains) {
|
||||
const domains = [...new Set((candidateDomains || []).filter(Boolean).map(domain => String(domain).toLowerCase()))];
|
||||
if (!domains.length) return;
|
||||
const stillInUse = new Set([...sites, ...proxies, ...redirects].filter(item => item.enabled).flatMap(item => normalizeDomains(item.domain, item.domains)).map(domain => domain.toLowerCase()));
|
||||
const orphaned = domains.filter(domain => !stillInUse.has(domain));
|
||||
if (!orphaned.length) return;
|
||||
const files = await walkFiles(certificateDir).catch(() => []);
|
||||
const removed = new Set();
|
||||
for (const file of files) {
|
||||
const directory = path.dirname(file);
|
||||
if (orphaned.includes(path.basename(directory).toLowerCase()) && !removed.has(directory)) {
|
||||
await fsp.rm(directory, { recursive: true, force: true }).catch(() => {});
|
||||
removed.add(directory);
|
||||
}
|
||||
}
|
||||
if (removed.size) recordActivity(`Removed stored certificate data for ${orphaned.join(", ")} (no longer in use).`);
|
||||
}
|
||||
|
||||
async function domainReadiness() {
|
||||
const routes = [...sites.map(item => ({ ...item, kind: "Hosted site" })), ...proxies.map(item => ({ ...item, kind: "Proxy host" })), ...redirects.map(item => ({ ...item, kind: "Redirect host" }))].filter(item => item.enabled && item.domain).flatMap(item => normalizeDomains(item.domain, item.domains).map(domain => ({ ...item, domain })));
|
||||
const certs = await certificateInventory();
|
||||
@@ -510,6 +528,22 @@ async function checkAllProxies() {
|
||||
return proxies.map(publicProxy);
|
||||
}
|
||||
|
||||
const SENSITIVE_QUERY_PARAM_PATTERNS = [/token/i, /secret/i, /password/i, /passwd/i, /auth/i, /session/i, /api[-_]?key/i, /credential/i];
|
||||
|
||||
function redactUri(uri) {
|
||||
const str = String(uri || "");
|
||||
const queryIndex = str.indexOf("?");
|
||||
if (queryIndex === -1) return str;
|
||||
const pathPart = str.slice(0, queryIndex);
|
||||
let params;
|
||||
try { params = new URLSearchParams(str.slice(queryIndex + 1)); } catch { return `${pathPart}?REDACTED`; }
|
||||
let redactedAny = false;
|
||||
for (const name of [...params.keys()]) {
|
||||
if (SENSITIVE_QUERY_PARAM_PATTERNS.some(pattern => pattern.test(name))) { params.set(name, "REDACTED"); redactedAny = true; }
|
||||
}
|
||||
return redactedAny ? `${pathPart}?${params.toString()}` : str;
|
||||
}
|
||||
|
||||
async function readAccessLogs(limit = 100, host = "") {
|
||||
const files = (await fsp.readdir(logsDir).catch(() => [])).filter(name => name === "access.json" || name.startsWith("access.json.")).sort().reverse();
|
||||
const entries = [];
|
||||
@@ -519,7 +553,7 @@ async function readAccessLogs(limit = 100, host = "") {
|
||||
try {
|
||||
const raw = JSON.parse(line); const request = raw.request || {}; const requestHost = String(request.host || "").split(":")[0];
|
||||
if (host && requestHost !== host) continue;
|
||||
entries.push({ at: raw.ts ? new Date(raw.ts * 1000).toISOString() : null, host: requestHost, method: request.method, uri: request.uri, status: raw.status, size: raw.size, durationMs: Number.isFinite(raw.duration) ? Math.round(raw.duration * 1000) : null, remoteIp: request.remote_ip || null });
|
||||
entries.push({ at: raw.ts ? new Date(raw.ts * 1000).toISOString() : null, host: requestHost, method: request.method, uri: redactUri(request.uri), status: raw.status, size: raw.size, durationMs: Number.isFinite(raw.duration) ? Math.round(raw.duration * 1000) : null, remoteIp: request.remote_ip || null });
|
||||
if (entries.length >= limit) return entries;
|
||||
} catch { /* Skip incomplete lines while Caddy writes. */ }
|
||||
}
|
||||
@@ -848,6 +882,11 @@ const iconUpload = multer({ dest: uploadDir, limits: { fileSize: 2 * 1024 * 1024
|
||||
app.disable("x-powered-by");
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.get(["/", "/index.html"], (req, res) => {
|
||||
const html = fs.readFileSync(path.join(publicDir, "index.html"), "utf8")
|
||||
.replace(/\/(app|features)\.js\?v=[^"']+/g, `/$1.js?v=${appVersion}`);
|
||||
res.type("html").send(html);
|
||||
});
|
||||
app.use(express.static(publicDir));
|
||||
app.use("/site-icons", express.static(iconsDir, { immutable: true, maxAge: "30d", setHeaders: res => res.setHeader("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") }));
|
||||
|
||||
@@ -1055,6 +1094,9 @@ app.get("/api/icons/search", async (req, res, next) => {
|
||||
res.json(results);
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
function entryLabel(item) {
|
||||
return item?.name || item?.displayName || item?.username || "item";
|
||||
}
|
||||
app.put("/api/:kind/:id/icon", async (req, res, next) => {
|
||||
try {
|
||||
const collection = req.params.kind === "sites" ? sites : req.params.kind === "proxies" ? proxies : req.params.kind === "redirects" ? redirects : req.params.kind === "access-lists" ? accessLists : req.params.kind === "groups" ? groups : req.params.kind === "users" ? users : null;
|
||||
@@ -1066,7 +1108,7 @@ app.put("/api/:kind/:id/icon", async (req, res, next) => {
|
||||
if (!/^https:\/\//i.test(url) || url.length > 2048) return res.status(400).json({ error: "Icon URL must be a valid HTTPS URL under 2048 characters." });
|
||||
item.iconSlug = null; item.icon = url;
|
||||
if (collection === sites) await saveSites(); else if (collection === proxies) await saveProxies(); else if (collection === redirects) await saveRedirects(); else if (collection === groups) await saveGroups(); else if (collection === users) await saveUsers(); else await saveAccessLists();
|
||||
recordActivity(`Icon URL updated for “${item.name}”.`);
|
||||
recordActivity(`Icon URL updated for “${entryLabel(item)}”.`);
|
||||
return res.json(item);
|
||||
}
|
||||
const slug = String(req.body.slug || "").trim();
|
||||
@@ -1074,7 +1116,7 @@ app.put("/api/:kind/:id/icon", async (req, res, next) => {
|
||||
item.iconSlug = slug || null;
|
||||
item.icon = icon;
|
||||
if (collection === sites) await saveSites(); else if (collection === proxies) await saveProxies(); else if (collection === redirects) await saveRedirects(); else if (collection === groups) await saveGroups(); else await saveAccessLists();
|
||||
recordActivity(`${slug ? "Icon updated" : "Icon reset"} for “${item.name}”.`);
|
||||
recordActivity(`${slug ? "Icon updated" : "Icon reset"} for “${entryLabel(item)}”.`);
|
||||
res.json(item);
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
@@ -1091,7 +1133,7 @@ app.post("/api/:kind/:id/icon", iconUpload.single("icon"), async (req, res, next
|
||||
await fsp.rename(req.file.path, path.join(iconsDir, filename));
|
||||
item.iconSlug = null; item.icon = `/site-icons/${filename}`;
|
||||
if (collection === sites) await saveSites(); else if (collection === proxies) await saveProxies(); else if (collection === redirects) await saveRedirects(); else if (collection === groups) await saveGroups(); else if (collection === users) await saveUsers(); else await saveAccessLists();
|
||||
recordActivity(`Custom icon uploaded for “${item.name}”.`);
|
||||
recordActivity(`Custom icon uploaded for “${entryLabel(item)}”.`);
|
||||
res.json(item);
|
||||
} catch (error) { next(error); }
|
||||
finally { if (req.file?.path) await fsp.rm(req.file.path, { force: true }).catch(() => {}); }
|
||||
@@ -1157,6 +1199,7 @@ app.delete("/api/sites/:id", async (req, res, next) => {
|
||||
await stopSite(site.id);
|
||||
await syncCaddy();
|
||||
await fsp.rm(path.join(sitesDir, site.id), { recursive: true, force: true });
|
||||
await pruneOrphanedCertificates(normalizeDomains(site.domain, site.domains));
|
||||
await saveSites();
|
||||
recordActivity(`Hosted site “${site.name}” deleted.`);
|
||||
res.status(204).end();
|
||||
@@ -1166,6 +1209,7 @@ app.patch("/api/sites/:id", async (req, res, next) => {
|
||||
try {
|
||||
const site = sites.find(item => item.id === req.params.id);
|
||||
if (!site) return res.status(404).json({ error: "Site not found." });
|
||||
const previousDomains = normalizeDomains(site.domain, site.domains);
|
||||
const domain = normalizeDomain(req.body.domain);
|
||||
const domains = normalizeDomains(domain, req.body.domains !== undefined ? req.body.domains : site.domains);
|
||||
const domainError = validateDomains(domains, site.id);
|
||||
@@ -1177,6 +1221,7 @@ app.patch("/api/sites/:id", async (req, res, next) => {
|
||||
if (site.healthEnabled === false) upstreamHealth.set(site.id, { status: "unmonitored", checkedAt: null, history: [] });
|
||||
else { upstreamHealth.set(site.id, { status: "pending", checkedAt: null, history: [] }); checkProxy({ ...site, target: `http://127.0.0.1:${site.port}` }).catch(error => console.warn("Hosted site health check failed:", error.message)); }
|
||||
await syncCaddy();
|
||||
await pruneOrphanedCertificates(previousDomains);
|
||||
await saveSites();
|
||||
recordActivity(`Gateway settings updated for “${site.name}”.`);
|
||||
res.json(publicSite(site));
|
||||
@@ -1214,6 +1259,7 @@ app.patch("/api/proxies/:id", async (req, res, next) => {
|
||||
try {
|
||||
const proxy = proxies.find(item => item.id === req.params.id);
|
||||
if (!proxy) return res.status(404).json({ error: "Proxy host not found." });
|
||||
const previousDomains = normalizeDomains(proxy.domain, proxy.domains);
|
||||
if (req.body.domain !== undefined) {
|
||||
const domain = normalizeDomain(req.body.domain);
|
||||
const domains = normalizeDomains(domain, req.body.domains !== undefined ? req.body.domains : proxy.domains);
|
||||
@@ -1232,6 +1278,7 @@ app.patch("/api/proxies/:id", async (req, res, next) => {
|
||||
if (req.body.hsts !== undefined) proxy.hsts = req.body.hsts === true;
|
||||
applyAdvancedSettings(proxy, req.body);
|
||||
await syncCaddy();
|
||||
await pruneOrphanedCertificates(previousDomains);
|
||||
await saveProxies();
|
||||
recordActivity(`Proxy host “${proxy.name}” updated.`);
|
||||
res.json(publicProxy(proxy));
|
||||
@@ -1272,6 +1319,8 @@ app.delete("/api/proxies/:id", async (req, res, next) => {
|
||||
if (index < 0) return res.status(404).json({ error: "Proxy host not found." });
|
||||
const [proxy] = proxies.splice(index, 1);
|
||||
await syncCaddy();
|
||||
await fsp.rm(path.join(customCertificatesDir, proxy.id), { recursive: true, force: true }).catch(() => {});
|
||||
await pruneOrphanedCertificates(normalizeDomains(proxy.domain, proxy.domains));
|
||||
await saveProxies();
|
||||
recordActivity(`Proxy host “${proxy.name}” deleted.`);
|
||||
res.status(204).end();
|
||||
@@ -1355,6 +1404,7 @@ app.post("/api/redirects", async (req, res, next) => {
|
||||
app.patch("/api/redirects/:id", async (req, res, next) => {
|
||||
try {
|
||||
const item = redirects.find(value => value.id === req.params.id); if (!item) return res.status(404).json({ error: "Redirect Host not found." });
|
||||
const previousDomains = normalizeDomains(item.domain, item.domains);
|
||||
if (req.body.domain !== undefined || req.body.domains !== undefined) { const domain = normalizeDomain(req.body.domain ?? item.domain); const domains = normalizeDomains(domain, req.body.domains !== undefined ? req.body.domains : item.domains); const error = validateDomains(domains, item.id); if (error || !domain) return res.status(400).json({ error: error || "Primary source domain is required." }); item.domain = domain; item.domains = domains; }
|
||||
if (req.body.enabled !== undefined) item.enabled = Boolean(req.body.enabled);
|
||||
for (const key of ["name","target","accessListId"]) if (req.body[key] !== undefined) item[key] = String(req.body[key]).trim();
|
||||
@@ -1363,11 +1413,11 @@ app.patch("/api/redirects/:id", async (req, res, next) => {
|
||||
if (req.body.preservePath !== undefined) item.preservePath = Boolean(req.body.preservePath);
|
||||
if (req.body.tls !== undefined) item.tls = ["http","automatic","internal"].includes(req.body.tls) ? req.body.tls : item.tls;
|
||||
if (req.body.hsts !== undefined) item.hsts = Boolean(req.body.hsts);
|
||||
await syncCaddy(); await saveRedirects(); recordActivity(`Redirect Host “${item.name}” updated.`); res.json(item);
|
||||
await syncCaddy(); await pruneOrphanedCertificates(previousDomains); await saveRedirects(); recordActivity(`Redirect Host “${item.name}” updated.`); res.json(item);
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
app.delete("/api/redirects/:id", async (req, res, next) => {
|
||||
try { const index = redirects.findIndex(item => item.id === req.params.id); if (index < 0) return res.status(404).json({ error: "Redirect Host not found." }); const [item] = redirects.splice(index, 1); await syncCaddy(); await saveRedirects(); recordActivity(`Redirect Host “${item.name}” deleted.`); res.status(204).end(); } catch (error) { next(error); }
|
||||
try { const index = redirects.findIndex(item => item.id === req.params.id); if (index < 0) return res.status(404).json({ error: "Redirect Host not found." }); const [item] = redirects.splice(index, 1); await syncCaddy(); await pruneOrphanedCertificates(normalizeDomains(item.domain, item.domains)); await saveRedirects(); recordActivity(`Redirect Host “${item.name}” deleted.`); res.status(204).end(); } catch (error) { next(error); }
|
||||
});
|
||||
|
||||
app.patch("/api/settings", async (req, res, next) => {
|
||||
@@ -1419,10 +1469,18 @@ app.delete("/api/backups/:filename", async (req, res, next) => {
|
||||
try { const filename = path.basename(req.params.filename); if (!filename.endsWith(".sgbackup")) return res.status(400).json({ error: "Invalid backup." }); await fsp.rm(path.join(backupsDir, filename)); recordActivity(`Backup ${filename} deleted.`); res.status(204).end(); } catch (error) { next(error); }
|
||||
});
|
||||
function humanizeGatewayActivityError(message) { const text = String(message || "Unexpected gateway error"); if (/upstream address scheme is HTTP but transport is configured for HTTP\+TLS/i.test(text)) return "Gateway configuration rejected: HTTP upstream cannot use HTTPS transport. Disable upstream TLS verification or change the upstream URL to HTTPS."; if (/upstream address scheme is HTTPS but transport is configured for plain HTTP/i.test(text)) return "Gateway configuration rejected: HTTPS upstream requires HTTPS transport settings. Change the upstream URL or transport setting."; if (/duplicate.*address|already.*site address/i.test(text)) return "Gateway configuration rejected: This hostname or address is already used by another host. Choose a unique hostname and port."; if (/dial tcp|no such host|lookup .* no such host|upstream.*(invalid|malformed)/i.test(text)) return "Gateway configuration rejected: The upstream address could not be reached or is invalid. Check the hostname, IP address, and port."; if (/invalid hostname|host name.*invalid|malformed.*host/i.test(text)) return "Gateway configuration rejected: The hostname is not valid. Use a valid domain name without a protocol or path."; if (/unrecognized directive|unknown directive|parsing caddyfile tokens/i.test(text)) return "Gateway configuration rejected: The gateway configuration contains an unsupported or malformed directive. Check the selected host settings."; if (/certificate|tls.*(config|handshake)|no certificate/i.test(text)) return "Gateway configuration rejected: The TLS certificate configuration is invalid or unavailable. Check the certificate, key, and HTTPS settings."; return text.replace(/^Gateway configuration was rejected:\s*/i, "Gateway configuration rejected: ").replace(/\s+Details:\s+[\s\S]*$/i, ""); }
|
||||
const GATEWAY_CONFIG_ROUTE = /^\/api\/(sites|proxies|redirects|access-lists)(\/|$)/i;
|
||||
app.use((error, req, res, next) => {
|
||||
console.error(error);
|
||||
recordActivity(`${req.method} ${req.path}: ${humanizeGatewayActivityError(error.message)}`, "error");
|
||||
res.status(error.status || 500).json({ error: error.message || "Something went wrong." });
|
||||
const rawMessage = error.message || "Something went wrong.";
|
||||
const isConfigRoute = GATEWAY_CONFIG_ROUTE.test(req.path) && ["PATCH", "POST", "DELETE", "PUT"].includes(req.method);
|
||||
let logMessage = rawMessage;
|
||||
if (isConfigRoute) {
|
||||
const humanized = humanizeGatewayActivityError(rawMessage);
|
||||
logMessage = /^Gateway configuration rejected:/i.test(humanized) ? humanized : `Gateway configuration rejected: ${humanized}`;
|
||||
}
|
||||
recordActivity(`${req.method} ${req.path}: ${logMessage}`, "error");
|
||||
res.status(error.status || 500).json({ error: rawMessage });
|
||||
});
|
||||
|
||||
app.listen(adminPort, "0.0.0.0", () => {
|
||||
|
||||
Reference in New Issue
Block a user