v0.24.13
This commit is contained in:
1 parent
bd077754a9
commit
ed7e3377c1
40 files changed
+1764
-365
No files matched your search
@@ -112,7 +112,7 @@ function appConfirm(message, options = {}) {
|
||||
}
|
||||
function apiArea(path) {
|
||||
const section = path.match(/^\/api\/config\/([^/?]+)/)?.[1];
|
||||
if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "geoip": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not_found_hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security", "user-agent-blocking": "security" })[section] || "configuration";
|
||||
if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "geoip": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not-found-hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security", "global-security": "settings" })[section] || "configuration";
|
||||
if (path === "/api/config") return "configuration";
|
||||
if (path.startsWith("/api/traffic")) return "traffic";
|
||||
if (path.startsWith("/api/cache")) return "cache";
|
||||
@@ -386,9 +386,6 @@ function renderConfig() {
|
||||
$("host-policy-list").replaceChildren(...(config.host_policies || []).map((policy, index) => { const box = el("div", undefined, "entity"); box.append(el("span", policy.hosts.join(", "), "entity-title"), el("p", `${t("aclDefault")}: ${t(policy.acl.default || "deny")}\n${rulesText(policy.acl)}`, "entity-details")); const buttons = el("div", undefined, "entity-actions"); buttons.append(action(t("edit"), () => { $("policy-hosts").value = policy.hosts.join(", "); $("policy-default").value = policy.acl.default || "deny"; $("policy-rules").value = rulesText(policy.acl); const editor = $("security-policy-editor"); if (editor) { editor.open = true; editor.scrollIntoView({ block: "nearest" }); } }), action(t("remove"), async () => { if (await appConfirm(t("confirmDelete"), { danger: true })) { const next = structuredClone(config); next.host_policies.splice(index, 1); await saveSection("host-policies", next.host_policies); } })); box.append(buttons); return box; }));
|
||||
if (!config.host_policies?.length) $("host-policy-list").append(el("p", t("noPolicies"), "empty"));
|
||||
if ($("security-policy-count")) $("security-policy-count").textContent = String((config.host_policies || []).length);
|
||||
const uaPolicy = config.user_agent_blocking || {};
|
||||
if ($("global-ua-block-enabled")) $("global-ua-block-enabled").checked = uaPolicy.enabled === true;
|
||||
if ($("global-ua-block-patterns")) $("global-ua-block-patterns").value = (uaPolicy.patterns || []).join("\n");
|
||||
}
|
||||
async function saveRequest(path, payload) { if (busy) { toast(t("busy"), true); return false; } if (!authenticated || !envelope) return false; busy = true; const epoch = authEpoch; try { const value = await api(path, { method: "PUT", body: JSON.stringify(payload) }); if (!authenticated || epoch !== authEpoch) return false; envelope = value; if (path === "/api/config" || path === "/api/config/tls") await refreshCertificateInventory(); if (!authenticated || epoch !== authEpoch || !envelope) return false; renderConfig(); toast(t("saved")); await refresh(true); return true; } catch (e) { toast(e.message, true); if (e.status === 401) lock(); return false; } finally { busy = false; } }
|
||||
async function save(config) { return saveRequest("/api/config", { expected_revision: envelope?.revision, config }); }
|
||||
@@ -514,7 +511,69 @@ document.addEventListener("click", event => { if (dashboardSidebar?.classList.co
|
||||
document.addEventListener("keydown", event => { if (event.key === "Escape" && dashboardSidebar?.classList.contains("mobile-nav-open")) { setMobileMenu(false); mobileMenuToggle?.focus(); } });
|
||||
window.addEventListener("popstate", () => { setPanel(panelFromLocation(), false); setMobileMenu(false); });
|
||||
$("host-policy-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const hosts = [...new Set($("policy-hosts").value.split(",").map(h => h.trim().toLowerCase()).filter(Boolean))]; const acl = { default: $("policy-default").value, rules: parseRules($("policy-rules").value, $("policy-rules")) }; const next = structuredClone(envelope.config); next.host_policies = (next.host_policies || []).map(p => ({ ...p, hosts: p.hosts.filter(h => !hosts.includes(h)) })).filter(p => p.hosts.length); next.host_policies.push({ hosts, acl }); if (await saveSection("host-policies", next.host_policies)) { form.reset(); if ($("security-policy-editor")) $("security-policy-editor").open = false; } }); } catch (e) { toast(e.message, true); } });
|
||||
$("global-ua-block-form")?.addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const patterns = [...new Set($("global-ua-block-patterns").value.split(/\r?\n/).map(value => value.trim()).filter(Boolean))]; await saveSection("user-agent-blocking", { enabled: $("global-ua-block-enabled").checked, patterns }); }); } catch (e) { toast(e.message, true); } });
|
||||
// Global HTTP controls are a single atomic API section. A header-only save preserves
|
||||
// the current block rules; a block-only save preserves the current security headers.
|
||||
const globalLines = id => [...new Set($(id).value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean))];
|
||||
const globalPatternLines = id => [...new Set($(id).value.split(/\r?\n/).map(v => v.trim()).filter(Boolean))];
|
||||
function globalSecurityPayload(security, ua) {
|
||||
return { ...security, user_agent_blocking: ua };
|
||||
}
|
||||
$("global-security-form")?.addEventListener("submit", async event => {
|
||||
event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return;
|
||||
try {
|
||||
await withFormSubmitState(form, event.submitter, async () => {
|
||||
const previous = envelope.config.global_security || {}, rate = previous.rate_limit || {};
|
||||
const countryMode = $("global-country-mode").value;
|
||||
const countries = globalLines("global-country-list").map(v => v.toUpperCase());
|
||||
if (countryMode !== "off" && !countries.length) throw fieldError("global-country-list", t("globalCountriesRequired"));
|
||||
if (countries.some(v => !/^[A-Z]{2}$/.test(v))) throw fieldError("global-country-list", t("globalCountriesInvalid"));
|
||||
const methodMode = $("global-method-mode").value;
|
||||
const methods = [...new Set($("global-method-list").value.split(/[\s,]+/).map(v => v.trim().toUpperCase()).filter(Boolean))];
|
||||
if (methodMode !== "off" && !methods.length) throw fieldError("global-method-list", t("globalMethodsRequired"));
|
||||
const contentTypes = globalLines("global-content-type-list").map(v => v.toLowerCase());
|
||||
if ($("global-content-type-enabled").checked && !contentTypes.length) throw fieldError("global-content-type-list", t("globalContentTypesRequired"));
|
||||
const acl = $("global-ip-enabled").checked ? { default: $("global-ip-default").value, rules: parseRules($("global-ip-rules").value, $("global-ip-rules")) } : null;
|
||||
const ua = { enabled: $("global-ua-block-enabled").checked, patterns: globalPatternLines("global-ua-block-patterns") };
|
||||
const security = {
|
||||
...previous,
|
||||
country_access: { mode: countryMode, countries },
|
||||
acl,
|
||||
method_policy: { mode: methodMode, methods },
|
||||
content_type_policy: { enabled: $("global-content-type-enabled").checked, blocked: contentTypes },
|
||||
rate_limit: { ...rate, enabled: $("global-rate-enabled").checked,
|
||||
requests: Number($("global-rate-requests").value),
|
||||
window_seconds: Number($("global-rate-window").value),
|
||||
burst: Number($("global-rate-burst").value) },
|
||||
};
|
||||
if (await saveSection("global-security", globalSecurityPayload(security, ua))) {
|
||||
setSettingsDirty("global-security-form", false); renderSettings();
|
||||
}
|
||||
});
|
||||
} catch (e) { toast(e.message, true); }
|
||||
});
|
||||
$("global-security-headers-form")?.addEventListener("submit", async event => {
|
||||
event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return;
|
||||
try {
|
||||
await withFormSubmitState(form, event.submitter, async () => {
|
||||
const previous = envelope.config.global_security || {};
|
||||
const optional = id => $(id).value.trim() || null;
|
||||
const security = { ...previous, security_headers: {
|
||||
hsts: { enabled: $("global-hsts-enabled").checked,
|
||||
max_age_seconds: Number($("global-hsts-age").value),
|
||||
include_subdomains: $("global-hsts-enabled").checked && $("global-hsts-sub").checked,
|
||||
preload: $("global-hsts-enabled").checked && $("global-hsts-preload").checked },
|
||||
nosniff: $("global-nosniff").checked, frame_options: $("global-frame").value,
|
||||
referrer_policy: optional("global-referrer"),
|
||||
content_security_policy: optional("global-csp"),
|
||||
permissions_policy: optional("global-permissions")
|
||||
}};
|
||||
if (await saveSection("global-security", globalSecurityPayload(security, previous.user_agent_blocking || { enabled: false, patterns: [] }))) {
|
||||
setSettingsDirty("global-security-headers-form", false); renderSettings();
|
||||
}
|
||||
});
|
||||
} catch (e) { toast(e.message, true); }
|
||||
});
|
||||
|
||||
$("password-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; try { await withFormSubmitState(form, event.submitter, async () => { if ($("new-password").value !== $("new-password-confirm").value) throw fieldError("new-password-confirm", t("passwordMismatch")); await api("/api/auth/password", { method: "POST", body: JSON.stringify({ current_password: $("current-password").value, new_password: $("new-password").value }) }); lock(); toast(t("passwordChanged")); }); } catch (e) { toast(e.message, true); } });
|
||||
$("revoke-sessions").addEventListener("click", async () => { if (!(await appConfirm(t("confirmRevoke"), { danger: true }))) return; try { await api("/api/auth/logout-all", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { toast(e.message, true); } });
|
||||
$("security-audit-open").addEventListener("click", () => { const dialog = $("audit-dialog"); if (!dialog.open) dialog.showModal(); void loadAudit(); });
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
output.textContent = target.hidden ? "" : commands({ ...options, user: account.value });
|
||||
output.hidden = !output.textContent;
|
||||
description.hidden = output.hidden;
|
||||
copy.hidden = !output.textContent;
|
||||
copy.disabled = !output.textContent;
|
||||
empty.hidden = !!output.textContent;
|
||||
empty.textContent = t(options.kind === "web" ? "pathPermissionsWebPrompt" : "pathPermissionsCertificatePrompt");
|
||||
@@ -133,7 +134,7 @@
|
||||
const empty = el("p", undefined, "fine");
|
||||
details.append(summary, label, help);
|
||||
actions.append(copy, details);
|
||||
target.replaceChildren(description, output, empty, actions);
|
||||
target.replaceChildren(output, description, empty, actions);
|
||||
view = { target, options, description, summary, help, accountLabel, account, output, empty, copy };
|
||||
views.set(target, view);
|
||||
account.addEventListener("input", () => update(view));
|
||||
|
||||
@@ -76,6 +76,61 @@ function renderSettings() {
|
||||
$("error-log-status").textContent = errorSink ? `${t("loggingQueue")}: ${fmt(errorSink.queue_dropped)} ${t("loggingDropped")} / ${fmt(errorSink.written)} ${t("loggingSent")} / ${fmt(errorSink.errors)} ${t("errors")} · ${t("loggingCapacity")}: ${fmt(errorSink.queue_capacity)}` : "";
|
||||
if (!settingsDirty.has("forwarding-form")) { $("trusted-proxies").value = (config.forwarding?.trusted_proxies || []).join("\n"); $("trusted-proto").checked = !!config.forwarding?.trust_forwarded_proto; }
|
||||
if (!settingsDirty.has("headers-form")) { for (const direction of ["request", "response"]) renderHeaderRules(direction, config.headers?.[direction] || []); }
|
||||
const global = config.global_security || {};
|
||||
if (!settingsDirty.has("global-security-form")) {
|
||||
const country = global.country_access || {}, methods = global.method_policy || {}, content = global.content_type_policy || {}, rate = global.rate_limit || {};
|
||||
const ua = global.user_agent_blocking || {}, acl = global.acl;
|
||||
$("global-country-mode").value = country.mode || "off";
|
||||
$("global-country-list").value = (country.countries || []).join("\n");
|
||||
$("global-ua-block-enabled").checked = ua.enabled === true;
|
||||
$("global-ua-block-patterns").value = (ua.patterns || []).join("\n");
|
||||
$("global-ip-enabled").checked = !!acl;
|
||||
$("global-ip-default").value = acl?.default || "allow";
|
||||
$("global-ip-rules").value = rulesText(acl);
|
||||
$("global-method-mode").value = methods.mode || "off";
|
||||
$("global-method-list").value = (methods.methods || []).join("\n");
|
||||
$("global-content-type-enabled").checked = content.enabled === true;
|
||||
$("global-content-type-list").value = (content.blocked || []).join("\n");
|
||||
$("global-rate-enabled").checked = rate.enabled === true;
|
||||
$("global-rate-requests").value = rate.requests ?? 120;
|
||||
$("global-rate-window").value = rate.window_seconds ?? 60;
|
||||
$("global-rate-burst").value = rate.burst ?? 30;
|
||||
syncGlobalSecurityControls();
|
||||
}
|
||||
if (!settingsDirty.has("global-security-headers-form")) {
|
||||
const headers = global.security_headers || {}, hsts = headers.hsts || {};
|
||||
$("global-hsts-enabled").checked = hsts.enabled === true;
|
||||
$("global-hsts-age").value = hsts.max_age_seconds ?? 31536000;
|
||||
$("global-hsts-sub").checked = hsts.include_subdomains === true;
|
||||
$("global-hsts-preload").checked = hsts.preload === true;
|
||||
$("global-nosniff").checked = headers.nosniff === true;
|
||||
$("global-frame").value = headers.frame_options || "preserve";
|
||||
$("global-referrer").value = headers.referrer_policy || "";
|
||||
$("global-csp").value = headers.content_security_policy || "";
|
||||
$("global-permissions").value = headers.permissions_policy || "";
|
||||
syncGlobalSecurityControls();
|
||||
}
|
||||
}
|
||||
function syncGlobalSecurityControls() {
|
||||
for (const [toggle, children] of [
|
||||
["global-country-mode", ["global-country-list"]],
|
||||
["global-ua-block-enabled", ["global-ua-block-patterns"]],
|
||||
["global-ip-enabled", ["global-ip-default", "global-ip-rules"]],
|
||||
["global-method-mode", ["global-method-list"]],
|
||||
["global-content-type-enabled", ["global-content-type-list"]],
|
||||
["global-rate-enabled", ["global-rate-requests", "global-rate-window", "global-rate-burst"]],
|
||||
["global-hsts-enabled", ["global-hsts-age", "global-hsts-sub", "global-hsts-preload"]]
|
||||
]) {
|
||||
const source = $(toggle);
|
||||
const enabled = source.type === "checkbox" ? source.checked : source.value !== "off";
|
||||
for (const id of children) $(id).disabled = !enabled;
|
||||
}
|
||||
}
|
||||
for (const id of ["global-country-mode", "global-ua-block-enabled", "global-ip-enabled", "global-method-mode", "global-content-type-enabled", "global-rate-enabled", "global-hsts-enabled"]) {
|
||||
$(id)?.addEventListener("change", syncGlobalSecurityControls);
|
||||
}
|
||||
for (const id of ["global-security-form", "global-security-headers-form"]) {
|
||||
for (const type of ["input", "change"]) $(id)?.addEventListener(type, () => setSettingsDirty(id));
|
||||
}
|
||||
function effectiveCacheMemory() {
|
||||
const d = latestStatus?.system || {}, host = Number(d.memory_total_bytes), cgroup = Number(d.cgroup_memory_max_bytes);
|
||||
|
||||
Reference in new issue
Block a user