410 lines
37 KiB
JavaScript
410 lines
37 KiB
JavaScript
function renderSettings() {
|
|
if (!envelope) return; const config = envelope.config; window.rustProxySettings?.render();
|
|
if (!settingsDirty.has("listeners-form")) window.rustProxyIntegrations?.renderListeners();
|
|
if (!settingsDirty.has("http2-form")) window.rustProxyIntegrations?.renderHttp2Settings();
|
|
if (!settingsDirty.has("http3-form")) window.rustProxyIntegrations?.renderHttp3Settings();
|
|
if (!settingsDirty.has("response-form")) {
|
|
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
|
$("expose-request-id").checked = !!diagnostics.request_id;
|
|
$("server-mode").value = server.mode || "custom"; $("server-value").value = server.value || "RustProxy";
|
|
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
|
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
|
}
|
|
if (!settingsDirty.has("cache-form")) {
|
|
const c = config.cache || {}; $("cache-enabled").checked = !!c.enabled; $("cache-memory").value = (c.max_bytes ?? 268435456) / 1048576;
|
|
$("cache-transient-memory").value = (c.transient_max_bytes ?? 67108864) / 1048576;
|
|
$("cache-object").value = (c.max_object_bytes ?? 16777216) / 1024; $("cache-entries").value = c.max_entries ?? 4096;
|
|
$("cache-eviction-limit").value = c.hot_eviction_max_objects ?? 64;
|
|
$("cache-auto-trim-enabled").checked = c.hot_auto_trim_enabled !== false;
|
|
$("cache-auto-trim-after").value = c.hot_auto_trim_after_seconds ?? 300;
|
|
$("cache-auto-trim-target").value = c.hot_auto_trim_target_percent ?? 80;
|
|
$("cache-cold-enabled").checked = !!c.cold_enabled; $("cache-cold-path").value = c.cold_path || "cache/cold";
|
|
$("cache-cold-bytes").value = (c.cold_max_bytes ?? 4294967296) / 1048576; $("cache-cold-entries").value = c.cold_max_entries ?? 50000; $("cache-cold-after").value = c.cold_after_seconds ?? 300;
|
|
$("cache-cold-queue-bytes").value = (c.cold_queue_max_bytes ?? 33554432) / 1048576;
|
|
$("cache-cold-gc-interval").value = c.cold_gc_interval_seconds ?? 60; $("cache-cold-gc-max-delete").value = c.cold_gc_max_delete ?? 256;
|
|
$("cache-cold-on-idle").checked = c.cold_on_idle !== false; $("cache-cold-on-age").checked = c.cold_on_age === true; $("cache-cold-on-lru").checked = c.cold_on_lru !== false; $("cache-cold-on-shutdown").checked = c.cold_on_shutdown !== false; $("cache-cold-max-hot").value = c.cold_max_hot_seconds ?? 3600;
|
|
$("cache-ttl").value = c.max_ttl_seconds ?? 86400; $("cache-default-ttl").value = c.default_ttl_seconds ?? 0;
|
|
$("cache-collapse-enabled").checked = c.collapse_enabled !== false; $("cache-collapse-timeout").value = c.collapse_wait_timeout_ms ?? 1500; $("cache-collapse-fallback").value = c.collapse_timeout_fallback || "origin";
|
|
$("cache-sie-timeout").checked = c.stale_if_error_timeout !== false; $("cache-sie-connection").checked = c.stale_if_error_connection !== false; $("cache-sie-dns").checked = c.stale_if_error_dns !== false; $("cache-sie-tls").checked = c.stale_if_error_tls !== false; $("cache-sie-statuses").value = (c.stale_if_error_statuses || [500, 502, 503, 504]).join(", ");
|
|
$("cache-proactive-enabled").checked = c.proactive_refresh_enabled === true; $("cache-proactive-before").value = c.proactive_refresh_before_expiry_seconds ?? 15; $("cache-proactive-percent").value = c.proactive_refresh_percent ?? 10; $("cache-proactive-inflight").value = c.proactive_refresh_max_inflight ?? 4;
|
|
$("cache-query").checked = !!c.bypass_query; $("cache-prefixes").value = (c.bypass_prefixes || []).join("\n");
|
|
$("cache-static-cookie").checked = c.static_ignore_cookies !== false; $("cache-static-extensions").value = (c.static_extensions || []).join("\n");
|
|
$("cache-status-header").checked = !!c.expose_status; $("cache-hits-header").checked = !!c.expose_hits; $("cache-rfc-header").checked = !!c.expose_cache_status; $("cache-resident-age-header").checked = !!c.expose_resident_age;
|
|
const viaHeader = c.via_header === undefined ? "RustProxy" : c.via_header;
|
|
$("cache-via-enabled").checked = viaHeader !== null; $("cache-via-value").value = viaHeader || "RustProxy"; syncCacheVia();
|
|
setCacheMemoryPresetSelection(["conservative", "recommended", "performance"].includes(c.memory_preset) ? c.memory_preset : null);
|
|
setCacheColdPresetSelection(c.cold_preset === "recommended" ? "recommended" : null);
|
|
syncColdCachePolicy();
|
|
syncAdvancedCachePolicy();
|
|
updateCacheMemorySizing();
|
|
}
|
|
if (!settingsDirty.has("transform-form")) {
|
|
const tr = config.transform || {};
|
|
setTransformPresetSelection(["conservative", "recommended", "performance", "uber"].includes(tr.preset) ? tr.preset : null);
|
|
$("transform-enabled").checked = !!tr.enabled;
|
|
$("transform-javascript").checked = tr.javascript !== false;
|
|
$("transform-css").checked = tr.css !== false;
|
|
$("transform-html").checked = !!tr.html;
|
|
$("transform-svg").checked = !!tr.svg;
|
|
$("transform-json").checked = !!tr.json;
|
|
$("transform-extensions").value = (tr.extensions || ["js", "mjs", "css", "html", "htm", "svg", "json"]).join("\n");
|
|
$("transform-max-body").value = (tr.max_body_bytes ?? 2097152) / 1024;
|
|
$("transform-memory-budget").value = (tr.memory_budget_bytes ?? 134217728) / 1048576;
|
|
$("transform-max-inflight").value = tr.max_inflight ?? 32;
|
|
updateTransformResourceSizing();
|
|
}
|
|
if (!settingsDirty.has("logging-form")) {
|
|
const l = config.logging || {}, file = l.file || {}, logstash = l.logstash || {}, loki = l.loki || {}, format = l.format || {};
|
|
$("log-capture-user-agent").checked = l.capture_user_agent !== false;
|
|
$("log-capture-country").checked = l.capture_country === true;
|
|
$("log-file-enabled").checked = !!file.enabled; $("log-file-per-vhost").checked = !!file.per_vhost; $("log-file-path").value = file.path || "/var/log/rustproxy/access.jsonl"; $("log-file-directory").value = file.directory || "/var/log/rustproxy/vhosts"; syncLoggingFileMode();
|
|
$("log-logstash-enabled").checked = !!logstash.enabled; $("log-logstash-address").value = logstash.address || "127.0.0.1:5044";
|
|
$("log-loki-enabled").checked = !!loki.enabled; $("log-loki-url").value = loki.url || "http://127.0.0.1:3100/loki/api/v1/push"; $("log-loki-token").value = loki.bearer_token_env || "";
|
|
$("log-loki-labels").value = JSON.stringify(loki.labels || { job: "rustproxy" }, null, 2); $("log-format-fields").value = JSON.stringify(format.fields || {}, null, 2); $("log-format-static").value = JSON.stringify(format.static_fields || {}, null, 2);
|
|
$("log-format-preset").value = detectLoggingPreset(format.fields || {});
|
|
}
|
|
if (!settingsDirty.has("error-log-form")) {
|
|
const errorLog = config.error_log || {};
|
|
$("error-log-enabled").checked = !!errorLog.enabled;
|
|
$("error-log-path").value = errorLog.path || "/var/log/rustproxy/error.log";
|
|
$("error-log-format").value = ["JSON", "RAW"].includes(errorLog.format) ? errorLog.format : "JSON";
|
|
syncErrorLogMode();
|
|
}
|
|
const sink = latestStatus?.logging?.sinks, errorSink = latestStatus?.logging?.error_file;
|
|
$("logging-status").textContent = sink ? `${t("loggingQueue")}: ${fmt(sink.queue_dropped)} ${t("loggingDropped")} / ${fmt(sink.sent)} ${t("loggingSent")} / ${fmt(sink.errors)} ${t("errors")} · ${t("loggingCapacity")}: ${fmt(sink.queue_capacity)}` : "";
|
|
$("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] || []); }
|
|
}
|
|
function effectiveCacheMemory() {
|
|
const d = latestStatus?.system || {}, host = Number(d.memory_total_bytes), cgroup = Number(d.cgroup_memory_max_bytes);
|
|
const candidates = [host, cgroup].filter(value => Number.isFinite(value) && value > 0);
|
|
if (!candidates.length) return null;
|
|
const bytes = Math.min(...candidates);
|
|
return { bytes, cgroupLimited: Number.isFinite(cgroup) && cgroup > 0 && (!Number.isFinite(host) || cgroup < host) };
|
|
}
|
|
function cacheMemoryHeadroom() {
|
|
const d = latestStatus?.system || {};
|
|
const hostTotal = Number(d.memory_total_bytes), hostUsed = Number(d.memory_used_bytes);
|
|
const cgroupMax = Number(d.cgroup_memory_max_bytes), cgroupCurrent = Number(d.cgroup_memory_current_bytes);
|
|
const candidates = [];
|
|
if (Number.isFinite(hostTotal) && hostTotal > 0 && Number.isFinite(hostUsed) && hostUsed >= 0) candidates.push(Math.max(0, hostTotal - hostUsed));
|
|
if (Number.isFinite(cgroupMax) && cgroupMax > 0 && Number.isFinite(cgroupCurrent) && cgroupCurrent >= 0) candidates.push(Math.max(0, cgroupMax - cgroupCurrent));
|
|
return candidates.length ? Math.min(...candidates) : null;
|
|
}
|
|
function cachePresetValues(kind, bytes) {
|
|
const totalMiB = bytes / 1048576, profile = {
|
|
conservative: { ratio: .125, objectMiB: 8, eviction: 32 },
|
|
recommended: { ratio: .20, objectMiB: 16, eviction: 64 },
|
|
performance: { ratio: .30, objectMiB: 32, eviction: 128 }
|
|
}[kind] || { ratio: .20, objectMiB: 16, eviction: 64 };
|
|
const round16 = value => Math.max(16, Math.floor(value / 16) * 16);
|
|
const hot = Math.max(64, Math.min(round16(totalMiB * profile.ratio), round16(totalMiB * .5)));
|
|
const transient = Math.max(16, round16(hot * .25));
|
|
const queue = Math.max(8, Math.floor((transient * .5) / 8) * 8);
|
|
const objectMiB = Math.max(1, Math.min(profile.objectMiB, Math.floor(hot / 16)));
|
|
const entries = Math.max(1024, Math.min(16384, Math.round(hot * 16)));
|
|
return { hot, transient, queue, objectKiB: objectMiB * 1024, entries, eviction: profile.eviction };
|
|
}
|
|
function cacheMemoryPresetName(kind) {
|
|
return kind ? t({ conservative: "cachePresetConservative", recommended: "cachePresetRecommended", performance: "cachePresetPerformance" }[kind]) : t("cachePresetCustom");
|
|
}
|
|
function setCacheMemoryPresetSelection(kind) {
|
|
selectedCacheMemoryPreset = ["conservative", "recommended", "performance"].includes(kind) ? kind : null;
|
|
document.querySelectorAll("[data-cache-memory-preset]").forEach(button => {
|
|
const active = button.dataset.cacheMemoryPreset === selectedCacheMemoryPreset;
|
|
button.classList.toggle("primary", active); button.classList.toggle("outline", !active);
|
|
button.setAttribute("aria-pressed", String(active));
|
|
});
|
|
const badge = $("cache-memory-preset-selected");
|
|
if (badge) badge.textContent = `${t("cachePresetSelected")}: ${cacheMemoryPresetName(selectedCacheMemoryPreset)}`;
|
|
}
|
|
function setCacheColdPresetSelection(kind) {
|
|
selectedCacheColdPreset = kind === "recommended" ? "recommended" : null;
|
|
document.querySelectorAll("[data-cache-cold-preset]").forEach(button => {
|
|
const active = button.dataset.cacheColdPreset === selectedCacheColdPreset;
|
|
button.classList.toggle("primary", active); button.classList.toggle("outline", !active);
|
|
button.setAttribute("aria-pressed", String(active));
|
|
});
|
|
const badge = $("cache-cold-preset-selected");
|
|
if (badge) badge.textContent = `${t("cachePresetSelected")}: ${selectedCacheColdPreset ? t("cachePresetRecommended") : t("cachePresetCustom")}`;
|
|
}
|
|
function updateCacheMemorySizing() {
|
|
const detected = effectiveCacheMemory(), badge = $("cache-memory-detected"), warning = $("cache-memory-warning");
|
|
if (!badge || !warning) return;
|
|
const buttons = document.querySelectorAll("[data-cache-memory-preset]");
|
|
if (!detected) {
|
|
badge.textContent = t("cacheMemoryNotDetected"); buttons.forEach(button => { button.disabled = true; });
|
|
$("cache-memory-preset-summary").textContent = t("cacheMemoryPresetUnavailable"); warning.hidden = true; return;
|
|
}
|
|
if (detected.bytes < 256 * 1048576) {
|
|
badge.textContent = `${detected.cgroupLimited ? t("cacheMemoryCgroup") : t("cacheMemoryDetected")}: ${fmtBytes(detected.bytes)}`;
|
|
buttons.forEach(button => { button.disabled = true; });
|
|
$("cache-memory-preset-summary").textContent = t("cacheMemoryPresetTooSmall");
|
|
warning.hidden = false; warning.textContent = t("cacheMemoryPresetTooSmall"); return;
|
|
}
|
|
buttons.forEach(button => { button.disabled = false; });
|
|
badge.textContent = `${detected.cgroupLimited ? t("cacheMemoryCgroup") : t("cacheMemoryDetected")}: ${fmtBytes(detected.bytes)}`;
|
|
const recommended = cachePresetValues("recommended", detected.bytes);
|
|
$("cache-memory-preset-summary").textContent = `${t("cacheMemoryRecommendedFor")} HOT ${recommended.hot} MiB · ${t("cachePresetTransientShort")} ${recommended.transient} MiB · ${t("cachePresetObjectShort")} ${recommended.objectKiB / 1024} MiB · ${t("cachePresetQueueShort")} ${recommended.queue} MiB`;
|
|
const hot = Number($("cache-memory")?.value) * 1048576, transient = Number($("cache-transient-memory")?.value) * 1048576;
|
|
const queue = Number($("cache-cold-queue-bytes")?.value) * 1048576;
|
|
const object = Number($("cache-object")?.value) * 1024;
|
|
const high = Number.isFinite(hot + transient) && hot + transient > detected.bytes * .35;
|
|
const invalidQueue = Number.isFinite(queue) && Number.isFinite(transient) && queue > transient;
|
|
const objectOverTransient = Number.isFinite(object) && Number.isFinite(transient) && object > transient;
|
|
const headroom = cacheMemoryHeadroom();
|
|
const currentCache = (Number(latestStatus?.cache?.hot_allocated_bytes ?? latestStatus?.cache?.resident_bytes) || 0) + (Number(latestStatus?.cache?.transient_bytes) || 0);
|
|
const additionalBudget = Math.max(0, hot + transient - currentCache);
|
|
const lowHeadroom = headroom != null && Number.isFinite(additionalBudget) && additionalBudget > headroom * .75;
|
|
warning.hidden = !(high || invalidQueue || objectOverTransient || lowHeadroom);
|
|
if (invalidQueue) warning.textContent = t("cacheQueueOverTransientWarning");
|
|
else if (objectOverTransient) warning.textContent = t("cacheObjectOverTransientWarning");
|
|
else if (lowHeadroom) warning.textContent = `${t("cacheMemoryHeadroomWarning")} ${fmtBytes(additionalBudget)} / ${fmtBytes(headroom)}.`;
|
|
else if (high) warning.textContent = `${t("cacheMemoryHighWarning")} ${fmtBytes(hot + transient)} / ${fmtBytes(detected.bytes)}.`;
|
|
}
|
|
function applyCacheMemoryPreset(kind) {
|
|
const detected = effectiveCacheMemory(); if (!detected || detected.bytes < 256 * 1048576) return;
|
|
const preset = cachePresetValues(kind, detected.bytes);
|
|
$("cache-memory").value = preset.hot; $("cache-transient-memory").value = preset.transient;
|
|
$("cache-object").value = preset.objectKiB; $("cache-entries").value = preset.entries;
|
|
$("cache-eviction-limit").value = preset.eviction; $("cache-cold-queue-bytes").value = preset.queue;
|
|
setCacheMemoryPresetSelection(kind);
|
|
setSettingsDirty("cache-form"); updateCacheMemorySizing();
|
|
}
|
|
function syncCacheVia() {
|
|
const enabled = $("cache-via-enabled")?.checked;
|
|
if ($("cache-via-value")) $("cache-via-value").disabled = !enabled;
|
|
}
|
|
function applyRecommendedColdCachePreset() {
|
|
const transient = Number($("cache-transient-memory").value);
|
|
const queue = Number.isFinite(transient) && transient > 0 ? Math.max(0.0625, Math.floor((transient * .5) / 8) * 8 || Math.min(transient, 8)) : 32;
|
|
$("cache-cold-enabled").checked = true;
|
|
if (!$("cache-cold-path").value.trim()) $("cache-cold-path").value = "cache/cold";
|
|
$("cache-cold-on-idle").checked = true; $("cache-cold-on-age").checked = false;
|
|
$("cache-cold-on-lru").checked = true; $("cache-cold-on-shutdown").checked = true;
|
|
$("cache-cold-after").value = 300; $("cache-cold-max-hot").value = 3600;
|
|
$("cache-cold-gc-interval").value = 60; $("cache-cold-gc-max-delete").value = 256;
|
|
$("cache-cold-queue-bytes").value = Math.min(queue, transient || queue);
|
|
setCacheColdPresetSelection("recommended");
|
|
syncColdCachePolicy(); setSettingsDirty("cache-form"); updateCacheMemorySizing();
|
|
}
|
|
function transformPresetValues(kind) {
|
|
return {
|
|
conservative: { memoryMiB: 32, bodyKiB: 1024, inflight: 8 },
|
|
recommended: { memoryMiB: 128, bodyKiB: 2048, inflight: 32 },
|
|
performance: { memoryMiB: 512, bodyKiB: 8192, inflight: 64 },
|
|
uber: { memoryMiB: 1024, bodyKiB: 32768, inflight: 128 }
|
|
}[kind] || { memoryMiB: 128, bodyKiB: 2048, inflight: 32 };
|
|
}
|
|
function transformPresetName(kind) {
|
|
return kind ? t({ conservative: "transformPresetConservative", recommended: "transformPresetRecommended", performance: "transformPresetPerformance", uber: "transformPresetUber" }[kind]) : t("cachePresetCustom");
|
|
}
|
|
function setTransformPresetSelection(kind) {
|
|
selectedTransformPreset = ["conservative", "recommended", "performance", "uber"].includes(kind) ? kind : null;
|
|
document.querySelectorAll("[data-transform-preset]").forEach(button => {
|
|
const active = button.dataset.transformPreset === selectedTransformPreset;
|
|
button.classList.toggle("primary", active); button.classList.toggle("outline", !active);
|
|
button.setAttribute("aria-pressed", String(active));
|
|
});
|
|
const badge = $("transform-preset-selected");
|
|
if (badge) badge.textContent = `${t("cachePresetSelected")}: ${transformPresetName(selectedTransformPreset)}`;
|
|
}
|
|
function updateTransformResourceSizing() {
|
|
const warning = $("transform-memory-warning"); if (!warning) return;
|
|
const memoryMiB = Number($("transform-memory-budget")?.value), bodyKiB = Number($("transform-max-body")?.value), inflight = Number($("transform-max-inflight")?.value);
|
|
const detected = effectiveCacheMemory();
|
|
const budgetBytes = memoryMiB * 1048576, bodyBytes = bodyKiB * 1024;
|
|
const worstEncodedEstimate = bodyBytes * 3;
|
|
const budgetCannotFitMax = Number.isFinite(budgetBytes) && Number.isFinite(worstEncodedEstimate) && budgetBytes < worstEncodedEstimate;
|
|
const highHostShare = detected && Number.isFinite(budgetBytes) && budgetBytes > detected.bytes * .5;
|
|
warning.hidden = !(budgetCannotFitMax || highHostShare);
|
|
if (budgetCannotFitMax) warning.textContent = t("transformBudgetTooSmallWarning");
|
|
else if (highHostShare) warning.textContent = `${t("transformBudgetHighWarning")} ${fmtBytes(budgetBytes)} / ${fmtBytes(detected.bytes)}.`;
|
|
const summary = $("transform-preset-summary");
|
|
if (summary) summary.textContent = `${t("transformResourceCurrent")}: ${Number.isFinite(memoryMiB) ? memoryMiB : "-"} MiB · ${Number.isFinite(bodyKiB) ? bodyKiB : "-"} KiB · ${t("transformMaxInflightShort")} ${Number.isFinite(inflight) ? inflight : "-"}`;
|
|
}
|
|
function applyTransformPreset(kind) {
|
|
const preset = transformPresetValues(kind);
|
|
$("transform-memory-budget").value = preset.memoryMiB;
|
|
$("transform-max-body").value = preset.bodyKiB;
|
|
$("transform-max-inflight").value = preset.inflight;
|
|
setTransformPresetSelection(kind);
|
|
setSettingsDirty("transform-form"); updateTransformResourceSizing();
|
|
}
|
|
function syncServerMode() { $("server-value").disabled = $("server-mode").value !== "custom"; $("server-value").required = !$("server-value").disabled; }
|
|
function syncLoggingFileMode() {
|
|
const enabled = $("log-file-enabled").checked, perVhost = $("log-file-per-vhost").checked;
|
|
$("log-file-per-vhost").disabled = !enabled;
|
|
$("log-file-path").disabled = !enabled || perVhost;
|
|
$("log-file-directory").disabled = !enabled || !perVhost;
|
|
}
|
|
function syncErrorLogMode() {
|
|
const disabled = !$("error-log-enabled").checked;
|
|
$("error-log-path").disabled = disabled;
|
|
$("error-log-format").disabled = disabled;
|
|
}
|
|
|
|
function syncColdCachePolicy() {
|
|
const enabled = $("cache-cold-enabled").checked;
|
|
for (const id of ["cache-cold-path", "cache-cold-bytes", "cache-cold-entries", "cache-cold-queue-bytes", "cache-cold-gc-interval", "cache-cold-gc-max-delete", "cache-cold-on-idle", "cache-cold-on-age", "cache-cold-on-lru", "cache-cold-on-shutdown"]) $(id).disabled = !enabled;
|
|
$("cache-cold-after").disabled = !enabled || !$("cache-cold-on-idle").checked;
|
|
$("cache-cold-max-hot").disabled = !enabled || !$("cache-cold-on-age").checked;
|
|
}
|
|
function syncAdvancedCachePolicy() {
|
|
const collapse = $("cache-collapse-enabled").checked;
|
|
$("cache-collapse-timeout").disabled = !collapse;
|
|
$("cache-collapse-fallback").disabled = !collapse;
|
|
const proactive = $("cache-proactive-enabled").checked;
|
|
for (const id of ["cache-proactive-before", "cache-proactive-percent", "cache-proactive-inflight"]) $(id).disabled = !proactive;
|
|
}
|
|
function bindSettingsForm(id, apply, section, selectValue = next => next[section]) {
|
|
const form = $(id); for (const event of ["input", "change"]) form.addEventListener(event, () => setSettingsDirty(id));
|
|
form.addEventListener("submit", async event => {
|
|
event.preventDefault(); if (!envelope || busy) return;
|
|
try {
|
|
await withFormSubmitState(form, event.submitter, async () => {
|
|
const next = structuredClone(envelope.config); apply(next);
|
|
if (await saveSection(section, selectValue(next))) { setSettingsDirty(id, false); renderSettings(); }
|
|
});
|
|
} catch (error) { toast(error.message, true); }
|
|
});
|
|
}
|
|
bindSettingsForm("listeners-form", next => { next.listeners = window.rustProxyIntegrations.collectListeners(); }, "listeners");
|
|
bindSettingsForm("http2-form", next => {
|
|
if (settingsDirty.has("listeners-form")) throw new Error(t("http2ListenersDirty"));
|
|
next.listeners = window.rustProxyIntegrations.collectHttp2Listeners();
|
|
}, "listeners", next => next.listeners);
|
|
bindSettingsForm("http3-form", next => {
|
|
if (settingsDirty.has("listeners-form")) throw new Error(t("http3ListenersDirty"));
|
|
next.listeners = window.rustProxyIntegrations.collectHttp3Listeners();
|
|
}, "listeners", next => next.listeners);
|
|
bindSettingsForm("response-form", next => {
|
|
next.server_header = { mode: $("server-mode").value, value: $("server-value").value };
|
|
next.diagnostic_headers = { request_id: $("expose-request-id").checked };
|
|
next.compression = { min_bytes: Number($("compress-min").value) }; for (const name of ["gzip", "br", "zstd", "deflate"]) next.compression[name] = $(`compress-${name}`).checked;
|
|
}, "response", next => ({ server_header: next.server_header, diagnostic_headers: next.diagnostic_headers, compression: next.compression }));
|
|
bindSettingsForm("cache-form", next => {
|
|
next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked, via_header: $("cache-via-enabled").checked ? $("cache-via-value").value.trim() : null };
|
|
}, "cache");
|
|
bindSettingsForm("transform-form", next => {
|
|
next.transform = {
|
|
enabled: $("transform-enabled").checked,
|
|
preset: selectedTransformPreset,
|
|
javascript: $("transform-javascript").checked,
|
|
css: $("transform-css").checked,
|
|
html: $("transform-html").checked,
|
|
svg: $("transform-svg").checked,
|
|
json: $("transform-json").checked,
|
|
extensions: $("transform-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean),
|
|
max_body_bytes: Math.round(Number($("transform-max-body").value) * 1024),
|
|
memory_budget_bytes: Math.round(Number($("transform-memory-budget").value) * 1048576),
|
|
max_inflight: Number($("transform-max-inflight").value)
|
|
};
|
|
}, "transform");
|
|
const LOGGING_SOURCE_FIELDS = ["event", "protocol_error", "error_detail", "local_addr", "tls_sni", "vhost", "route", "cache_effective_enabled", "transform_effective_enabled", "backend", "id", "time_ms", "method", "host", "path", "http_version", "transport", "websocket_bootstrap", "websocket_phase", "websocket_error", "h3_reset_code", "upstream_status", "upstream_error", "client_ip", "user_agent", "country", "status", "duration_ms", "request_bytes", "response_bytes", "outcome", "cache", "cache_reason"];
|
|
const LOGGING_PRESETS = {
|
|
standard: {},
|
|
minimal: { time_ms: "time_ms", method: "method", host: "host", path: "path", status: "status", duration_ms: "duration_ms" },
|
|
nginx: { time_ms: "time_ms", remote_addr: "client_ip", http_user_agent: "user_agent", country: "country", request_method: "method", host: "host", request_uri: "path", status: "status", request_time_ms: "duration_ms", request_length: "request_bytes", bytes_sent: "response_bytes", upstream: "backend", cache: "cache", cache_reason: "cache_reason" },
|
|
ecs: { "event.created_ms": "time_ms", "client.address": "client_ip", "user_agent.original": "user_agent", "client.geo.country_iso_code": "country", "http.request.method": "method", "server.address": "host", "url.path": "path", "http.response.status_code": "status", "event.duration_ms": "duration_ms", "http.request.body.bytes": "request_bytes", "http.response.body.bytes": "response_bytes", "service.target.name": "backend", "event.outcome": "outcome", "cache.result": "cache", "cache.reason": "cache_reason" }
|
|
};
|
|
const LOGGING_PREVIEW_VALUES = { event: "request", protocol_error: null, error_detail: null, local_addr: "192.0.2.1:443", tls_sni: "app.example", vhost: "app", route: "root", cache_effective_enabled: true, transform_effective_enabled: false, backend: "origin-1", id: 1842, time_ms: 1769328123456, method: "CONNECT", host: "app.example", path: "/socket.io/", http_version: "3", transport: "h3", websocket_bootstrap: "h3_extended_connect", websocket_phase: "tunnel_closed", websocket_error: null, h3_reset_code: null, upstream_status: 101, client_ip: "203.0.113.24", user_agent: "Mozilla/5.0 RustProxy-Test", country: "PL", status: 200, duration_ms: 12, request_bytes: 0, response_bytes: 4821, outcome: "websocket_closed", cache: "BYPASS", cache_reason: "request_upgrade" };
|
|
function parseJsonObjectValue(value, target = null) { let parsed; try { parsed = JSON.parse(value || "{}"); } catch { throw fieldError(target, t("loggingJsonInvalid")); } if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw fieldError(target, t("loggingJsonInvalid")); return parsed; }
|
|
function loggingMappingKey(mapping) { return JSON.stringify(Object.entries(mapping || {}).sort(([a], [b]) => a.localeCompare(b))); }
|
|
function detectLoggingPreset(mapping) { const key = loggingMappingKey(mapping); for (const [name, preset] of Object.entries(LOGGING_PRESETS)) if (loggingMappingKey(preset) === key) return name; return "custom"; }
|
|
function selectedLoggingMapping() {
|
|
const rows = [...$("logging-field-rows").querySelectorAll("tr")], mapping = {};
|
|
for (const row of rows) {
|
|
const enabled = row.querySelector("input[type=checkbox]").checked;
|
|
const source = row.dataset.source;
|
|
const output = row.querySelector("input[type=text]").value.trim();
|
|
if (enabled) {
|
|
if (!output) throw new Error(t("loggingOutputRequired"));
|
|
if (Object.hasOwn(mapping, output)) throw new Error(t("loggingOutputDuplicate"));
|
|
mapping[output] = source;
|
|
}
|
|
}
|
|
if (Object.keys(mapping).length === LOGGING_SOURCE_FIELDS.length && LOGGING_SOURCE_FIELDS.every(name => mapping[name] === name)) return {};
|
|
return mapping;
|
|
}
|
|
function renderLoggingPreview() {
|
|
let mapping;
|
|
try { mapping = selectedLoggingMapping(); } catch (error) { $("logging-format-preview").textContent = error.message; return; }
|
|
let staticFields = {}; try { staticFields = parseJsonObjectValue($("log-format-static").value, $("log-format-static")); } catch { }
|
|
const source = Object.keys(mapping).length ? mapping : Object.fromEntries(LOGGING_SOURCE_FIELDS.map(name => [name, name]));
|
|
const preview = { ...staticFields };
|
|
for (const [output, name] of Object.entries(source)) preview[output] = LOGGING_PREVIEW_VALUES[name];
|
|
$("logging-format-preview").textContent = JSON.stringify(preview, null, 2);
|
|
}
|
|
function openLoggingBuilder() {
|
|
let mapping; try { mapping = parseJsonObjectValue($("log-format-fields").value, $("log-format-fields")); } catch (error) { toast(error.message, true); return; }
|
|
const identity = !Object.keys(mapping).length;
|
|
const bySource = new Map();
|
|
for (const [output, source] of Object.entries(mapping)) if (!bySource.has(source)) bySource.set(source, output);
|
|
const rows = LOGGING_SOURCE_FIELDS.map(source => {
|
|
const row = el("tr"); row.dataset.source = source;
|
|
const checkCell = el("td"), sourceCell = el("td", source), outputCell = el("td");
|
|
const check = document.createElement("input"); check.type = "checkbox"; check.checked = identity || bySource.has(source);
|
|
const output = document.createElement("input"); output.type = "text"; output.maxLength = 128; output.value = bySource.get(source) || source; output.disabled = !check.checked;
|
|
check.addEventListener("change", () => { output.disabled = !check.checked; renderLoggingPreview(); });
|
|
output.addEventListener("input", renderLoggingPreview);
|
|
checkCell.append(check); outputCell.append(output); row.append(checkCell, sourceCell, outputCell); return row;
|
|
});
|
|
$("logging-field-rows").replaceChildren(...rows); renderLoggingPreview();
|
|
const dialog = $("logging-format-dialog"); if (!dialog.open) dialog.showModal();
|
|
}
|
|
function applyLoggingPreset(name) {
|
|
if (name === "custom") return;
|
|
$("log-format-fields").value = JSON.stringify(LOGGING_PRESETS[name], null, 2);
|
|
setSettingsDirty("logging-form");
|
|
}
|
|
const selectLoggingSection = next => ({ logging: next.logging, error_log: next.error_log });
|
|
bindSettingsForm("logging-form", next => {
|
|
const parseObject = id => parseJsonObjectValue($(id).value, $(id));
|
|
next.logging = {
|
|
capture_user_agent: $("log-capture-user-agent").checked,
|
|
capture_country: $("log-capture-country").checked,
|
|
file: { enabled: $("log-file-enabled").checked, path: $("log-file-path").value.trim(), per_vhost: $("log-file-per-vhost").checked, directory: $("log-file-directory").value.trim() },
|
|
logstash: { enabled: $("log-logstash-enabled").checked, address: $("log-logstash-address").value.trim() },
|
|
loki: { enabled: $("log-loki-enabled").checked, url: $("log-loki-url").value.trim(), bearer_token_env: $("log-loki-token").value.trim() || null, labels: parseObject("log-loki-labels") },
|
|
format: { fields: parseObject("log-format-fields"), static_fields: parseObject("log-format-static") }
|
|
};
|
|
}, "logging", selectLoggingSection);
|
|
bindSettingsForm("error-log-form", next => {
|
|
next.error_log = {
|
|
enabled: $("error-log-enabled").checked,
|
|
path: $("error-log-path").value.trim(),
|
|
format: $("error-log-format").value
|
|
};
|
|
}, "logging", selectLoggingSection);
|
|
bindSettingsForm("forwarding-form", next => { next.forwarding = { trusted_proxies: $("trusted-proxies").value.split(/[\n,]/).map(s => s.trim()).filter(Boolean), trust_forwarded_proto: $("trusted-proto").checked }; }, "forwarding");
|
|
bindSettingsForm("headers-form", next => { next.headers = { request: collectHeaderRules("request"), response: collectHeaderRules("response") }; }, "headers");
|
|
$("log-format-preset").addEventListener("change", event => applyLoggingPreset(event.target.value));
|
|
$("error-log-enabled").addEventListener("change", syncErrorLogMode);
|
|
$("log-format-open").addEventListener("click", openLoggingBuilder);
|
|
$("logging-format-close").addEventListener("click", () => $("logging-format-dialog").close());
|
|
$("logging-format-cancel").addEventListener("click", () => $("logging-format-dialog").close());
|
|
$("logging-format-apply").addEventListener("click", () => { try { const mapping = selectedLoggingMapping(); $("log-format-fields").value = JSON.stringify(mapping, null, 2); $("log-format-preset").value = detectLoggingPreset(mapping); setSettingsDirty("logging-form"); $("logging-format-dialog").close(); } catch (error) { toast(error.message, true); } });
|
|
$("server-mode").addEventListener("change", syncServerMode);
|
|
$("log-file-enabled").addEventListener("change", syncLoggingFileMode);
|
|
$("log-file-per-vhost").addEventListener("change", syncLoggingFileMode);
|
|
for (const id of ["cache-cold-enabled", "cache-cold-on-idle", "cache-cold-on-age"]) $(id).addEventListener("change", syncColdCachePolicy);
|
|
$("cache-via-enabled").addEventListener("change", syncCacheVia);
|
|
document.querySelectorAll("[data-cache-memory-preset]").forEach(button => button.addEventListener("click", () => applyCacheMemoryPreset(button.dataset.cacheMemoryPreset)));
|
|
document.querySelectorAll("[data-cache-cold-preset]").forEach(button => button.addEventListener("click", applyRecommendedColdCachePreset));
|
|
document.querySelectorAll("[data-transform-preset]").forEach(button => button.addEventListener("click", () => applyTransformPreset(button.dataset.transformPreset)));
|
|
for (const id of ["cache-memory", "cache-transient-memory", "cache-object", "cache-entries", "cache-eviction-limit"]) $(id).addEventListener("input", () => { setCacheMemoryPresetSelection(null); updateCacheMemorySizing(); });
|
|
$("cache-cold-queue-bytes").addEventListener("input", () => { setCacheMemoryPresetSelection(null); setCacheColdPresetSelection(null); updateCacheMemorySizing(); });
|
|
for (const id of ["cache-cold-enabled", "cache-cold-on-idle", "cache-cold-on-age", "cache-cold-on-lru", "cache-cold-on-shutdown", "cache-cold-after", "cache-cold-max-hot", "cache-cold-gc-interval", "cache-cold-gc-max-delete"]) {
|
|
for (const event of ["input", "change"]) $(id).addEventListener(event, () => setCacheColdPresetSelection(null));
|
|
}
|
|
for (const id of ["transform-memory-budget", "transform-max-body", "transform-max-inflight"]) $(id).addEventListener("input", () => { setTransformPresetSelection(null); updateTransformResourceSizing(); });
|
|
for (const id of ["cache-collapse-enabled", "cache-proactive-enabled"]) $(id).addEventListener("change", syncAdvancedCachePolicy);
|
|
syncColdCachePolicy();
|
|
syncAdvancedCachePolicy();
|
|
$("purge-cache").addEventListener("click", async () => { if (!(await appConfirm(t("confirmPurge"), { danger: true }))) return; const button = $("purge-cache"); button.disabled = true; try { const result = await api("/api/cache/purge", { method: "POST", body: "{}" }); toast(`${t("cachePurged")}: ${result.purged_entries}`); await refresh(true); } catch (error) { toast(error.message, true); } finally { button.disabled = false; } });
|
|
$("add-response-header").addEventListener("click", () => appendHeaderRule("response"));
|
|
$("add-request-header").addEventListener("click", () => appendHeaderRule("request"));
|
|
window.addEventListener("beforeunload", event => { if (settingsDirty.size) { event.preventDefault(); event.returnValue = ""; } });
|