v0.34.5
This commit is contained in:
1 parent
3776edf4fc
commit
7cfc3a6da1
35 files changed
+1377
-875
No files matched your search
@@ -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": [
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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 pdf 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 pdf 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 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 upload_max_filesize = 64M 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/ 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 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 upload_max_filesize = 64M 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/ 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
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user