This commit is contained in:
Mateusz Gruszczyński committed 2026-10-21 09:38:21 +02:00
1 parent 3776edf4fc
commit 7cfc3a6da1
35 files changed
+1377 -875

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.34.4",
"version": "0.34.5",
"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. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
},
"servers": [
+4 -2
View File
@@ -342,6 +342,8 @@
mismatchLabel.title = t(certMismatch.disabled ? "dockerCertMismatchUnavailable" : "dockerCertMismatchWarning");
const certificateBar = el("div", undefined, "docker-service-certificate-bar");
certificateBar.append(mismatchLabel);
const backendIpSettings = el("div", undefined, "docker-backend-ip-settings");
backendIpSettings.append(labeled("dockerBackendIp", target), certificateBar);
const h2Enabled = input("", "checkbox", "docker-http2-enabled"); h2Enabled.checked = assignment?.http2_enabled !== false;
// New publications default to H3 off; legacy assignments keep their saved behavior.
@@ -353,8 +355,8 @@
h1.append(el("strong", "HTTP/1.1"), el("small", t("dockerHttp1Always"), "fine"));
protocolOptions.append(h1, checkboxField("dockerHttp2Enabled", h2Enabled), checkboxField("dockerHttp3Enabled", h3Enabled));
protocolBox.append(protocolSummary, protocolOptions);
route.append(labeled("dockerHostname", host, t("dockerHostnameHelp").replace("{domain}", agent.base_domain)), aliases.node, labeled("dockerBackendIp", target), labeled("dockerBackendScheme", scheme), switches);
editor.append(certificateBar, route, protocolBox, aclEditor(assignment, agent), timeoutEditor(assignment), healthEditor(assignment), passiveHealthEditor(assignment));
route.append(labeled("dockerHostname", host, t("dockerHostnameHelp").replace("{domain}", agent.base_domain)), aliases.node, backendIpSettings, labeled("dockerBackendScheme", scheme), switches);
editor.append(route, protocolBox, 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} · H1${h2Enabled.checked ? "/H2" : ""}${h3Enabled.checked ? "/H3" : ""}${aliasCount ? ` · +${aliasCount} ${t("dockerAliasesShort")}` : ""}`; } updateHint();
for (const control of [host, aliases.input, target, scheme]) { control.addEventListener("input", updateHint); control.addEventListener("change", updateHint); }
+41 -2
View File
@@ -89,7 +89,7 @@ main {
.swagger-ui input,
.swagger-ui textarea,
.swagger-ui .dialog-ux .modal-ux {
border-radius: 0
border-radius: var(--rp-docs-radius-surface, 0)
}
.swagger-ui .opblock .opblock-summary {
@@ -343,4 +343,43 @@ main {
.markdown-reader {
padding: 18px
}
}
}
/* Authenticated documentation uses the same stored corner preference as the
main panel, while retaining its original flat shape for "Original". */
html[data-corner-style=square] {
--rp-docs-radius-surface: 0px;
--rp-docs-radius-control: 0px;
}
html[data-corner-style=subtle] {
--rp-docs-radius-surface: 5px;
--rp-docs-radius-control: 4px;
}
html[data-corner-style=rounded] {
--rp-docs-radius-surface: 12px;
--rp-docs-radius-control: 8px;
}
html[data-corner-style=extra] {
--rp-docs-radius-surface: 18px;
--rp-docs-radius-control: 12px;
}
.docs-session,
.docs-api-controls,
.reader-sidebar,
.markdown-reader,
.docs-confirm,
#docs-error {
border-radius: var(--rp-docs-radius-surface, 0);
}
.docs-bar button,
.docs-confirm-actions button,
.docs-tabs button,
.reader-sidebar input,
.reader-list button,
.swagger-ui .btn,
.swagger-ui select,
.swagger-ui input,
.swagger-ui textarea {
border-radius: var(--rp-docs-radius-control, 0);
}
+5
View File
@@ -1,6 +1,11 @@
"use strict";
(() => {
const element = id => document.getElementById(id);
const corners = new Set(["default", "square", "subtle", "rounded", "extra"]);
try {
const savedCorner = localStorage.getItem("rp-corner-style");
document.documentElement.dataset.cornerStyle = corners.has(savedCorner) ? savedCorner : "default";
} catch { document.documentElement.dataset.cornerStyle = "default"; }
let session = null, spec = null, ui = null, translations = {}, fallback = {}, epoch = 0;
let documents = [], docsByPath = new Map(), currentDocument = null, activeTab = "reader";
let channel = null; try { channel = new BroadcastChannel("rustproxy-admin-session"); } catch { }
+264 -197
View File
@@ -1922,7 +1922,7 @@
</div>
<p class="fine" data-i18n="refreshHelp">Per-account refresh intervals. Off pauses
all automatic sources; manual refresh remains available.</p>
<div class="appearance-settings-grid">
<div class="appearance-settings-grid refresh-settings-grid">
<label class="check"><input type="checkbox" checked id="refresh-live-enabled"
data-refresh-preference="live_enabled" />
<span data-i18n="refreshLiveEnabled">Automatic refresh
@@ -2920,19 +2920,6 @@
id="runtime-system-memory">-</code></div>
<div><span class="small-label" data-i18n="systemUptime">System uptime</span><code
id="runtime-system-uptime">-</code></div>
</div>
<h3 data-i18n="systemProcessHeading">RustProxy process</h3>
<div class="runtime-information-grid">
<div><span class="small-label" data-i18n="systemAppVersion">RustProxy
version</span><code id="runtime-version">-</code></div>
<div><span class="small-label" data-i18n="systemProcessUptime">Process
uptime</span><code id="runtime-process-uptime">-</code></div>
<div><span class="small-label" data-i18n="systemProcessMemory">Process RAM
(RSS)</span><code id="runtime-process-memory">-</code></div>
<div><span class="small-label" data-i18n="systemProcessThreads">Process
threads</span><code id="runtime-process-threads">-</code></div>
<div><span class="small-label" data-i18n="systemOpenFds">Open file
descriptors</span><code id="runtime-open-fds">-</code></div>
<div><span class="small-label" data-i18n="statisticsSource">Statistics
source</span><code id="runtime-statistics-source">-</code></div>
</div>
@@ -3550,11 +3537,13 @@
<div class="vh-field"><span class="small-label" data-i18n="webServicePresetAction">Preset
action</span><button class="outline" data-i18n="webServicePresetApply"
id="web-service-preset-apply" type="button">Apply preset</button></div>
<label class="check"><input id="web-service-hardening-attacks" type="checkbox" /><span
data-i18n="webServiceHardeningAttacks">Block common attack probes</span></label>
<label class="check"><input id="web-service-hardening-headers" type="checkbox" /><span
data-i18n="webServiceHardeningHeaders">Strip unnecessary fingerprint
headers</span></label>
<div class="vh-wide web-service-preset-toggles">
<label class="check"><input id="web-service-hardening-attacks" type="checkbox" /><span
data-i18n="webServiceHardeningAttacks">Block common attack probes</span></label>
<label class="check"><input id="web-service-hardening-headers" type="checkbox" /><span
data-i18n="webServiceHardeningHeaders">Strip unnecessary fingerprint
headers</span></label>
</div>
<p class="fine vh-wide" data-i18n="webServicePresetHelp" id="web-service-preset-summary">
Presets fill normal Web Service settings and remain fully editable. Document root,
service ID/name and PHP-FPM endpoint are not replaced.</p>
@@ -3629,167 +3618,235 @@
responses are never stored.</p>
</div>
<div class="vh-grid web-service-page" data-web-service-page="delivery" hidden>
<label class="vh-wide"><span data-i18n="webServiceSendfile">Linux sendfile (zero-copy)</span>
<select id="web-service-sendfile">
<option value="off" data-i18n="notEnabled">Disabled</option>
<option value="auto" data-i18n="webServiceSendfileAuto">Auto (eligible plain HTTP/1.1)
</option>
</select><small class="fine" data-i18n="webServiceSendfileHelp">Opt-in for static GET files at
least 64 KiB on Linux HTTP/1.1 without TLS, cache, compression or transform. Other requests
use safe streaming; switching this setting never clears the cache.</small></label>
<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
data-i18n="webServicePrecompressedBr">Brotli (.br)</span></label>
<label class="check"><input id="web-service-precompressed-gzip" type="checkbox" /><span
data-i18n="webServicePrecompressedGzip">Gzip (.gz)</span></label>
<label class="vh-wide"><span data-i18n="webServiceDownloadExtensions">Download
extensions</span><textarea id="web-service-download-extensions" maxlength="4096" rows="5"
spellcheck="false" placeholder="zip&#10;pdf&#10;tar"></textarea><small class="fine"
data-i18n="webServiceDownloadExtensionsHelp">Files with these extensions receive
Content-Disposition: attachment.</small></label>
<p class="fine vh-wide" data-i18n="webServiceRangeHelp">Static and precompressed representations
keep validators, single-range requests, 206/416 and streaming delivery.</p>
<section class="vh-wide vh-setting-section web-service-setting-section">
<h3 data-i18n="webServiceStaticDeliverySection">Static file delivery</h3>
<div class="vh-grid">
<label class="vh-wide"><span data-i18n="webServiceSendfile">Linux sendfile
(zero-copy)</span>
<select id="web-service-sendfile">
<option value="off" data-i18n="notEnabled">Disabled</option>
<option value="auto" data-i18n="webServiceSendfileAuto">Auto (eligible plain
HTTP/1.1)
</option>
</select><small class="fine" data-i18n="webServiceSendfileHelp">Opt-in for static GET
files at
least 64 KiB on Linux HTTP/1.1 without TLS, cache, compression or transform. Other
requests
use safe streaming; switching this setting never clears the cache.</small></label>
<label class="vh-wide"><span data-i18n="webServiceDownloadExtensions">Download
extensions</span><textarea id="web-service-download-extensions" maxlength="4096"
rows="5" spellcheck="false" placeholder="zip&#10;pdf&#10;tar"></textarea><small
class="fine" data-i18n="webServiceDownloadExtensionsHelp">Files with these
extensions receive
Content-Disposition: attachment.</small></label>
<p class="fine vh-wide" data-i18n="webServiceRangeHelp">Static and precompressed
representations
keep validators, single-range requests, 206/416 and streaming delivery.</p>
</div>
</section>
<section class="vh-wide vh-setting-section web-service-setting-section">
<h3 data-i18n="webServiceDynamicCompressionSection">Dynamic compression</h3>
<div class="vh-grid">
<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>
</div>
</section>
<section class="vh-wide vh-setting-section web-service-setting-section">
<h3 data-i18n="webServiceSidecarsSection">Precompressed assets</h3>
<div class="vh-grid">
<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
data-i18n="webServicePrecompressedBr">Brotli (.br)</span></label>
<label class="check"><input id="web-service-precompressed-gzip" type="checkbox" /><span
data-i18n="webServicePrecompressedGzip">Gzip (.gz)</span></label>
</div>
</section>
</div>
<div class="vh-grid web-service-page" data-web-service-page="php" hidden>
<label class="vh-wide"><span data-i18n="phpProfileSelect">PHP-FPM configuration source</span>
<select id="web-service-php-profile">
<option value="" data-i18n="phpProfileLocal">Local configuration</option>
</select>
<small class="fine" data-i18n="phpProfileHelp">Select a shared profile or edit PHP-FPM locally.
Profile changes apply to all attached Web Services.</small>
</label>
<div class="vh-wide"><button type="button" class="outline" id="web-service-php-profiles"
data-i18n="phpProfileManage">Manage PHP-FPM profiles</button></div>
<div class="vh-wide php-switch-row"><label class="check php-switch-label"><input
id="web-service-php-enabled" type="checkbox" /><span
data-i18n="webServicePhpEnabled">Enable PHP-FPM / FastCGI</span></label>
<details class="metric-info php-field-info">
<summary data-i18n-aria="phpHintEnabled" aria-label="About enabling PHP-FPM">i</summary>
<div class="metric-info-popover">
<p class="fine" data-i18n="phpHintEnabled">Enables execution of configured PHP
extensions via the PHP-FPM socket. The pool must already be running; RustProxy does
not start PHP. Other files remain static.</p>
<section class="vh-wide vh-setting-section web-service-setting-section">
<h3 data-i18n="webServicePhpSourceSection">PHP-FPM source and endpoint</h3>
<div class="vh-grid">
<label class="vh-wide"><span data-i18n="phpProfileSelect">PHP-FPM configuration
source</span>
<select id="web-service-php-profile">
<option value="" data-i18n="phpProfileLocal">Local configuration</option>
</select>
<small class="fine" data-i18n="phpProfileHelp">Select a shared profile or edit PHP-FPM
locally.
Profile changes apply to all attached Web Services.</small>
</label>
<div class="vh-wide"><button type="button" class="outline" id="web-service-php-profiles"
data-i18n="phpProfileManage">Manage PHP-FPM profiles</button></div>
<div class="vh-wide php-switch-row"><label class="check php-switch-label"><input
id="web-service-php-enabled" type="checkbox" /><span
data-i18n="webServicePhpEnabled">Enable PHP-FPM / FastCGI</span></label>
<details class="metric-info php-field-info">
<summary data-i18n-aria="phpHintEnabled" aria-label="About enabling PHP-FPM">i
</summary>
<div class="metric-info-popover">
<p class="fine" data-i18n="phpHintEnabled">Enables execution of configured PHP
extensions via the PHP-FPM socket. The pool must already be running;
RustProxy does
not start PHP. Other files remain static.</p>
</div>
</details>
</div>
</details>
</div>
<label class="vh-wide"><span data-i18n="webServicePhpEndpoint">PHP-FPM endpoint</span><input
id="web-service-php-endpoint" maxlength="512"
placeholder="unix:/run/php/php-fpm.sock" /><small class="fine"
data-i18n="webServicePhpEndpointHelp">Unix: unix:/run/php/php-fpm.sock. TCP:
tcp:127.0.0.1:9000 or tcp:[::1]:9000. TCP is intentionally limited to loopback
addresses.</small></label>
<label><span data-i18n="webServicePhpExtensions">PHP extensions</span><input
id="web-service-php-extensions" maxlength="512" placeholder="php, phtml" /><small
class="fine" data-i18n="phpHintExtensions">Comma-separated extensions without dots (e.g.
php, phtml). Only listed types are executed as PHP.</small></label>
<div class="vh-wide php-switch-row"><label class="check php-switch-label"><input
id="web-service-php-pass-authorization" type="checkbox" /><span
data-i18n="webServicePhpPassAuthorization">Forward Authorization header</span></label>
<details class="metric-info php-field-info">
<summary data-i18n-aria="phpHintPassAuthorization"
aria-label="Authorization forwarding information">i</summary>
<div class="metric-info-popover">
<p class="fine" data-i18n="phpHintPassAuthorization">Passes the incoming Authorization
header to PHP as HTTP_AUTHORIZATION. Enable for API/Bearer/Basic authentication;
disable if the application does not need it.</p>
<label class="vh-wide"><span data-i18n="webServicePhpEndpoint">PHP-FPM endpoint</span><input
id="web-service-php-endpoint" maxlength="512"
placeholder="unix:/run/php/php-fpm.sock" /><small class="fine"
data-i18n="webServicePhpEndpointHelp">Unix: unix:/run/php/php-fpm.sock. TCP:
tcp:127.0.0.1:9000 or tcp:[::1]:9000. TCP is intentionally limited to loopback
addresses.</small></label>
<label><span data-i18n="webServicePhpExtensions">PHP extensions</span><input
id="web-service-php-extensions" maxlength="512" placeholder="php, phtml" /><small
class="fine" data-i18n="phpHintExtensions">Comma-separated extensions without dots
(e.g.
php, phtml). Only listed types are executed as PHP.</small></label>
</div>
</section>
<section class="vh-wide vh-setting-section web-service-setting-section">
<h3 data-i18n="webServicePhpRequestSection">Request handling</h3>
<div class="vh-grid">
<div class="vh-wide php-switch-row"><label class="check php-switch-label"><input
id="web-service-php-pass-authorization" type="checkbox" /><span
data-i18n="webServicePhpPassAuthorization">Forward Authorization
header</span></label>
<details class="metric-info php-field-info">
<summary data-i18n-aria="phpHintPassAuthorization"
aria-label="Authorization forwarding information">i</summary>
<div class="metric-info-popover">
<p class="fine" data-i18n="phpHintPassAuthorization">Passes the incoming
Authorization
header to PHP as HTTP_AUTHORIZATION. Enable for API/Bearer/Basic
authentication;
disable if the application does not need it.</p>
</div>
</details>
</div>
</details>
</div>
<div class="vh-wide php-switch-row"><label class="check php-switch-label"><input
id="web-service-php-path-info" type="checkbox" /><span
data-i18n="webServicePhpPathInfo">Enable PHP PATH_INFO splitting</span></label>
<details class="metric-info php-field-info">
<summary data-i18n-aria="phpHintPathInfo" aria-label="PHP PATH_INFO information">i</summary>
<div class="metric-info-popover">
<p class="fine" data-i18n="phpHintPathInfo">Allows /static.php/skins/file.css to run the
existing static.php script with PATH_INFO=/skins/file.css. Needed by Roundcube
static.php routes; usually not required for WordPress. Off by default.</p>
<div class="vh-wide php-switch-row"><label class="check php-switch-label"><input
id="web-service-php-path-info" type="checkbox" /><span
data-i18n="webServicePhpPathInfo">Enable PHP PATH_INFO splitting</span></label>
<details class="metric-info php-field-info">
<summary data-i18n-aria="phpHintPathInfo" aria-label="PHP PATH_INFO information">i
</summary>
<div class="metric-info-popover">
<p class="fine" data-i18n="phpHintPathInfo">Allows /static.php/skins/file.css to
run the
existing static.php script with PATH_INFO=/skins/file.css. Needed by
Roundcube
static.php routes; usually not required for WordPress. Off by default.</p>
</div>
</details>
</div>
</details>
</div>
<label><span data-i18n="webServicePhpConnectTimeout">Connect timeout (ms)</span><input
id="web-service-php-connect-timeout" min="50" max="60000" step="1" type="number" /><small
class="fine" data-i18n="phpHintConnectTimeout">How long to wait for the FPM socket
connection; e.g. 1000 = 1 s (50–60000).</small></label>
<label><span data-i18n="webServicePhpRequestTimeout">Total FastCGI request timeout (ms)</span><input
id="web-service-php-request-timeout" min="100" max="3600000" step="1" type="number" /><small
class="fine" data-i18n="phpHintRequestTimeout">Time for the complete FastCGI exchange; e.g.
30000 = 30 s (100–3600000).</small></label>
<label><span data-i18n="webServicePhpConcurrency">Max concurrent PHP requests</span><input
id="web-service-php-concurrency" min="1" max="4096" step="1" type="number" /><small
class="fine" data-i18n="phpHintConcurrency">Max simultaneous PHP requests for this service;
e.g. 64. Exhaustion returns 503 (1–4096).</small></label>
<label><span data-i18n="webServicePhpBodyLimit">PHP request body limit (bytes, 0 =
unlimited)</span><input id="web-service-php-body-limit" min="0" max="8589934592" step="1"
type="number" /><small class="fine" data-i18n="phpHintBodyLimit">Bytes, not MB: 67108864 =
64 MiB; 0 = no proxy-side limit. PHP post_max_size is separate.</small></label>
<label><span data-i18n="webServicePhpHeaderLimit">Max FastCGI response header bytes</span><input
id="web-service-php-header-limit" min="4096" max="1048576" step="1" type="number" /><small
class="fine" data-i18n="phpHintHeaderLimit">Max PHP response-header size in bytes; default
65536 = 64 KiB (4096–1048576).</small></label>
<label><span data-i18n="webServicePhpStderrLimit">Max PHP-FPM stderr log bytes</span><input
id="web-service-php-stderr-limit" min="0" max="65536" step="1" type="number" /><small
class="fine" data-i18n="phpHintStderrLimit">Max FastCGI STDERR bytes kept per request; e.g.
4096. 0 disables capture.</small></label>
<label class="vh-wide"><span data-i18n="webServicePhpCustomParams">Custom FastCGI
parameters</span><textarea id="web-service-php-custom-params" maxlength="65536" rows="6"
spellcheck="false" placeholder="APP_ENV = production&#10;APP_DEBUG = 0"></textarea><small
class="fine" data-i18n="webServicePhpCustomParamsHelp">One NAME = value entry per line.
Static trusted values only; core CGI, HTTP_* and PHP_* control names are
reserved. Example: APP_ENV = production; no variable interpolation.</small></label>
<label class="vh-wide"><span data-i18n="webServicePhpValues">PHP INI overrides
(PHP_VALUE)</span><textarea id="web-service-php-values" maxlength="65536" rows="6"
spellcheck="false"
placeholder="memory_limit = 256M&#10;upload_max_filesize = 64M&#10;post_max_size = 64M"></textarea><small
class="fine" data-i18n="webServicePhpValuesHelp">One php.ini directive = value per line.
Examples: memory_limit = 256M, upload_max_filesize = 64M, post_max_size = 64M,
max_execution_time = 30. Use PHP_ADMIN_VALUE for operator-only settings.</small></label>
<label class="vh-wide"><span data-i18n="phpProfileAdminValues">PHP administrator INI overrides
(PHP_ADMIN_VALUE)</span><textarea id="web-service-php-admin-values" maxlength="65536"
rows="6" spellcheck="false"
placeholder="open_basedir = /var/www/webmail/:/tmp/&#10;display_errors = Off"></textarea><small
class="fine" data-i18n="phpHintAdminValues">Admin-only directives, e.g. open_basedir =
/var/www/webmail/:/tmp/. For isolation use a dedicated PHP-FPM pool/socket; dynamic INI
overrides can persist across requests sharing a worker.</small></label>
<div class="vh-wide form-actions"><button class="outline" data-i18n="webServicePhpDocsButton"
id="web-service-php-help" type="button">FastCGI syntax and safety</button></div>
<p class="fine vh-wide" data-i18n="webServicePhpHelp">RustProxy speaks FastCGI directly to a
long-running PHP-FPM pool. PHP is not spawned per request and PHP source is never served when
PHP handling is selected.</p>
</div>
</section>
<section class="vh-wide vh-setting-section web-service-setting-section">
<h3 data-i18n="webServicePhpLimitsSection">Timeouts and capacity</h3>
<div class="vh-grid">
<label><span data-i18n="webServicePhpConnectTimeout">Connect timeout (ms)</span><input
id="web-service-php-connect-timeout" min="50" max="60000" step="1"
type="number" /><small class="fine" data-i18n="phpHintConnectTimeout">How long to
wait for the FPM socket
connection; e.g. 1000 = 1 s (50–60000).</small></label>
<label><span data-i18n="webServicePhpRequestTimeout">Total FastCGI request timeout
(ms)</span><input id="web-service-php-request-timeout" min="100" max="3600000"
step="1" type="number" /><small class="fine" data-i18n="phpHintRequestTimeout">Time
for the complete FastCGI exchange; e.g.
30000 = 30 s (100–3600000).</small></label>
<label><span data-i18n="webServicePhpConcurrency">Max concurrent PHP requests</span><input
id="web-service-php-concurrency" min="1" max="4096" step="1" type="number" /><small
class="fine" data-i18n="phpHintConcurrency">Max simultaneous PHP requests for this
service;
e.g. 64. Exhaustion returns 503 (1–4096).</small></label>
<label><span data-i18n="webServicePhpBodyLimit">PHP request body limit (bytes, 0 =
unlimited)</span><input id="web-service-php-body-limit" min="0" max="8589934592"
step="1" type="number" /><small class="fine" data-i18n="phpHintBodyLimit">Bytes, not
MB: 67108864 =
64 MiB; 0 = no proxy-side limit. PHP post_max_size is separate.</small></label>
<label><span data-i18n="webServicePhpHeaderLimit">Max FastCGI response header
bytes</span><input id="web-service-php-header-limit" min="4096" max="1048576"
step="1" type="number" /><small class="fine" data-i18n="phpHintHeaderLimit">Max PHP
response-header size in bytes; default
65536 = 64 KiB (4096–1048576).</small></label>
<label><span data-i18n="webServicePhpStderrLimit">Max PHP-FPM stderr log bytes</span><input
id="web-service-php-stderr-limit" min="0" max="65536" step="1"
type="number" /><small class="fine" data-i18n="phpHintStderrLimit">Max FastCGI
STDERR bytes kept per request; e.g.
4096. 0 disables capture.</small></label>
</div>
</section>
<section class="vh-wide vh-setting-section web-service-setting-section">
<h3 data-i18n="webServicePhpOverridesSection">FastCGI and php.ini overrides</h3>
<div class="vh-grid">
<label class="vh-wide"><span data-i18n="webServicePhpCustomParams">Custom FastCGI
parameters</span><textarea id="web-service-php-custom-params" maxlength="65536"
rows="6" spellcheck="false"
placeholder="APP_ENV = production&#10;APP_DEBUG = 0"></textarea><small class="fine"
data-i18n="webServicePhpCustomParamsHelp">One NAME = value entry per line.
Static trusted values only; core CGI, HTTP_* and PHP_* control names are
reserved. Example: APP_ENV = production; no variable interpolation.</small></label>
<label class="vh-wide"><span data-i18n="webServicePhpValues">PHP INI overrides
(PHP_VALUE)</span><textarea id="web-service-php-values" maxlength="65536" rows="6"
spellcheck="false"
placeholder="memory_limit = 256M&#10;upload_max_filesize = 64M&#10;post_max_size = 64M"></textarea><small
class="fine" data-i18n="webServicePhpValuesHelp">One php.ini directive = value per
line.
Examples: memory_limit = 256M, upload_max_filesize = 64M, post_max_size = 64M,
max_execution_time = 30. Use PHP_ADMIN_VALUE for operator-only
settings.</small></label>
<label class="vh-wide"><span data-i18n="phpProfileAdminValues">PHP administrator INI
overrides
(PHP_ADMIN_VALUE)</span><textarea id="web-service-php-admin-values"
maxlength="65536" rows="6" spellcheck="false"
placeholder="open_basedir = /var/www/webmail/:/tmp/&#10;display_errors = Off"></textarea><small
class="fine" data-i18n="phpHintAdminValues">Admin-only directives, e.g. open_basedir
=
/var/www/webmail/:/tmp/. For isolation use a dedicated PHP-FPM pool/socket; dynamic
INI
overrides can persist across requests sharing a worker.</small></label>
<div class="vh-wide form-actions"><button class="outline"
data-i18n="webServicePhpDocsButton" id="web-service-php-help" type="button">FastCGI
syntax and safety</button></div>
<p class="fine vh-wide" data-i18n="webServicePhpHelp">RustProxy speaks FastCGI directly to a
long-running PHP-FPM pool. PHP is not spawned per request and PHP source is never served
when
PHP handling is selected.</p>
</div>
</section>
</div>
<div class="vh-grid web-service-page" data-web-service-page="security" hidden>
<label><span data-i18n="webServiceSymlinkPolicy">Symlink policy</span><select
@@ -3978,23 +4035,28 @@ log_errors = On</code></pre>
id="redirect-editing">NEW</span><button class="quiet" data-i18n="close" id="redirect-close"
type="button">Close</button>
</div>
<div class="vh-grid">
<label><span data-i18n="redirectId">Redirect ID</span><input id="redirect-id" maxlength="64"
pattern="[A-Za-z0-9_-]{1,64}" required="" /></label>
<label><span data-i18n="redirectName">Display name</span><input id="redirect-name"
maxlength="96" /></label>
<label class="check"><input id="redirect-enabled" type="checkbox" /><span
data-i18n="enabled">Enabled</span></label>
<div class="vh-wide" id="redirect-hosts-editor"></div>
<label><span data-i18n="vhCertificate">Certificate</span><select
id="redirect-certificate"></select></label>
</div>
<fieldset class="vh-group editor-identity-section">
<legend data-i18n="hostIdentitySection">Host and domains</legend>
<div class="vh-grid">
<label><span data-i18n="redirectId">Redirect ID</span><input id="redirect-id" maxlength="64"
pattern="[A-Za-z0-9_-]{1,64}" required="" /></label>
<label><span data-i18n="redirectName">Display name</span><input id="redirect-name"
maxlength="96" /></label>
<label class="check"><input id="redirect-enabled" type="checkbox" /><span
data-i18n="enabled">Enabled</span></label>
<div class="vh-wide" id="redirect-hosts-editor"></div>
</div>
</fieldset>
<section class="vh-group">
<div class="card-heading">
<div><span class="small-label" data-i18n="redirectFrontend">FRONTEND</span>
<h3 data-i18n="redirectListeners">Listeners</h3>
</div>
</div>
<div class="vh-grid editor-frontend-certificate">
<label><span data-i18n="vhCertificate">Certificate</span><select
id="redirect-certificate"></select></label>
</div>
<div class="redirect-listeners" id="redirect-listeners"></div>
<div class="protocol-toggle-row"><label class="check"><input id="redirect-http2" type="checkbox" /><span
data-i18n="vhHttp2Enabled">Enable HTTP/2 for this host</span></label><label
@@ -4027,23 +4089,28 @@ log_errors = On</code></pre>
id="notfound-editing">NEW</span><button class="quiet" data-i18n="close" id="notfound-close"
type="button">Close</button>
</div>
<div class="vh-grid">
<label><span data-i18n="notfoundId">Host ID</span><input id="notfound-id" maxlength="64"
pattern="[A-Za-z0-9_-]{1,64}" required="" /></label>
<label><span data-i18n="notfoundName">Display name</span><input id="notfound-name"
maxlength="96" /></label>
<label class="check"><input id="notfound-enabled" type="checkbox" /><span
data-i18n="enabled">Enabled</span></label>
<div class="vh-wide" id="notfound-hosts-editor"></div>
<label><span data-i18n="vhCertificate">Certificate</span><select
id="notfound-certificate"></select></label>
</div>
<fieldset class="vh-group editor-identity-section">
<legend data-i18n="hostIdentitySection">Host and domains</legend>
<div class="vh-grid">
<label><span data-i18n="notfoundId">Host ID</span><input id="notfound-id" maxlength="64"
pattern="[A-Za-z0-9_-]{1,64}" required="" /></label>
<label><span data-i18n="notfoundName">Display name</span><input id="notfound-name"
maxlength="96" /></label>
<label class="check"><input id="notfound-enabled" type="checkbox" /><span
data-i18n="enabled">Enabled</span></label>
<div class="vh-wide" id="notfound-hosts-editor"></div>
</div>
</fieldset>
<section class="vh-group">
<div class="card-heading">
<div><span class="small-label" data-i18n="notfoundFrontend">FRONTEND</span>
<h3 data-i18n="redirectListeners">Listeners</h3>
</div>
</div>
<div class="vh-grid editor-frontend-certificate">
<label><span data-i18n="vhCertificate">Certificate</span><select
id="notfound-certificate"></select></label>
</div>
<div id="notfound-listeners"></div>
<div class="protocol-toggle-row"><label class="check"><input id="notfound-http2" type="checkbox" /><span
data-i18n="vhHttp2Enabled">Enable HTTP/2 for this host</span></label><label
-9
View File
@@ -3,8 +3,6 @@
const buttons = [...document.querySelectorAll("[data-settings-tab]")];
const search = $("settings-search"), searchStatus = $("settings-search-status");
const legacyTab = (typeof location !== "undefined" && location.pathname === "/configuration") ? "manual-config" : "appearance";
// Explicit URL tabs win. Otherwise reopen the last chosen Settings tab,
// falling back to Appearance for new browsers and invalid stored values.
let rememberedTab = null;
try { rememberedTab = localStorage.getItem("rp-settings-last-tab"); } catch { }
const defaultTab = legacyTab === "manual-config" ? legacyTab : (rememberedTab || "appearance");
@@ -72,8 +70,6 @@
const target = $("api-tokens-card");
if (target) { target.scrollIntoView({ block: "start" }); target.focus({ preventScroll: true }); }
}
// Preserve a real deep link (including opening in a new tab), while switching
// tabs without reloading the panel for ordinary left-click navigation.
$("api-tokens-security-link")?.addEventListener("click", event => {
if (event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
event.preventDefault();
@@ -222,7 +218,6 @@
const ram = hasMetric(system.memory_used_bytes) || hasMetric(system.memory_total_bytes)
? `${humanBytes(system.memory_used_bytes)} / ${humanBytes(system.memory_total_bytes)}` : t("unavailable");
for (const [id, value] of [
["runtime-version", latestStatus?.version],
["runtime-platform", runtime.platform],
["runtime-operating-system", runtime.operating_system],
["runtime-kernel-version", runtime.kernel_version],
@@ -231,10 +226,6 @@
["runtime-cpu-usage", percent(system.cpu_percent)],
["runtime-system-memory", ram],
["runtime-system-uptime", humanUptime(system.system_uptime_seconds)],
["runtime-process-uptime", humanUptime(latestStatus?.uptime_seconds)],
["runtime-process-memory", humanBytes(system.process_rss_bytes)],
["runtime-process-threads", humanCount(system.process_threads)],
["runtime-open-fds", humanCount(system.open_fds)],
["runtime-statistics-source", runtime.statistics_source],
["runtime-data-directory", runtime.data_directory],
["runtime-database-path", runtime.database_path]
+32 -15
View File
@@ -61,25 +61,42 @@
if (backendList?.children.length >= 32) { toast(t("streamBackendLimit"), true); return; }
const row = el("fieldset", undefined, "stream-backend-row vh-wide"); row.append(el("legend", backend.id || t("streamBackend")));
const controls = el("div", undefined, "vh-grid"); row.append(controls);
field(controls, "backend-id", "streamBackendId", backend.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" });
field(controls, "backend-name", "streamBackendName", backend.name || "", "text", { maxLength: 96 });
field(controls, "backend-address", "streamBackendAddress", backend.address, "text", { required: true, maxLength: 128 });
field(controls, "backend-weight", "streamBackendWeight", backend.weight ?? 1, "number", { min: 1, max: 16, required: true });
field(controls, "backend-max", "streamBackendMaxSessions", backend.max_sessions ?? "", "number", { min: 1, max: 65535 });
field(controls, "backend-enabled", "vhEnabled", backend.enabled !== false, "checkbox");
field(controls, "backend-drain", "streamBackendDrain", backend.drain === true, "checkbox");
const backendSection = key => {
const section = el("section", undefined, "vh-wide vh-setting-section stream-backend-section");
section.append(text("h3", key));
const grid = el("div", undefined, "vh-grid"); section.append(grid); controls.append(section);
return grid;
};
const connectionFields = backendSection("streamBackendConnectionSection");
field(connectionFields, "backend-id", "streamBackendId", backend.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" });
field(connectionFields, "backend-name", "streamBackendName", backend.name || "", "text", { maxLength: 96 });
field(connectionFields, "backend-address", "streamBackendAddress", backend.address, "text", { required: true, maxLength: 128 });
field(connectionFields, "backend-weight", "streamBackendWeight", backend.weight ?? 1, "number", { min: 1, max: 16, required: true });
field(connectionFields, "backend-max", "streamBackendMaxSessions", backend.max_sessions ?? "", "number", { min: 1, max: 65535 });
field(connectionFields, "backend-enabled", "vhEnabled", backend.enabled !== false, "checkbox");
field(connectionFields, "backend-drain", "streamBackendDrain", backend.drain === true, "checkbox");
const detail = backend.metrics_enabled == null ? "inherit" : String(backend.metrics_enabled);
field(controls, "backend-metrics", "streamBackendDetailMetrics", detail, "select", { values: [["inherit", "streamMetricsFromStream"], ["true", "streamMetricsOn"], ["false", "streamMetricsOff"]] });
field(connectionFields, "backend-metrics", "streamBackendDetailMetrics", detail, "select", { values: [["inherit", "streamMetricsFromStream"], ["true", "streamMetricsOn"], ["false", "streamMetricsOff"]] });
const hc = backend.health || backendDefaults().health;
field(controls, "backend-health-mode", "streamHealthMode", hc.mode || "off", "select", { values: [["off", "streamHealthOff"], [protocol === "tcp" ? "tcp_connect" : "udp_probe", protocol === "tcp" ? "streamHealthTcp" : "streamHealthUdp"]] });
field(controls, "backend-health-interval", "streamHealthInterval", hc.interval_seconds ?? 10, "number", { min: 1, max: 3600, required: true });
field(controls, "backend-health-timeout", "streamHealthTimeout", hc.timeout_ms ?? 1000, "number", { min: 50, max: 30000, required: true });
field(controls, "backend-health-rise", "streamHealthRise", hc.rise ?? 2, "number", { min: 1, max: 10, required: true });
field(controls, "backend-health-fall", "streamHealthFall", hc.fall ?? 2, "number", { min: 1, max: 10, required: true });
const healthFields = backendSection("streamBackendHealthSection");
const healthMode = field(healthFields, "backend-health-mode", "streamHealthMode", hc.mode || "off", "select", { values: [["off", "streamHealthOff"], [protocol === "tcp" ? "tcp_connect" : "udp_probe", protocol === "tcp" ? "streamHealthTcp" : "streamHealthUdp"]] });
const probeFields = el("div", undefined, "vh-wide vh-grid stream-health-probe-fields");
healthFields.append(probeFields);
const syncProbeFields = () => {
const inactive = healthMode.value === "off";
probeFields.hidden = inactive;
probeFields.querySelectorAll("input, select, textarea").forEach(input => { input.disabled = inactive; });
};
healthMode.addEventListener("change", syncProbeFields);
field(probeFields, "backend-health-interval", "streamHealthInterval", hc.interval_seconds ?? 10, "number", { min: 1, max: 3600, required: true });
field(probeFields, "backend-health-timeout", "streamHealthTimeout", hc.timeout_ms ?? 1000, "number", { min: 50, max: 30000, required: true });
field(probeFields, "backend-health-rise", "streamHealthRise", hc.rise ?? 2, "number", { min: 1, max: 10, required: true });
field(probeFields, "backend-health-fall", "streamHealthFall", hc.fall ?? 2, "number", { min: 1, max: 10, required: true });
if (protocol === "udp") {
field(controls, "backend-health-payload", "streamHealthPayload", hc.payload_hex || "", "text", { maxLength: 256 });
field(controls, "backend-health-expected", "streamHealthExpected", hc.expect_hex || "", "text", { maxLength: 256 });
field(probeFields, "backend-health-payload", "streamHealthPayload", hc.payload_hex || "", "text", { maxLength: 256 });
field(probeFields, "backend-health-expected", "streamHealthExpected", hc.expect_hex || "", "text", { maxLength: 256 });
}
syncProbeFields();
const remove = button("remove", () => { if (backendList.children.length > 1) { row.remove(); changed(); } });
row.append(remove); backendList.append(row);
}
+334 -531
View File
File diff suppressed because it is too large. Load diff
+21 -9
View File
@@ -20,10 +20,18 @@ const field = (parent, name, key, value, type = "text", options = {}) => {
};
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", vhCompression: "compression", 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", vhSslSettings: "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; };
// Presentation-only subgroup: all existing inputs remain inside their original form.
const subsection = (parent, key) => {
const box = el("section", undefined, "vh-wide vh-setting-section");
box.append(text("h3", key));
const body = el("div", undefined, "vh-grid");
box.append(body); parent.append(box);
return body;
};
const button = (key, callback, cls = "quiet") => { const b = text("button", key, cls); b.type = "button"; b.addEventListener("click", callback); return b; };
const read = (name, scope = form) => scope.querySelector(`[name="${name}"]`);
const str = (name, scope) => read(name, scope).value.trim();
@@ -403,17 +411,21 @@ function locationsEditor(parent, values) {
note(handlingBody, "locationRewriteHelp");
const extra = el("details", undefined, "vh-wide location-section"), accessBody = el("div", undefined, "vh-grid"); extra.append(text("summary", "locationAccess"), accessBody); body.append(extra);
const locationProfile = field(accessBody, "location-acl-profile", "locationAclProfile", value.acl_profile_id || "", "select");
const locationProfiles = subsection(accessBody, "vhAclProfile");
const locationProfile = field(locationProfiles, "location-acl-profile", "locationAclProfile", value.acl_profile_id || "", "select");
const locationNoProfile = text("option", "vhAclProfileNone"); locationNoProfile.value = "";
locationProfile.append(locationNoProfile, ...(envelope?.config.acl_profiles || []).map(profile => { const option = el("option", profile.name || profile.id); option.value = profile.id; return option; }));
locationProfile.value = value.acl_profile_id || "";
field(accessBody, "location-method-mode", "locationMethodMode", value.method_policy?.mode || "off", "select", { values: [["off", "vhOff"], ["allow", "locationMethodAllow"], ["block", "locationMethodBlock"]] });
field(accessBody, "location-method-list", "locationMethodList", (value.method_policy?.methods || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(accessBody, "locationMethodHelp");
field(accessBody, "location-country-mode", "locationCountryMode", value.country_access?.mode || "off", "select", { values: [["off", "vhOff"], ["block", "vhCountryBlock"], ["allow", "vhCountryAllow"]] });
field(accessBody, "location-country-list", "locationCountryList", (value.country_access?.countries || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(accessBody, "locationCountryHelp");
const acl = aclEditor(accessBody, value.acl, sync); row.readAcl = () => acl.read();
const locationMethods = subsection(accessBody, "vhTrafficMethodsSection");
field(locationMethods, "location-method-mode", "locationMethodMode", value.method_policy?.mode || "off", "select", { values: [["off", "vhOff"], ["allow", "locationMethodAllow"], ["block", "locationMethodBlock"]] });
field(locationMethods, "location-method-list", "locationMethodList", (value.method_policy?.methods || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(locationMethods, "locationMethodHelp");
const locationCountries = subsection(accessBody, "vhTrafficCountrySection");
field(locationCountries, "location-country-mode", "locationCountryMode", value.country_access?.mode || "off", "select", { values: [["off", "vhOff"], ["block", "vhCountryBlock"], ["allow", "vhCountryAllow"]] });
field(locationCountries, "location-country-list", "locationCountryList", (value.country_access?.countries || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(locationCountries, "locationCountryHelp");
const locationAcl = subsection(accessBody, "vhLocationIpAclSection");
const acl = aclEditor(locationAcl, value.acl, sync); row.readAcl = () => acl.read();
const headerBox = el("details", undefined, "vh-wide location-section location-header-editor"), headerBody = el("div", undefined, "vh-grid"); headerBox.append(text("summary", "locationHeaders"), headerBody); body.append(headerBox);
const headerToolbar = el("div", undefined, "vh-wide location-header-toolbar"), visualPane = el("div", undefined, "vh-wide vh-grid"), jsonPane = el("div", undefined, "vh-wide vh-grid"); jsonPane.hidden = true;
const visualButton = button("headersVisualBuilder", () => { visualPane.hidden = false; jsonPane.hidden = true; }), jsonButton = button("headersAdvancedJson", () => { visualPane.hidden = true; jsonPane.hidden = false; }), docsButton = button("documentation", () => openLocationHelp("headers"));
+99 -64
View File
@@ -37,23 +37,25 @@ function load(value = defaults(), migrationIds = [], show = true) {
draftEpoch++; stagedCertificates = []; uploading = false; backendHealthEditorControl = null;
editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true;
container.replaceChildren(); $("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
let g = group("vhEditor"); field(g, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(g, "vhIdRenameHelp");
let g = group("vhEditor"); const basicFields = subsection(g, "vhGeneralDetailsSection");
field(basicFields, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(basicFields, "vhIdRenameHelp");
let refreshCanonical = () => { };
const enabledControl = field(g, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
const enabledControl = field(basicFields, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
window.rustProxyEnableControl(enabledControl, "vhEnableHint");
const enabledLabel = enabledControl.closest("label");
enabledLabel?.querySelector(".vh-enable-hint")?.remove();
enabledLabel?.classList.add("vh-enable-no-hint");
const mainSource = field(g, "source-type", "sourceType", value.source?.type || "reverse_proxy", "select", { values: [["reverse_proxy", "sourceReverseProxy"], ["web_server", "sourceWebServer"]] });
const mainWebService = field(g, "source-web-service", "webService", value.source?.web_service_id || "", "select"); fillWebServices(mainWebService, value.source?.web_service_id || "");
const mainSource = field(basicFields, "source-type", "sourceType", value.source?.type || "reverse_proxy", "select", { values: [["reverse_proxy", "sourceReverseProxy"], ["web_server", "sourceWebServer"]] });
const mainWebService = field(basicFields, "source-web-service", "webService", value.source?.web_service_id || "", "select"); fillWebServices(mainWebService, value.source?.web_service_id || "");
const syncMainSource = () => { const web = mainSource.value === "web_server"; mainWebService.disabled = !web; mainWebService.required = web; }; mainSource.addEventListener("change", syncMainSource); syncMainSource();
note(g, "sourceHelp");
const websocketEnabled = field(g, "websockets", "vhWebsockets", value.websockets !== false, "checkbox");
note(basicFields, "sourceHelp");
const websocketFields = subsection(g, "vhWebsocketOptionsSection");
const websocketEnabled = field(websocketFields, "websockets", "vhWebsockets", value.websockets !== false, "checkbox");
const websocketModeValue = value.websocket_rfc8441 === true && value.websocket_rfc9220 === true ? "h2h3" : value.websocket_rfc8441 === true ? "h2" : value.websocket_rfc9220 === true ? "h3" : "h1";
const websocketMode = field(g, "websocket-mode", "vhWebsocketMode", websocketModeValue, "select", { values: [["h1", "vhWebsocketH1"], ["h2", "vhWebsocketH2Fallback"], ["h3", "vhWebsocketH3Fallback"], ["h2h3", "vhWebsocketH2H3Fallback"]] });
note(g, "vhWebsocketTransportHelp");
const websocketLegacy = field(g, "websocket-legacy-support", "vhWebsocketLegacySupport", value.websocket_legacy_support === true, "checkbox");
note(g, "vhWebsocketLegacySupportHelp");
const websocketMode = field(websocketFields, "websocket-mode", "vhWebsocketMode", websocketModeValue, "select", { values: [["h1", "vhWebsocketH1"], ["h2", "vhWebsocketH2Fallback"], ["h3", "vhWebsocketH3Fallback"], ["h2h3", "vhWebsocketH2H3Fallback"]] });
note(websocketFields, "vhWebsocketTransportHelp");
const websocketLegacy = field(websocketFields, "websocket-legacy-support", "vhWebsocketLegacySupport", value.websocket_legacy_support === true, "checkbox");
note(websocketFields, "vhWebsocketLegacySupportHelp");
g = group("vhFrontends");
readHosts = hostTagEditor(g, value.frontend?.hosts || [], hosts => refreshCanonical(hosts));
const bindingControl = window.rustProxyIntegrations.bindings(g, value.frontend?.listener_ids || [], !editing); readBindings = bindingControl.read;
@@ -182,21 +184,25 @@ function load(value = defaults(), migrationIds = [], show = true) {
field(g, "proxy-send-timeout", "vhProxySendTimeout", proxyTimeouts.send_timeout_seconds ?? 60, "number", { required: true, min: 1, max: 86400 });
note(g, "vhProxyTimeoutsHelp");
const settings = value.load_balancing || {}, health = settings.health_check || {}, keepalive = settings.keepalive || {}, retry = settings.retry || {}, sticky = settings.sticky || {};
g = group("vhBalancing"); field(g, "algorithm", "vhAlgorithm", settings.algorithm || "least_connections", "select", { values: [["round_robin", "vhRoundRobin"], ["least_connections", "vhLeastConnections"], ["least_traffic", "vhLeastTraffic"], ["adaptive", "vhAdaptive"], ["ip_hash", "vhIpHash"]] });
field(g, "slow-start", "vhSlowStart", settings.slow_start_seconds ?? 10, "number", { required: true, min: 0, max: 300 });
field(g, "retry-max", "vhRetryMax", retry.max_retries ?? 1, "number", { required: true, min: 0, max: 3 });
field(g, "retry-connect", "vhRetryConnect", retry.retry_connect_errors !== false, "checkbox");
field(g, "retry-timeouts", "vhRetryTimeouts", retry.retry_timeouts !== false, "checkbox");
field(g, "retry-statuses", "vhRetryStatuses", (retry.retry_statuses || [502, 503, 504]).join(", "), "text", { maxLength: 64 });
note(g, "vhRetryHelp");
field(g, "keepalive", "vhKeepalive", keepalive.enabled !== false, "checkbox");
field(g, "idle-timeout", "vhIdleTimeout", keepalive.idle_timeout_seconds ?? 30, "number", { required: true, min: 1, max: 3600 });
field(g, "max-idle", "vhMaxIdle", keepalive.max_idle_per_host ?? 64, "number", { required: true, min: 1, max: 64 });
field(g, "tcp-keepalive", "vhTcpKeepalive", keepalive.tcp_keepalive_seconds ?? 30, "number", { required: true, min: 1, max: 3600 });
field(g, "pool-recycle-requests", "vhPoolRecycleRequests", keepalive.pool_recycle_requests ?? 25000, "number", { required: true, min: 0, max: 10000000 });
field(g, "pool-recycle-seconds", "vhPoolRecycleSeconds", keepalive.pool_recycle_seconds ?? 3600, "number", { required: true, min: 0, max: 604800 });
field(g, "http1-max-buffer", "vhHttp1MaxBuffer", keepalive.http1_max_buffer_bytes ?? 131072, "number", { required: true, min: 8192, max: 1048576 });
note(g, "vhPoolRecycleHelp");
g = group("vhBalancing");
const schedulingFields = subsection(g, "vhBalancerAlgorithmSection");
field(schedulingFields, "algorithm", "vhAlgorithm", settings.algorithm || "least_connections", "select", { values: [["round_robin", "vhRoundRobin"], ["least_connections", "vhLeastConnections"], ["least_traffic", "vhLeastTraffic"], ["adaptive", "vhAdaptive"], ["ip_hash", "vhIpHash"]] });
field(schedulingFields, "slow-start", "vhSlowStart", settings.slow_start_seconds ?? 10, "number", { required: true, min: 0, max: 300 });
const retryFields = subsection(g, "vhBalancerRetrySection");
field(retryFields, "retry-max", "vhRetryMax", retry.max_retries ?? 1, "number", { required: true, min: 0, max: 3 });
field(retryFields, "retry-connect", "vhRetryConnect", retry.retry_connect_errors !== false, "checkbox");
field(retryFields, "retry-timeouts", "vhRetryTimeouts", retry.retry_timeouts !== false, "checkbox");
field(retryFields, "retry-statuses", "vhRetryStatuses", (retry.retry_statuses || [502, 503, 504]).join(", "), "text", { maxLength: 64 });
note(retryFields, "vhRetryHelp");
const keepaliveFields = subsection(g, "vhBalancerKeepaliveSection");
field(keepaliveFields, "keepalive", "vhKeepalive", keepalive.enabled !== false, "checkbox");
field(keepaliveFields, "idle-timeout", "vhIdleTimeout", keepalive.idle_timeout_seconds ?? 30, "number", { required: true, min: 1, max: 3600 });
field(keepaliveFields, "max-idle", "vhMaxIdle", keepalive.max_idle_per_host ?? 64, "number", { required: true, min: 1, max: 64 });
field(keepaliveFields, "tcp-keepalive", "vhTcpKeepalive", keepalive.tcp_keepalive_seconds ?? 30, "number", { required: true, min: 1, max: 3600 });
field(keepaliveFields, "pool-recycle-requests", "vhPoolRecycleRequests", keepalive.pool_recycle_requests ?? 25000, "number", { required: true, min: 0, max: 10000000 });
field(keepaliveFields, "pool-recycle-seconds", "vhPoolRecycleSeconds", keepalive.pool_recycle_seconds ?? 3600, "number", { required: true, min: 0, max: 604800 });
field(keepaliveFields, "http1-max-buffer", "vhHttp1MaxBuffer", keepalive.http1_max_buffer_bytes ?? 131072, "number", { required: true, min: 8192, max: 1048576 });
note(keepaliveFields, "vhPoolRecycleHelp");
g = group("vhHealth"); const healthMode = field(g, "health-mode", "vhHealthMode", health.mode || "tcp", "select", { values: [["off", "vhOff"], ["tcp", "vhTcp"], ["http", "vhHttp"]] });
const healthOffWarning = note(g, "vhHealthOffWarning"); const syncHealthWarning = () => { healthOffWarning.hidden = healthMode.value !== "off"; }; healthMode.addEventListener("change", syncHealthWarning); syncHealthWarning();
field(g, "health-path", "vhHealthPath", health.path || "/", "text", { required: true, maxLength: 1024 });
@@ -234,36 +240,60 @@ function load(value = defaults(), migrationIds = [], show = true) {
field(g, "cookie-ttl", "vhCookieTtl", sticky.ttl_seconds ?? 3600, "number", { required: true, min: 60, max: 604800 }); note(g, "vhStickyNote");
const rate = value.rate_limit || {}, access = rate.country_access || {}, methodPolicy = value.method_policy || {};
g = group("vhTrafficPolicy");
// Policies share their original form/data model; the boundaries are presentational only.
const aclFields = subsection(g, "vhAclProfile");
const selectedProfiles = new Set([value.acl_profile_id, ...(value.acl_profile_ids || [])].filter(Boolean));
const profileBox = el("div", undefined, "vh-wide vh-acl-profile-picker");
profileBox.append(text("strong", "vhAclProfile"));
const choices = window.rustProxyAclPicker.create([...selectedProfiles], "vh-acl-profile-choices");
profileBox.append(choices);
const multiWarning = text("p", "multiAclWarning", "fine acl-stack-warning");
const syncMultiWarning = () => { multiWarning.hidden = choices.querySelectorAll('input:checked').length < 2; };
choices.addEventListener("change", syncMultiWarning); syncMultiWarning();
profileBox.append(multiWarning); g.append(profileBox); note(g, "vhAclProfileHelp");
field(g, "method-mode", "vhMethodMode", methodPolicy.mode || "off", "select", { values: [["off", "vhOff"], ["allow", "locationMethodAllow"], ["block", "locationMethodBlock"]] });
field(g, "method-list", "vhMethodList", (methodPolicy.methods || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(g, "vhMethodHelp");
field(g, "rate-enabled", "vhRateEnabled", rate.enabled === true, "checkbox");
field(g, "rate-requests", "vhRateRequests", rate.requests ?? 120, "number", { required: true, min: 1, max: 1000000 });
field(g, "rate-window", "vhRateWindow", rate.window_seconds ?? 60, "number", { required: true, min: 1, max: 3600 });
field(g, "rate-burst", "vhRateBurst", rate.burst ?? 30, "number", { required: true, min: 0, max: 1000000 });
field(g, "country-mode", "vhCountryMode", access.mode || "off", "select", { values: [["off", "vhOff"], ["block", "vhCountryBlock"], ["allow", "vhCountryAllow"]] });
field(g, "country-list", "vhCountryList", (access.countries || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(g, "vhCountryPolicyHelp"); countryRatesReader = countryRateEditor(g, rate.country_limits || []); uaRatesReader = userAgentRateEditor(g, rate.user_agent_limits || []); note(g, "vhRateHelp");
profileBox.append(multiWarning); aclFields.append(profileBox); note(aclFields, "vhAclProfileHelp");
const inlineAclFields = subsection(g, "vhIpAccess");
aclControl = aclEditor(inlineAclFields, value.acl, () => { if (aclControl) read("acl").value = JSON.stringify(aclControl.read(), null, 2); });
note(inlineAclFields, "vhInlineAclHelp");
const methodFields = subsection(g, "vhTrafficMethodsSection");
field(methodFields, "method-mode", "vhMethodMode", methodPolicy.mode || "off", "select", { values: [["off", "vhOff"], ["allow", "locationMethodAllow"], ["block", "locationMethodBlock"]] });
field(methodFields, "method-list", "vhMethodList", (methodPolicy.methods || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(methodFields, "vhMethodHelp");
const rateFields = subsection(g, "vhTrafficRateSection");
field(rateFields, "rate-enabled", "vhRateEnabled", rate.enabled === true, "checkbox");
field(rateFields, "rate-requests", "vhRateRequests", rate.requests ?? 120, "number", { required: true, min: 1, max: 1000000 });
field(rateFields, "rate-window", "vhRateWindow", rate.window_seconds ?? 60, "number", { required: true, min: 1, max: 3600 });
field(rateFields, "rate-burst", "vhRateBurst", rate.burst ?? 30, "number", { required: true, min: 0, max: 1000000 });
note(rateFields, "vhRateHelp");
const countryFields = subsection(g, "vhTrafficCountrySection");
field(countryFields, "country-mode", "vhCountryMode", access.mode || "off", "select", { values: [["off", "vhOff"], ["block", "vhCountryBlock"], ["allow", "vhCountryAllow"]] });
field(countryFields, "country-list", "vhCountryList", (access.countries || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(countryFields, "vhCountryPolicyHelp");
const countryRateFields = subsection(g, "vhTrafficCountryRatesSection");
countryRatesReader = countryRateEditor(countryRateFields, rate.country_limits || []);
const uaRateFields = subsection(g, "vhUaRateTitle");
uaRatesReader = userAgentRateEditor(uaRateFields, rate.user_agent_limits || [], false);
const security = value.security_headers || {}, hsts = security.hsts || {};
g = group("vhSecurity"); field(g, "hsts", "vhHsts", hsts.enabled, "checkbox"); field(g, "hsts-age", "vhHstsAge", hsts.max_age_seconds ?? 31536000, "number", { required: true, min: 0, max: 63072000 });
field(g, "hsts-sub", "vhHstsSub", hsts.include_subdomains, "checkbox"); field(g, "hsts-preload", "vhHstsPreload", hsts.preload, "checkbox"); field(g, "nosniff", "vhNosniff", security.nosniff, "checkbox");
field(g, "frame", "vhFrame", security.frame_options || "preserve", "select", { values: [["preserve", "vhPreserve"], ["deny", "vhDeny"], ["sameorigin", "vhSameorigin"]] });
field(g, "referrer", "vhReferrer", security.referrer_policy || "", "text", { maxLength: 8192 });
field(g, "csp", "vhCsp", security.content_security_policy || "", "textarea", { rows: 2, maxLength: 8192 });
field(g, "permissions", "vhPermissions", security.permissions_policy || "", "text", { maxLength: 8192 });
g = group("vhSecurity");
const hstsFields = subsection(g, "vhSecurityHstsSection");
field(hstsFields, "hsts", "vhHsts", hsts.enabled, "checkbox");
field(hstsFields, "hsts-age", "vhHstsAge", hsts.max_age_seconds ?? 31536000, "number", { required: true, min: 0, max: 63072000 });
field(hstsFields, "hsts-sub", "vhHstsSub", hsts.include_subdomains, "checkbox");
field(hstsFields, "hsts-preload", "vhHstsPreload", hsts.preload, "checkbox");
const headersFields = subsection(g, "vhSecurityHeadersSection");
field(headersFields, "nosniff", "vhNosniff", security.nosniff, "checkbox");
field(headersFields, "frame", "vhFrame", security.frame_options || "preserve", "select", { values: [["preserve", "vhPreserve"], ["deny", "vhDeny"], ["sameorigin", "vhSameorigin"]] });
field(headersFields, "referrer", "vhReferrer", security.referrer_policy || "", "text", { maxLength: 8192 });
field(headersFields, "csp", "vhCsp", security.content_security_policy || "", "textarea", { rows: 2, maxLength: 8192 });
field(headersFields, "permissions", "vhPermissions", security.permissions_policy || "", "text", { maxLength: 8192 });
const uaBlock = value.user_agent_blocking || {};
field(g, "ua-block-enabled", "vhUaBlockEnabled", uaBlock.enabled === true, "checkbox");
field(g, "ua-block-patterns", "vhUaBlockPatterns", (uaBlock.patterns || []).join("\n"), "textarea", { rows: 4, maxLength: 32768 });
note(g, "vhUaBlockHelp");
const uaBlockingFields = subsection(g, "vhUaBlockSection");
field(uaBlockingFields, "ua-block-enabled", "vhUaBlockEnabled", uaBlock.enabled === true, "checkbox");
field(uaBlockingFields, "ua-block-patterns", "vhUaBlockPatterns", (uaBlock.patterns || []).join("\n"), "textarea", { rows: 4, maxLength: 32768 });
note(uaBlockingFields, "vhUaBlockHelp");
g = group("vhAdvanced", true); field(g, "paths", "vhPaths", JSON.stringify(value.routes || [], null, 2), "textarea", { rows: 5, maxLength: 131072 });
field(g, "acl", "vhAcl", JSON.stringify(value.acl ?? null, null, 2), "textarea", { rows: 5, maxLength: 32768 });
field(g, "headers", "vhHeaders", JSON.stringify(value.headers || {}, null, 2), "textarea", { rows: 5, maxLength: 65536 });
@@ -273,13 +303,16 @@ function load(value = defaults(), migrationIds = [], show = true) {
note(g, "vhErrorFallbackHelp");
locationsReader = locationsEditor(g, value.routes || []);
g = group("vhSsl");
const selector = field(g, "certificate-id", "vhCertificate", value.certificate_id || "", "select");
const certificateRow = el("div", undefined, "vh-wide vh-certificate-select-row"); g.append(certificateRow);
const selector = field(certificateRow, "certificate-id", "vhCertificate", value.certificate_id || "", "select");
const fillCertificates = () => { const selected = selector.value || value.certificate_id || ""; const inherit = text("option", "vhCertificateDefault"); inherit.value = ""; selector.replaceChildren(inherit, ...[...(envelope?.config.tls?.certificates || []), ...stagedCertificates].map(c => { const o = el("option", c.id); o.value = c.id; return o; })); selector.value = selected; }; fillCertificates();
g.append(button("certManage", async () => { if (!(await closeEditor())) return; setPanel("tls"); })); note(g, "certSelectHint");
certificateRow.append(button("certManage", async () => { if (!(await closeEditor())) return; setPanel("tls"); }));
note(g, "certSelectHint");
const mismatch = field(g, "allow-certificate-mismatch", "vhCertificateMismatchForce", value.allow_certificate_mismatch === true, "checkbox");
const mismatchHint = note(g, "vhCertificateMismatchWarning");
const syncMismatch = () => { mismatch.disabled = !selector.value; if (!selector.value) mismatch.checked = false; mismatchHint.hidden = !mismatch.checked; };
selector.addEventListener("change", syncMismatch); mismatch.addEventListener("change", syncMismatch); syncMismatch();
g = group("vhSslSettings");
const inheritedTlsPolicy = envelope?.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] };
const tlsPolicy = value.tls_policy || inheritedTlsPolicy;
const customTlsPolicy = field(g, "tls-policy-custom", "vhTlsOverride", value.tls_policy != null, "checkbox");
@@ -288,15 +321,11 @@ function load(value = defaults(), migrationIds = [], show = true) {
const tlsCiphers = field(g, "tls-policy-ciphers", "tlsCiphers", (tlsPolicy.cipher_suites || []).join("\n"), "textarea", { rows: 7, maxLength: 8192 });
const syncTlsPolicy = () => { for (const input of [tls12, tls13, tlsCiphers]) input.disabled = !customTlsPolicy.checked; };
customTlsPolicy.addEventListener("change", syncTlsPolicy); syncTlsPolicy(); note(g, "vhTlsOverrideHelp");
g = group("vhTinyauth"); const auth = value.tinyauth || {}, authRoot = g;
g = group("vhTinyauth"); const auth = value.tinyauth || {};
const authMode = field(g, "auth-mode", "authMode", value.basic_auth?.enabled ? "basic" : auth.enabled ? "tinyauth" : "none", "select", { values: [["none", "authNone"], ["tinyauth", "authTinyauth"], ["basic", "authBasic"]] });
note(g, "authModeHelp");
const aclBody = el("section", undefined, "vh-wide auth-subcard auth-ip-card"); authRoot.append(aclBody);
aclBody.append(text("h3", "vhIpAccess"));
aclControl = aclEditor(aclBody, value.acl, () => { if (aclControl) read("acl").value = JSON.stringify(aclControl.read(), null, 2); });
const taBody = el("section", undefined, "vh-wide auth-subcard"), basicBody = el("section", undefined, "vh-wide auth-subcard"); authRoot.append(taBody, basicBody);
const taBody = el("section", undefined, "vh-wide auth-subcard"), basicBody = el("section", undefined, "vh-wide auth-subcard"); g.append(taBody, basicBody);
taBody.append(text("h3", "authTinyauth"));
const taPrimary = el("div", undefined, "vh-grid"); taBody.append(taPrimary);
const authEnable = field(taPrimary, "auth-enabled", "taEnable", auth.enabled, "checkbox");
@@ -361,16 +390,21 @@ function load(value = defaults(), migrationIds = [], show = true) {
field(cacheOptions, "vh-cache-object", "vhCacheObject", cache.max_object_bytes == null ? "" : cache.max_object_bytes / 1024, "number", { min: 1, step: "any", max: (envelope?.config.cache?.max_object_bytes ?? 1048576) / 1024 });
tri(cacheOptions, "vh-cache-query", "cacheBypassQuery", cache.bypass_query);
tri(cacheOptions, "vh-cache-static-cookie", "vhCacheStaticCookie", cache.static_ignore_cookies);
const customStatic = field(cacheOptions, "vh-cache-static-custom", "vhCacheStaticCustom", cache.static_extensions != null, "checkbox");
const staticExtensions = field(cacheOptions, "vh-cache-static-extensions", "vhCacheStaticExtensions", (cache.static_extensions || envelope?.config.cache?.static_extensions || []).join("\n"), "textarea", { rows: 5, maxLength: 2048 });
const syncStatic = () => { staticExtensions.disabled = !customStatic.checked; }; customStatic.addEventListener("change", syncStatic); syncStatic(); note(cacheOptions, "vhCacheStaticHelp");
const staticExtensionFields = el("div", undefined, "vh-wide vh-override-fields"); cacheOptions.append(staticExtensionFields);
const customStatic = field(staticExtensionFields, "vh-cache-static-custom", "vhCacheStaticCustom", cache.static_extensions != null, "checkbox");
const staticExtensions = field(staticExtensionFields, "vh-cache-static-extensions", "vhCacheStaticExtensions", (cache.static_extensions || envelope?.config.cache?.static_extensions || []).join("\n"), "textarea", { rows: 5, maxLength: 2048 });
const syncStatic = () => { staticExtensions.disabled = !customStatic.checked; }; customStatic.addEventListener("change", syncStatic); syncStatic(); note(staticExtensionFields, "vhCacheStaticHelp");
tri(cacheOptions, "vh-cache-status", "cacheStatusHeader", cache.expose_status); tri(cacheOptions, "vh-cache-hits", "cacheHitsHeader", cache.expose_hits); tri(cacheOptions, "vh-cache-rfc", "cacheRfcHeader", cache.expose_cache_status); tri(cacheOptions, "vh-cache-resident-age", "cacheResidentAge", cache.expose_resident_age); tri(cacheOptions, "vh-cache-only-hits", "cacheOnlyHits", cache.expose_only_hits);
field(cacheOptions, "vh-cache-prefix-custom", "vhCacheCustomPrefixes", cache.bypass_prefixes != null, "checkbox");
field(cacheOptions, "vh-cache-prefixes", "cacheBypassPrefixes", (cache.bypass_prefixes || []).join("\n"), "textarea", { rows: 2, maxLength: 32768 }); note(cacheOptions, "vhCacheHint");
const forceCache = field(cacheOptions, "vh-cache-force-enabled", "vhCacheForceEnabled", cache.unsafe_force?.enabled === true, "checkbox");
const forceTargets = field(cacheOptions, "vh-cache-force-targets", "vhCacheForceTargets", (cache.unsafe_force?.targets || []).join("\n"), "textarea", { rows: 3, maxLength: 65536 });
const bypassFields = el("div", undefined, "vh-wide vh-override-fields"); cacheOptions.append(bypassFields);
const customCachePrefixes = field(bypassFields, "vh-cache-prefix-custom", "vhCacheCustomPrefixes", cache.bypass_prefixes != null, "checkbox");
const bypassPrefixes = field(bypassFields, "vh-cache-prefixes", "cacheBypassPrefixes", (cache.bypass_prefixes || []).join("\n"), "textarea", { rows: 2, maxLength: 32768 });
const syncBypassPrefixes = () => { bypassPrefixes.disabled = !customCachePrefixes.checked; };
customCachePrefixes.addEventListener("change", syncBypassPrefixes); syncBypassPrefixes(); note(bypassFields, "vhCacheHint");
const unsafeCacheFields = el("div", undefined, "vh-wide vh-override-fields"); cacheOptions.append(unsafeCacheFields);
const forceCache = field(unsafeCacheFields, "vh-cache-force-enabled", "vhCacheForceEnabled", cache.unsafe_force?.enabled === true, "checkbox");
const forceTargets = field(unsafeCacheFields, "vh-cache-force-targets", "vhCacheForceTargets", (cache.unsafe_force?.targets || []).join("\n"), "textarea", { rows: 3, maxLength: 65536 });
const syncForceCache = () => { forceTargets.disabled = !forceCache.checked; }; forceCache.addEventListener("change", async () => { if (forceCache.checked && !(await appConfirm(t("vhCacheForceConfirm"), { danger: true }))) forceCache.checked = false; syncForceCache(); }); syncForceCache();
note(cacheOptions, "vhCacheForceHelp");
note(unsafeCacheFields, "vhCacheForceHelp");
if (editing) cacheOptions.append(button("vhPurgeCache", () => purge(editing)));
const syncCacheVisibility = () => {
const enabled = envelope?.config.cache?.enabled === true && cacheToggle.value !== "off";
@@ -381,8 +415,9 @@ function load(value = defaults(), migrationIds = [], show = true) {
const transformEnabled = field(g, "vh-transform-enabled", "vhTransformEnable", transform.enabled === true, "checkbox");
const transformOptions = el("div", undefined, "vh-wide vh-grid vh-transform-options"); g.append(transformOptions);
for (const [name, key] of [["javascript", "transformJavascript"], ["css", "transformCss"], ["html", "transformHtml"], ["svg", "transformSvg"], ["json", "transformJson"]]) tri(transformOptions, `vh-transform-${name}`, key, transform[name]);
const customTransformExtensions = field(transformOptions, "vh-transform-extensions-custom", "vhTransformExtensionsCustom", transform.extensions != null, "checkbox");
const transformExtensions = field(transformOptions, "vh-transform-extensions", "transformExtensions", (transform.extensions || envelope?.config.transform?.extensions || []).join("\n"), "textarea", { rows: 5, maxLength: 2048 });
const transformExtensionFields = el("div", undefined, "vh-wide vh-override-fields"); transformOptions.append(transformExtensionFields);
const customTransformExtensions = field(transformExtensionFields, "vh-transform-extensions-custom", "vhTransformExtensionsCustom", transform.extensions != null, "checkbox");
const transformExtensions = field(transformExtensionFields, "vh-transform-extensions", "transformExtensions", (transform.extensions || envelope?.config.transform?.extensions || []).join("\n"), "textarea", { rows: 5, maxLength: 2048 });
const syncTransform = () => {
transformOptions.hidden = !transformEnabled.checked;
transformExtensions.disabled = !customTransformExtensions.checked;
+2 -2
View File
@@ -1,6 +1,6 @@
"use strict";
function userAgentRateEditor(parent, values = []) {
parent.append(text("h3", "vhUaRateTitle", "subheading vh-wide"));
function userAgentRateEditor(parent, values = [], showHeading = true) {
if (showHeading) parent.append(text("h3", "vhUaRateTitle", "subheading vh-wide"));
const rows = el("div", undefined, "vh-wide vh-ua-rates"); parent.append(rows);
const add = (value = {}) => {
if (rows.children.length >= 64) { toast(t("vhUaRateMax"), true); return; }
+3 -2
View File
@@ -456,7 +456,9 @@ mod tests {
assert!(blocked.forced_mismatches.is_empty());
vhost.allow_certificate_mismatch = true;
let forced = for_vhosts(&settings, &config.vhosts).unwrap();
assert!(forced.forced_mismatches.contains(&("named".into(), "other.example.test".into())));
assert!(forced
.forced_mismatches
.contains(&("named".into(), "other.example.test".into())));
assert!(compile(&forced, Some(dir.path())).is_ok());
let saved = serde_json::to_value(&forced).unwrap();
assert!(saved.get("forced_mismatches").is_none());
@@ -472,5 +474,4 @@ mod tests {
let partial = for_vhosts(&settings, &config.vhosts).unwrap();
assert!(compile(&partial, Some(dir.path())).is_err());
}
}