This commit is contained in:
Mateusz Gruszczyński committed 2026-10-07 08:51:04 +02:00
1 parent a9f9450f80
commit 762eb9e28b
81 files changed
+1242 -1095

No files matched your search

+1
View File
@@ -409,6 +409,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
"/docs/V0.25.1_AUDIT.md"
)),
),
reader_doc!("docs/V0.27.0_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.26.7_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.26.6_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.26.5_IMPLEMENTATION.md", "technical"),
+106 -5
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.26.7",
"version": "0.27.0",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
},
"servers": [
@@ -529,6 +529,9 @@
},
"source": {
"$ref": "#/components/schemas/Source"
},
"compression": {
"$ref": "#/components/schemas/CompressionOverrides"
}
},
"additionalProperties": true,
@@ -575,9 +578,7 @@
"$ref": "#/components/schemas/CacheSettings"
},
"compression": {
"type": "object",
"properties": {},
"additionalProperties": true
"$ref": "#/components/schemas/CompressionSettings"
},
"headers": {
"$ref": "#/components/schemas/HeaderRules"
@@ -2418,6 +2419,36 @@
"type": "boolean",
"default": false,
"description": "Enable the configured static-response transform engine for the generated Docker vhost. Global transform must also be enabled."
},
"compression_enabled": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Per-published-vhost dynamic compression override. null inherits the enclosing/global policy; true/false enables or disables the scope."
},
"compression_zstd": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Optional per-Docker-vhost Zstandard override. null inherits; globally disabled codecs remain unavailable."
},
"compression_br": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Optional per-Docker-vhost Brotli override. null inherits; globally disabled codecs remain unavailable."
},
"compression_gzip": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Optional per-Docker-vhost gzip override. null inherits; globally disabled codecs remain unavailable."
},
"compression_deflate": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Optional per-Docker-vhost deflate override. null inherits; globally disabled codecs remain unavailable."
}
},
"required": [
@@ -4761,6 +4792,9 @@
}
},
"description": "Optional Web Service hardening. This is a bounded deterministic compatibility/security layer, not a generic WAF."
},
"compression": {
"$ref": "#/components/schemas/CompressionOverrides"
}
},
"required": [
@@ -5061,6 +5095,73 @@
}
},
"additionalProperties": false
},
"CompressionSettings": {
"type": "object",
"additionalProperties": false,
"properties": {
"gzip": {
"type": "boolean",
"default": true
},
"br": {
"type": "boolean",
"default": true
},
"zstd": {
"type": "boolean",
"default": true
},
"deflate": {
"type": "boolean",
"default": false
},
"min_bytes": {
"type": "integer",
"format": "uint64",
"minimum": 32,
"maximum": 1048576,
"default": 1024
}
},
"description": "Global downstream compression codecs and minimum size. Client q-values win; equal-quality codecs use zstd, then br, gzip and deflate. Existing Content-Encoding is never recompressed."
},
"CompressionOverrides": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "null inherits the enclosing policy; true/false explicitly enables or disables dynamic compression at this scope."
},
"zstd": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Optional Zstandard override at this scope. null inherits; true is effective only when the codec is enabled globally; false disables it."
},
"br": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Optional Brotli override at this scope. null inherits; true is effective only when the codec is enabled globally; false disables it."
},
"gzip": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Optional gzip override at this scope. null inherits; true is effective only when the codec is enabled globally; false disables it."
},
"deflate": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Optional deflate override at this scope. null inherits; true is effective only when the codec is enabled globally; false disables it."
}
},
"description": "Scoped dynamic-compression policy. The enabled switch and individual codecs inherit by default; global Response codec switches are the hard availability ceiling and min_bytes remains global."
}
}
},
@@ -5423,7 +5524,7 @@
}
}
},
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7\u201330 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7–30 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
"security": [],
"parameters": [
{
+1 -1
View File
@@ -6,7 +6,7 @@ function renderSettings() {
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 || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.7";
$("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();
}
+12 -4
View File
@@ -251,7 +251,8 @@
const inline = row.querySelector(".docker-acl-inline")?.checked;
return {
service_key: row.dataset.key, hostname: row.querySelector(".docker-hostname").value.trim().toLowerCase(), aliases: row._readAliases?.() || [], target_ip: row.querySelector(".docker-target-ip").value || null,
enabled: true, scheme: row.querySelector(".docker-scheme").value, preserve_host: row.querySelector(".docker-preserve-host").checked, force_ssl: row.querySelector(".docker-force-ssl").checked, websockets: row.querySelector(".docker-websockets").checked, cache_enabled: row.querySelector(".docker-cache-enabled").checked, transform_enabled: row.querySelector(".docker-transform-enabled").checked,
enabled: true, scheme: row.querySelector(".docker-scheme").value, preserve_host: row.querySelector(".docker-preserve-host").checked, force_ssl: row.querySelector(".docker-force-ssl").checked, websockets: row.querySelector(".docker-websockets").checked, cache_enabled: row.querySelector(".docker-cache-enabled").checked, transform_enabled: row.querySelector(".docker-transform-enabled").checked, compression_enabled: (() => { const value = row.querySelector(".docker-compression-enabled").value; return value === "" ? null : value === "on"; })(),
compression_zstd: (() => { const value = row.querySelector(".docker-compression-zstd").value; return value === "" ? null : value === "on"; })(), compression_br: (() => { const value = row.querySelector(".docker-compression-br").value; return value === "" ? null : value === "on"; })(), compression_gzip: (() => { const value = row.querySelector(".docker-compression-gzip").value; return value === "" ? null : value === "on"; })(), compression_deflate: (() => { const value = row.querySelector(".docker-compression-deflate").value; return value === "" ? null : value === "on"; })(),
acl_profile_id: row.querySelector(".docker-acl-profile")?.value || null, acl: inline ? { default: row.querySelector(".docker-acl-default").value, rules: parseRules(row.querySelector(".docker-acl-rules").value) } : null,
proxy_timeouts: { connect_timeout_seconds: Number(row.querySelector(".docker-proxy-connect").value), response_header_timeout_seconds: Number(row.querySelector(".docker-proxy-header").value), read_timeout_seconds: Number(row.querySelector(".docker-proxy-read").value), send_timeout_seconds: Number(row.querySelector(".docker-proxy-send").value) },
health_check: collectHealth(row), passive_health: collectPassiveHealth(row)
@@ -274,7 +275,7 @@
const runtime = runtimeFor(agent.id, key), visibility = el("button", "", "quiet docker-view-toggle"); visibility.type = "button";
const syncVisibility = () => { visibility.textContent = t(hiddenVhosts.has(row.dataset.viewKey) ? "dockerUnhideVhost" : "dockerHideVhost"); }; syncVisibility(); visibility.addEventListener("click", () => { const key = row.dataset.viewKey; if (hiddenVhosts.has(key)) hiddenVhosts.delete(key); else hiddenVhosts.add(key); saveHidden(); syncVisibility(); applyViewFilters(); });
const openApp = el("a", t("dockerOpenApp"), "outline docker-open-app"); openApp.target = "_blank"; openApp.rel = "noopener noreferrer";
const syncOpenApp = hostname => { const url = proxiedUrl(agent, hostname); openApp.href = url || "#"; openApp.hidden = !publish.checked || !url || stale; openApp.title = url || ""; };
const syncOpenApp = hostname => { const url = proxiedUrl(agent, hostname); openApp.href = url || "#"; openApp.hidden = !publish.checked || !url; openApp.title = url || ""; };
syncOpenApp(legacyHostname(assignment));
const headActions = el("div", undefined, "docker-service-head-actions"); headActions.append(runtimeSummary(runtime, assignment, stale), openApp, visibility); top.append(publishLabel, meta, headActions); row.append(top);
@@ -288,8 +289,14 @@
const websockets = input("", "checkbox", "docker-websockets"); websockets.checked = assignment?.websockets !== false; const websocketField = checkboxField("dockerWebsockets", websockets);
const cacheEnabled = input("", "checkbox", "docker-cache-enabled"); cacheEnabled.checked = assignment?.cache_enabled === true; const cacheField = checkboxField("dockerCache", cacheEnabled);
const transformEnabled = input("", "checkbox", "docker-transform-enabled"); transformEnabled.checked = assignment?.transform_enabled === true; const transformField = checkboxField("dockerTransform", transformEnabled);
const compressionSelect = (className, current) => { const select = el("select", undefined, className); for (const [value, keyName] of [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]]) { const option = el("option", t(keyName)); option.value = value; select.append(option); } select.value = current === true ? "on" : current === false ? "off" : ""; return select; };
const compressionEnabled = compressionSelect("docker-compression-enabled", assignment?.compression_enabled);
const compressionZstd = compressionSelect("docker-compression-zstd", assignment?.compression_zstd), compressionBr = compressionSelect("docker-compression-br", assignment?.compression_br), compressionGzip = compressionSelect("docker-compression-gzip", assignment?.compression_gzip), compressionDeflate = compressionSelect("docker-compression-deflate", assignment?.compression_deflate);
const compressionFields = [[labeled("compressionZstd", compressionZstd), compressionZstd], [labeled("compressionBr", compressionBr), compressionBr], [labeled("compressionGzip", compressionGzip), compressionGzip], [labeled("compressionDeflate", compressionDeflate), compressionDeflate]];
const syncCompressionVisibility = () => { const disabled = compressionEnabled.value === "off"; for (const [field, control] of compressionFields) { field.hidden = disabled; control.disabled = disabled; } };
compressionEnabled.addEventListener("change", syncCompressionVisibility); syncCompressionVisibility();
const switches = el("div", undefined, "docker-switch-row"); switches.append(preserveLabel, forceSslField, websocketField, cacheField, transformField);
route.append(labeled("dockerHostname", host, t("dockerHostnameHelp").replace("{domain}", agent.base_domain)), aliases.node, labeled("dockerBackendIp", target), labeled("dockerBackendScheme", scheme), switches);
route.append(labeled("dockerHostname", host, t("dockerHostnameHelp").replace("{domain}", agent.base_domain)), aliases.node, labeled("dockerBackendIp", target), labeled("dockerBackendScheme", scheme), labeled("dockerCompression", compressionEnabled, t("dockerCompressionHelp")), ...compressionFields.map(([field]) => field), switches);
editor.append(route, aclEditor(assignment, agent), timeoutEditor(assignment), healthEditor(assignment), passiveHealthEditor(assignment));
const actions = el("div", undefined, "docker-service-actions"), apply = el("button", t("dockerApplyService"), "outline docker-apply"); apply.type = "button"; apply.disabled = !publish.checked; apply.addEventListener("click", async () => { apply.disabled = true; try { await saveAssignment(agent.id, row, true); } catch (error) { toast(error.message, true); } finally { if (row.isConnected) apply.disabled = !publish.checked; } }); actions.append(apply); editor.append(actions); row.append(editor);
function updateHint() { const targetLabel = target.selectedOptions?.[0]?.textContent || t("dockerBackendIpAuto"), aliasCount = aliases.count(); editorHint.textContent = `${host.value.trim() || technical} → ${scheme.value.toUpperCase()} · ${targetLabel}${aliasCount ? ` · +${aliasCount} ${t("dockerAliasesShort")}` : ""}`; } updateHint();
@@ -312,7 +319,8 @@
const state = obs(agent.id), box = el("article", undefined, "card docker-agent-card"); box.dataset.agentId = agent.id; box.dataset.searchText = `${agent.id} ${agent.name || ""} ${agent.base_domain || ""}`.toLowerCase(); const head = el("div", undefined, "card-heading"), left = el("div"); left.append(el("span", agent.id, "small-label"), el("h2", agent.name));
const badges = el("div", undefined, "entity-actions"), connection = el("span", state?.ok ? t("dockerConnected") : state ? t("dockerError") : t("dockerWaiting"), `pill ${state?.ok ? "live" : ""}`), sec = securityInfo(agent, state); badges.append(el("span", t(sec.key), `pill ${sec.cls}`.trim()), connection); head.append(left, badges); box.append(head);
if (!agent.enabled) { box.append(el("p", t("dockerAgentDisabled"), "fine")); target.append(box); continue; }
if (!state?.ok || !state.inventory) { const error = el("div", undefined, "docker-agent-error"); error.append(el("b", t("dockerAgentConnectionFailed")), el("p", state?.error || t("dockerNoObservation"), "fine"), el("small", sec.detail, "fine")); box.append(error); target.append(box); continue; }
if (!state?.ok) { const error = el("div", undefined, "docker-agent-error"); error.append(el("b", t("dockerAgentConnectionFailed")), el("p", state?.error || t("dockerNoObservation"), "fine"), el("small", sec.detail, "fine")); box.append(error); }
if (!state?.inventory) { const rows = el("div", undefined, "docker-service-list"); for (const item of agent.assignments || []) rows.append(assignmentRow(agent, null, null, item, true)); if (!rows.children.length) rows.append(el("p", t("dockerNoPublishedPorts"), "empty")); box.append(rows); target.append(box); continue; }
const inv = state.inventory, defaultIp = agent.default_target_ip || agent.target_ip_override || t("dockerTargetAuto"), serviceCount = (inv.services || []).length, overview = el("div", undefined, "docker-agent-overview"); box.dataset.searchText += ` ${inv.hostname || ""}`.toLowerCase();
overview.append(el("span", `${t("dockerHost")}: ${inv.hostname}`), el("span", `${t("dockerServices")}: ${serviceCount}`), el("span", `${t("dockerLastChecked")}: ${checkedAgo(state.checked_at_unix_ms)}`), el("span", `*.${agent.base_domain}`)); box.append(overview);
const agentDetails = el("details", undefined, "docker-agent-details"), detailsSummary = el("summary", t("dockerAgentDetails")), summary = el("div", undefined, "docker-agent-summary");
+12 -7
View File
@@ -1921,10 +1921,10 @@
id="server-mode">
<option data-i18n="serverPreserve" value="preserve">Preserve origin value</option>
<option data-i18n="serverHide" value="hide">Hide completely</option>
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
<option data-i18n="serverCustom" selected value="custom">Replace / add custom value</option>
</select>
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.7"
id="server-value" maxlength="256" placeholder="RustProxy" value="RustProxy"
spellcheck="false" />
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
<div class="switch-grid">
@@ -1941,9 +1941,7 @@
<label data-i18n="compressionMin" for="compress-min">Minimum response size
(bytes)</label><input id="compress-min" max="1048576" min="32" required=""
type="number" />
<p class="fine" data-i18n="compressionHelp">Negotiates Accept-Encoding. Skips private
responses, cookies, ranges, SSE and no-transform; existing compressed bodies are
passed through.</p>
<p class="fine" data-i18n="compressionHelp">Negotiates Accept-Encoding using client q-values and zstd → br → gzip → deflate as the server preference for equal quality. Cookies, Authorization, Set-Cookie, private and no-store responses may be compressed. Existing Content-Encoding, ranges, SSE, upgrades and no-transform are passed through.</p>
<div class="form-actions"><button class="primary" data-i18n="saveResponse"
type="submit">Apply response settings</button></div>
</form>
@@ -2723,7 +2721,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.26.7</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>0.27.0</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
@@ -3107,6 +3105,13 @@
this controls browser/proxy caching and does not create a second RustProxy object cache.</p>
</div>
<div class="vh-grid web-service-page" data-web-service-page="delivery" hidden>
<label><span data-i18n="webServiceCompression">Dynamic compression</span><select id="web-service-compression"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select><small class="fine" data-i18n="webServiceCompressionHelp">Inherits the vhost/global policy by default. Codecs can be filtered per service; globally disabled codecs stay unavailable.</small></label>
<div class="vh-wide vh-grid" id="web-service-compression-options">
<label><span data-i18n="compressionZstd">Zstandard (zstd)</span><select id="web-service-compression-zstd"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select></label>
<label><span data-i18n="compressionBr">Brotli (br)</span><select id="web-service-compression-br"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select></label>
<label><span data-i18n="compressionGzip">gzip</span><select id="web-service-compression-gzip"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select></label>
<label><span data-i18n="compressionDeflate">deflate</span><select id="web-service-compression-deflate"><option value="" data-i18n="inherit">Inherit</option><option value="on" data-i18n="enabled">Enabled</option><option value="off" data-i18n="notEnabled">Disabled</option></select></label>
</div>
<label class="check"><input id="web-service-precompressed" type="checkbox" /><span
data-i18n="webServicePrecompressed">Serve precompressed sidecars</span></label>
<label class="check"><input id="web-service-precompressed-br" type="checkbox" /><span
@@ -3434,7 +3439,7 @@ max_execution_time = 30</code></pre>
<article class="card about-card">
<div class="about-product">
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
<div><span class="small-label">RustProxy</span><strong>0.26.7</strong></div>
<div><span class="small-label">RustProxy</span><strong>0.27.0</strong></div>
</div>
<dl class="about-meta">
<div>
+18 -1
View File
@@ -1738,8 +1738,25 @@ progress {
display: block;
width: 100%;
height: 5px;
margin: 6px 0 9px;
border: 0;
appearance: none;
-webkit-appearance: none;
background: var(--line);
accent-color: var(--green);
margin: 6px 0 9px
overflow: hidden
}
progress::-webkit-progress-bar {
background: var(--line)
}
progress::-webkit-progress-value {
background: var(--green)
}
progress::-moz-progress-bar {
background: var(--green)
}
.diagnostics-grid {
+4 -3
View File
@@ -22,7 +22,7 @@
};
const group = (key, advanced = false) => {
const box = el(advanced ? "details" : "fieldset", undefined, "vh-group");
box.dataset.tab = ({ vhEditor: "details", vhFrontends: "details", vhBackendPool: "backends", vhBackendDiagnostics: "backends", vhProxyTimeouts: "backends", vhBalancing: "backends", vhSticky: "backends", vhHealth: "health", vhPassiveHealth: "health", vhSecurity: "security", vhTrafficPolicy: "security", vhTinyauth: "auth", vhResponsePolicy: "security", vhHeaders: "security", vhAdvanced: "advanced", vhLocations: "locations", vhSsl: "ssl", vhCache: "cache", vhTransform: "transform", vhPages: "pages" })[key] || "advanced"; box.append(text(advanced ? "summary" : "legend", key));
box.dataset.tab = ({ vhEditor: "details", vhFrontends: "details", vhBackendPool: "backends", vhBackendDiagnostics: "backends", vhProxyTimeouts: "backends", vhBalancing: "backends", vhSticky: "backends", vhHealth: "health", vhPassiveHealth: "health", vhSecurity: "security", vhTrafficPolicy: "security", vhTinyauth: "auth", vhResponsePolicy: "security", vhHeaders: "security", vhAdvanced: "advanced", vhLocations: "locations", vhSsl: "ssl", vhCache: "cache", vhCompression: "compression", vhTransform: "transform", vhPages: "pages" })[key] || "advanced"; box.append(text(advanced ? "summary" : "legend", key));
const body = el("div", undefined, "vh-grid"); box.append(body); container.append(box); return body;
};
const note = (parent, key) => { const node = text("p", key, "fine vh-wide"); parent.append(node); return node; };
@@ -47,12 +47,13 @@
frontend: { hosts: [], listener_ids: [] },
source: { type: "reverse_proxy" },
backends: [{ id: "origin-1", url: "", weight: 1, enabled: true, pool_eligible: true, allow_private: true, accept_local_listener_risk: false, preserve_host: true, protocol: "http1", tls_verify: false }],
cache: { enabled: false }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
cache: { enabled: false }, compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
});
function activateTab(name) { document.querySelectorAll("#vhost-tabs button").forEach(b => { const active = b.dataset.tab === name; b.classList.toggle("active", active); b.setAttribute("aria-pressed", String(active)); b.setAttribute("aria-selected", String(active)); b.tabIndex = active ? 0 : -1; }); container.querySelectorAll(".vh-group").forEach(g => g.hidden = g.dataset.tab !== name); }
function initTabs() { const tabs = $("vhost-tabs"); tabs.replaceChildren(); tabs.setAttribute("role", "tablist"); for (const [name, key] of [["details", "vhDetailsTab"], ["backends", "vhBackendPool"], ["health", "vhHealth"], ["locations", "vhLocations"], ["auth", "vhTabAuth"], ["ssl", "vhSsl"], ["cache", "vhCache"], ["transform", "vhTransform"], ["security", "vhSecurity"], ["pages", "vhPages"], ["advanced", "advanced"]]) { const b = button(key, () => activateTab(name)); b.dataset.tab = name; b.setAttribute("role", "tab"); tabs.append(b); } activateTab("details"); }
function initTabs() { const tabs = $("vhost-tabs"); tabs.replaceChildren(); tabs.setAttribute("role", "tablist"); for (const [name, key] of [["details", "vhDetailsTab"], ["backends", "vhBackendPool"], ["health", "vhHealth"], ["locations", "vhLocations"], ["auth", "vhTabAuth"], ["ssl", "vhSsl"], ["cache", "vhCache"], ["compression", "vhCompression"], ["transform", "vhTransform"], ["security", "vhSecurity"], ["pages", "vhPages"], ["advanced", "advanced"]]) { const b = button(key, () => activateTab(name)); b.dataset.tab = name; b.setAttribute("role", "tab"); tabs.append(b); } activateTab("details"); }
function collectCache() { const tri = name => str(name) === "" ? null : str(name) === "on", number = (name, mul = 1) => { if (str(name) === "") return null; const value = Number(str(name)) * mul; if (!Number.isSafeInteger(value)) throw new Error(t("vhJsonInvalid")); return value; }; return { enabled: tri("vh-cache-enabled"), max_ttl_seconds: number("vh-cache-ttl"), default_ttl_seconds: number("vh-cache-default"), max_object_bytes: number("vh-cache-object", 1024), bypass_query: tri("vh-cache-query"), static_ignore_cookies: tri("vh-cache-static-cookie"), static_extensions: yes("vh-cache-static-custom") ? lines(str("vh-cache-static-extensions")).map(v => v.replace(/^\./, "").toLowerCase()) : null, expose_status: tri("vh-cache-status"), expose_hits: tri("vh-cache-hits"), expose_cache_status: tri("vh-cache-rfc"), expose_resident_age: tri("vh-cache-resident-age"), bypass_prefixes: yes("vh-cache-prefix-custom") ? lines(str("vh-cache-prefixes")) : null }; }
function collectCompression() { const tri = name => str(name) === "" ? null : str(name) === "on"; return { enabled: tri("vh-compression-enabled"), zstd: tri("vh-compression-zstd"), br: tri("vh-compression-br"), gzip: tri("vh-compression-gzip"), deflate: tri("vh-compression-deflate") }; }
function collectTransform() {
const tri = name => str(name) === "" ? null : str(name) === "on";
return {
+19 -2
View File
@@ -229,11 +229,28 @@
authMode.addEventListener("change", syncAuth); syncAuth();
g = group("vhResponsePolicy"); const server = value.server_header, diagnostics = value.diagnostic_headers;
const serverMode = field(g, "server-mode", "serverHeader", server?.mode || "inherit", "select", { values: [["inherit", "vhInherit"], ["preserve", "serverPreserve"], ["hide", "serverHide"], ["custom", "serverCustom"]] });
const serverValue = field(g, "server-value", "serverValue", server?.value || "RustProxy/v0.25.0", "text", { maxLength: 256 });
const serverValue = field(g, "server-value", "serverValue", server?.value || "RustProxy", "text", { maxLength: 256 });
const syncServer = () => { serverValue.disabled = serverMode.value !== "custom"; serverValue.required = !serverValue.disabled; }; serverMode.addEventListener("change", syncServer); syncServer();
const inheritDiagnostics = field(g, "diag-inherit", "diagInherit", !diagnostics, "checkbox"), diagInputs = [];
for (const [name, key] of [["request_id", "diagRequestId"]]) diagInputs.push(field(g, `diag-${name}`, key, diagnostics?.[name] ?? envelope?.config.diagnostic_headers?.[name], "checkbox"));
const syncDiag = () => { for (const input of diagInputs) input.disabled = inheritDiagnostics.checked; }; inheritDiagnostics.addEventListener("change", syncDiag); syncDiag(); note(g, "diagHelp");
g = group("vhCompression"); const compression = value.compression || {};
const compressionTri = (parent, name, key, current) => field(parent, name, key, current === true ? "on" : current === false ? "off" : "", "select", { values: [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]] });
const compressionToggle = compressionTri(g, "vh-compression-enabled", "compressionEnabled", compression.enabled);
const compressionOptions = el("div", undefined, "vh-wide vh-grid vh-compression-options"); g.append(compressionOptions);
const compressionCodecInputs = [
compressionTri(compressionOptions, "vh-compression-zstd", "compressionZstd", compression.zstd),
compressionTri(compressionOptions, "vh-compression-br", "compressionBr", compression.br),
compressionTri(compressionOptions, "vh-compression-gzip", "compressionGzip", compression.gzip),
compressionTri(compressionOptions, "vh-compression-deflate", "compressionDeflate", compression.deflate)
];
const syncCompressionVisibility = () => {
const disabled = compressionToggle.value === "off";
compressionOptions.hidden = disabled;
for (const input of compressionCodecInputs) input.disabled = disabled;
};
compressionToggle.addEventListener("change", syncCompressionVisibility); syncCompressionVisibility();
note(g, "vhCompressionHelp");
g = group("vhCache"); const cache = value.cache || {};
const tri = (parent, name, key, current) => field(parent, name, key, current === true ? "on" : current === false ? "off" : "", "select", { values: [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]] });
const cacheToggle = tri(g, "vh-cache-enabled", "cacheEnabled", cache.enabled);
@@ -315,7 +332,7 @@
if (JSON.stringify(selected) !== JSON.stringify(globalPolicy)) throw new Error(t("vhTlsHttp3Conflict"));
}
return {
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, force_ssl: yes("force-ssl"), forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"),
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, force_ssl: yes("force-ssl"), forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"),
proxy_timeouts: { connect_timeout_seconds: num("proxy-connect-timeout"), response_header_timeout_seconds: num("proxy-header-timeout"), read_timeout_seconds: num("proxy-read-timeout"), send_timeout_seconds: num("proxy-send-timeout") },
rate_limit: { enabled: yes("rate-enabled"), requests: num("rate-requests"), window_seconds: num("rate-window"), burst: num("rate-burst"), country_access: { mode: str("country-mode"), countries: lines(str("country-list")).map(value => value.toUpperCase()) }, country_limits: countryRatesReader() },
method_policy: { mode: str("method-mode"), methods: lines(str("method-list")).map(value => value.toUpperCase()) },
+20 -1
View File
@@ -161,6 +161,12 @@
]) $(id).disabled = !enabled;
}
function syncCompression() {
const disabled = $("web-service-compression").value === "off";
$("web-service-compression-options").hidden = disabled;
for (const name of ["zstd", "br", "gzip", "deflate"]) $( `web-service-compression-${name}` ).disabled = disabled;
}
function syncPrecompressed() {
const enabled = $("web-service-precompressed").checked;
$("web-service-precompressed-br").disabled = !enabled;
@@ -186,7 +192,7 @@
return {
id: "", name: "", document_root: "", index: ["index.html", "index.htm"], directory_listing: false,
mime_types: {}, default_mime_type: "application/octet-stream", cache_control: null, cache_rules: [], response_headers: {},
try_files: [], rewrites: [], precompressed: { enabled: false, brotli: true, gzip: true }, download_extensions: [],
try_files: [], rewrites: [], compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, precompressed: { enabled: false, brotli: true, gzip: true }, download_extensions: [],
symlink_policy: "deny", php: {
enabled: false, extensions: ["php"], endpoint: "unix:/run/php/php-fpm.sock", connect_timeout_ms: 1000,
request_timeout_ms: 30000, max_concurrent_requests: 64, pass_authorization: true, max_request_body_bytes: 0,
@@ -228,6 +234,8 @@
$("web-service-cache-control").value = item.cache_control || "";
$("web-service-cache-rules").value = cacheLines(item.cache_rules);
$("web-service-response-headers").value = mapLines(item.response_headers);
$("web-service-compression").value = item.compression?.enabled === true ? "on" : item.compression?.enabled === false ? "off" : "";
for (const name of ["zstd", "br", "gzip", "deflate"]) $( `web-service-compression-${name}` ).value = item.compression?.[name] === true ? "on" : item.compression?.[name] === false ? "off" : "";
$("web-service-precompressed").checked = item.precompressed?.enabled === true;
$("web-service-precompressed-br").checked = item.precompressed?.brotli !== false;
$("web-service-precompressed-gzip").checked = item.precompressed?.gzip !== false;
@@ -252,6 +260,7 @@
$("web-service-hardening-attacks").checked = item.hardening?.block_common_attacks === true;
$("web-service-hardening-headers").checked = item.hardening?.strip_unnecessary_headers === true;
syncPhp();
syncCompression();
syncPrecompressed();
syncHtaccess();
}
@@ -297,6 +306,7 @@
function open(value = null) {
const item = Object.assign(defaults(), structuredClone(value || {}));
item.compression = Object.assign(defaults().compression, item.compression || {});
item.precompressed = Object.assign(defaults().precompressed, item.precompressed || {});
item.php = Object.assign(defaults().php, item.php || {});
item.htaccess = Object.assign(defaults().htaccess, item.htaccess || {});
@@ -357,6 +367,7 @@
["webServiceTryFiles", (service.try_files || []).length ? String(service.try_files.length) : "-"],
["webServiceHtaccessEnabled", t(service.htaccess?.enabled ? "enabled" : "notEnabled")],
["webServicePhpEnabled", t(service.php?.enabled ? "enabled" : "notEnabled")],
["webServiceCompression", service.compression?.enabled === true ? t("enabled") : service.compression?.enabled === false ? t("notEnabled") : t("inherit")],
["webServicePrecompressed", t(service.precompressed?.enabled ? "enabled" : "notEnabled")],
["webServiceHardeningTitle", t((service.hardening?.block_common_attacks || service.hardening?.strip_unnecessary_headers) ? "enabled" : "notEnabled")],
["webServiceCacheControl", service.cache_control || t("inherit")],
@@ -390,6 +401,13 @@
response_headers: parseKeyValueLines($("web-service-response-headers").value, "webServiceResponseHeadersInvalid", key => key.toLowerCase()),
try_files: strictLines($("web-service-try-files").value),
rewrites: parseRewrites($("web-service-rewrites").value),
compression: {
enabled: $("web-service-compression").value === "" ? null : $("web-service-compression").value === "on",
zstd: $("web-service-compression-zstd").value === "" ? null : $("web-service-compression-zstd").value === "on",
br: $("web-service-compression-br").value === "" ? null : $("web-service-compression-br").value === "on",
gzip: $("web-service-compression-gzip").value === "" ? null : $("web-service-compression-gzip").value === "on",
deflate: $("web-service-compression-deflate").value === "" ? null : $("web-service-compression-deflate").value === "on"
},
precompressed: { enabled: $("web-service-precompressed").checked, brotli: $("web-service-precompressed-br").checked, gzip: $("web-service-precompressed-gzip").checked },
download_extensions: lines($("web-service-download-extensions").value).map(value => value.replace(/^\./, "").toLowerCase()),
symlink_policy: $("web-service-symlink-policy").value,
@@ -435,6 +453,7 @@
form.addEventListener("input", () => { dirty = true; });
form.addEventListener("change", () => { dirty = true; });
$("web-service-php-enabled").addEventListener("change", syncPhp);
$("web-service-compression").addEventListener("change", syncCompression);
$("web-service-precompressed").addEventListener("change", syncPrecompressed);
$("web-service-htaccess-enabled").addEventListener("change", syncHtaccess);
$("web-service-preset").addEventListener("change", presetChanged);