0.29.7
This commit is contained in:
1 parent
ad95bfb050
commit
1431dd47f0
62 files changed
+1360
-198
No files matched your search
@@ -173,6 +173,11 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
"guides",
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/PHP_FPM.md")),
|
||||
),
|
||||
(
|
||||
"docs/PHP_FPM_ISOLATION.md",
|
||||
"guides",
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/PHP_FPM_ISOLATION.md")),
|
||||
),
|
||||
(
|
||||
"docs/PROMETHEUS.md",
|
||||
"guides",
|
||||
|
||||
+44
-1
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.29.6",
|
||||
"version": "0.29.7",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
||||
},
|
||||
"servers": [
|
||||
@@ -527,6 +527,30 @@
|
||||
}
|
||||
},
|
||||
"description": "Dangerous per-vhost-only request-side forced-cache target prefixes. Response privacy exclusions remain authoritative."
|
||||
},
|
||||
"expose_only_hits": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "If true, only emit cache diagnostic headers for actual HIT responses, never MISS/BYPASS. null inherits global expose_only_hits. Via is controlled independently."
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"pages": {
|
||||
"type": "object",
|
||||
"description": "Vhost response/error pages and optional application HTTP-error interception. Interception replaces application response HTML with the configured vhost/global page, or a built-in RustProxy page, before cache admission. The application's HTTP status is always retained, regardless of standalone page status overrides. Internal location fallbacks take priority.",
|
||||
"properties": {
|
||||
"intercept_statuses": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"uniqueItems": true,
|
||||
"maxItems": 11,
|
||||
"description": "Off by default. For these origin response HTTP statuses, replace the application response with a RustProxy error page (no-store). Does not change RustProxy-generated routing/security errors.",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"enum": [400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
@@ -1818,6 +1842,11 @@
|
||||
"maximum": 95,
|
||||
"default": 80,
|
||||
"description": "HOT usage waterline targeted by automatic idle trimming."
|
||||
},
|
||||
"expose_only_hits": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Suppress RustProxy cache diagnostic headers on MISS/BYPASS. Cache HIT diagnostics remain governed by expose_* flags. The independently configured Via header is unaffected."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -1904,6 +1933,20 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Explicitly allow this backend to resolve to a local RustProxy listener, including a listener using port 9090. This can create a proxy loop or expose the administration plane. Loopback/private targets still require allow_private=true."
|
||||
},
|
||||
"health_check_enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Enable both active and passive health monitoring for this backend. False still permits internal pinned-location traffic and does not change pool_eligible."
|
||||
},
|
||||
"health_check": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/DockerHealthCheck"
|
||||
}
|
||||
],
|
||||
"nullable": true,
|
||||
"description": "Optional per-backend active health policy; null inherits the vhost policy, including its default Host."
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
|
||||
@@ -29,7 +29,7 @@ function renderSettings() {
|
||||
$("cache-proactive-enabled").checked = c.proactive_refresh_enabled === true; $("cache-proactive-before").value = c.proactive_refresh_before_expiry_seconds ?? 15; $("cache-proactive-percent").value = c.proactive_refresh_percent ?? 10; $("cache-proactive-inflight").value = c.proactive_refresh_max_inflight ?? 4;
|
||||
$("cache-query").checked = !!c.bypass_query; $("cache-prefixes").value = (c.bypass_prefixes || []).join("\n");
|
||||
$("cache-static-cookie").checked = c.static_ignore_cookies !== false; $("cache-static-extensions").value = (c.static_extensions || []).join("\n");
|
||||
$("cache-status-header").checked = !!c.expose_status; $("cache-hits-header").checked = !!c.expose_hits; $("cache-rfc-header").checked = !!c.expose_cache_status; $("cache-resident-age-header").checked = !!c.expose_resident_age;
|
||||
$("cache-status-header").checked = !!c.expose_status; $("cache-hits-header").checked = !!c.expose_hits; $("cache-rfc-header").checked = !!c.expose_cache_status; $("cache-resident-age-header").checked = !!c.expose_resident_age; $("cache-only-hits").checked = !!c.expose_only_hits;
|
||||
const viaHeader = c.via_header === undefined ? "RustProxy" : c.via_header;
|
||||
$("cache-via-enabled").checked = viaHeader !== null; $("cache-via-value").value = viaHeader || "RustProxy"; syncCacheVia();
|
||||
setCacheMemoryPresetSelection(["conservative", "recommended", "performance"].includes(c.memory_preset) ? c.memory_preset : null);
|
||||
@@ -285,7 +285,7 @@ bindSettingsForm("response-form", next => {
|
||||
next.compression = { min_bytes: Number($("compress-min").value) }; for (const name of ["gzip", "br", "zstd", "deflate"]) next.compression[name] = $(`compress-${name}`).checked;
|
||||
}, "response", next => ({ server_header: next.server_header, diagnostic_headers: next.diagnostic_headers, compression: next.compression }));
|
||||
bindSettingsForm("cache-form", next => {
|
||||
next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked, via_header: $("cache-via-enabled").checked ? $("cache-via-value").value.trim() : null };
|
||||
next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked, expose_only_hits: $("cache-only-hits").checked, via_header: $("cache-via-enabled").checked ? $("cache-via-value").value.trim() : null };
|
||||
}, "cache");
|
||||
bindSettingsForm("transform-form", next => {
|
||||
next.transform = {
|
||||
|
||||
+42
-50
@@ -2277,6 +2277,7 @@
|
||||
type="checkbox" /><span>Cache-Status (RFC 9211)</span></label><label
|
||||
class="check"><input id="cache-resident-age-header"
|
||||
type="checkbox" /><span>X-Cache-Resident-Age</span></label></div>
|
||||
<label class="check"><input id="cache-only-hits" type="checkbox" /><span data-i18n="cacheOnlyHits">Expose cache diagnostics only on HIT (hide MISS/BYPASS)</span></label>
|
||||
<div class="cache-via-setting">
|
||||
<label class="check"><input id="cache-via-enabled" type="checkbox" /><span
|
||||
data-i18n="cacheViaEnable">Add Via response header</span></label>
|
||||
@@ -3331,47 +3332,35 @@
|
||||
<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>
|
||||
<label class="check"><input id="web-service-php-enabled" type="checkbox" /><span
|
||||
data-i18n="webServicePhpEnabled">Enable PHP-FPM / FastCGI</span></label>
|
||||
<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>
|
||||
<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" /></label>
|
||||
<label class="check"><input id="web-service-php-pass-authorization" type="checkbox" /><span
|
||||
data-i18n="webServicePhpPassAuthorization">Forward Authorization header</span></label>
|
||||
<label class="check"><input id="web-service-php-path-info" type="checkbox" /><span
|
||||
data-i18n="webServicePhpPathInfo">Enable PHP PATH_INFO splitting</span></label>
|
||||
<label><span data-i18n="webServicePhpConnectTimeout">Connect timeout (ms)</span><input
|
||||
id="web-service-php-connect-timeout" min="50" max="60000" step="1" type="number" /></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" /></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" /></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" /></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" /></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" /></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></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></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.</small></label>
|
||||
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.
|
||||
RustProxy sends the validated set as PHP_VALUE; Use the separate operator-only PHP_ADMIN_VALUE field for administrative overrides.</small></label>
|
||||
<label class="vh-wide"><span>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">Trusted operator settings only. Restrict PHP-FPM socket/network access; do not use shared PHP-FPM pools as a security boundary.</small></label>
|
||||
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
|
||||
@@ -3411,6 +3400,8 @@
|
||||
<p class="fine" data-i18n="phpProfileDescription">Expand a profile to edit it. Changes are saved immediately and shared by every attached Web Service.</p>
|
||||
<div id="php-fpm-profiles-list"></div>
|
||||
<button type="button" class="outline" id="php-fpm-profile-add" data-i18n="phpProfileAdd">Add profile</button>
|
||||
<hr class="php-profile-help-divider" />
|
||||
<button type="button" class="outline" id="php-fpm-profiles-help" data-i18n="webServicePhpDocsButton">FastCGI syntax and safety</button>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="htaccess-help-title" class="vhost-dialog location-help-dialog" id="htaccess-help-dialog">
|
||||
@@ -3461,41 +3452,42 @@ RewriteRule . /index.php [L]</code></pre>
|
||||
<h2 data-i18n="webServicePhpDocsTitle" id="php-help-title">PHP-FPM / FastCGI configuration</h2>
|
||||
<button class="outline" data-i18n="close" id="php-help-close" type="button">Close</button>
|
||||
</div>
|
||||
<article class="card location-help-content">
|
||||
<p data-i18n="webServicePhpDocsIntro">RustProxy connects directly to an existing PHP-FPM pool. Prefer a Unix
|
||||
socket on the same host. TCP is accepted only on loopback and is not a public/network FastCGI transport.
|
||||
</p>
|
||||
<article class="card location-help-content php-reference">
|
||||
<p data-i18n="phpDocsScope">The same fields and syntax apply to a local Web Service PHP configuration and a reusable PHP-FPM profile. A selected profile overrides local PHP fields for all attached services. RustProxy connects to an already running PHP-FPM pool.</p>
|
||||
<h3 data-i18n="phpDocsSwitchesTitle">Enabling PHP and PATH_INFO</h3>
|
||||
<p 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>
|
||||
<p 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>
|
||||
<p 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>
|
||||
<h3 data-i18n="webServicePhpDocsEndpointTitle">Endpoint syntax</h3>
|
||||
<pre><code>unix:/run/php/php-fpm.sock
|
||||
<pre><code>unix:/run/php/php_wordpress.sock
|
||||
tcp:127.0.0.1:9000
|
||||
tcp:[::1]:9000</code></pre>
|
||||
<p class="fine" data-i18n="webServicePhpDocsEndpoint">For TCP, include the tcp: prefix, a literal loopback
|
||||
IP and port. Hostnames and non-loopback addresses are rejected. For Unix sockets, RustProxy must have
|
||||
permission to connect to the socket.</p>
|
||||
<p data-i18n="webServicePhpDocsEndpoint">Use unix: for the full local socket path or tcp: plus a literal loopback IP and port. Hostnames and non-loopback TCP are rejected. The RustProxy user must be allowed to connect to the socket. Separate pools when PHP INI security settings differ.</p>
|
||||
<h3 data-i18n="phpDocsLimitsTitle">Execution and size limits</h3>
|
||||
<p data-i18n="phpDocsLimitsText">PHP extensions: comma-separated names such as php, phtml (no leading dots). Connect timeout: 1000 ms = 1 second. Full FastCGI request timeout: 30000 ms = 30 seconds. Concurrent requests: 64 (returns 503 when exhausted). Request body: 67108864 bytes = 64 MiB; 0 means unlimited by RustProxy. Response headers: 65536 bytes. STDERR: 4096 bytes; 0 disables capture. All numeric fields require integers; PHP INI limits are separate.</p>
|
||||
<h3 data-i18n="webServicePhpDocsCustomTitle">Custom FastCGI parameters</h3>
|
||||
<pre><code>APP_ENV = production
|
||||
APP_DEBUG = 0
|
||||
TENANT = main</code></pre>
|
||||
<p data-i18n="webServicePhpDocsCustom">Use one NAME = value entry per line. Names are normalized to
|
||||
uppercase and may contain A-Z, 0-9 and underscore. These are static trusted operator values. RustProxy
|
||||
rejects core CGI names, HTTP_* names, PHP_VALUE and PHP_ADMIN_VALUE so they cannot override request
|
||||
routing, script selection or HTTP-derived parameters.</p>
|
||||
<h3 data-i18n="webServicePhpDocsValuesTitle">PHP INI overrides</h3>
|
||||
<p data-i18n="webServicePhpDocsCustom">Put these in Custom FastCGI parameters only. One NAME = value per line; names start with A-Z and then contain A-Z, 0-9 or underscore (max 64 characters). Values are static, up to 8192 bytes each; no placeholders are expanded. Do not use SCRIPT_FILENAME, HTTP_* or PHP_VALUE/PHP_ADMIN_VALUE here.</p>
|
||||
<h3 data-i18n="webServicePhpDocsValuesTitle">PHP_VALUE — regular php.ini overrides</h3>
|
||||
<pre><code>memory_limit = 256M
|
||||
upload_max_filesize = 64M
|
||||
post_max_size = 64M
|
||||
max_execution_time = 30</code></pre>
|
||||
<p data-i18n="webServicePhpDocsValues">These lines are combined into the FastCGI PHP_VALUE parameter.
|
||||
PHP-FPM/PHP still decides which directives are changeable at this scope. Newlines/control characters are
|
||||
rejected and PHP_ADMIN_VALUE is intentionally unavailable.</p>
|
||||
<h3 data-i18n="webServicePhpDocsLimitsTitle">Limits and headers</h3>
|
||||
<p data-i18n="webServicePhpDocsLimits">Request timeout covers the FastCGI exchange. The body limit is
|
||||
enforced while streaming and returns 413 when exceeded; 0 disables the RustProxy-side body limit.
|
||||
Response-header and stderr limits bound memory/diagnostic data. Authorization forwarding can be disabled
|
||||
for applications that do not need HTTP_AUTHORIZATION.</p>
|
||||
<p class="fine" data-i18n="webServicePhpDocsSecurity">Do not put secrets in custom parameters or PHP_VALUE
|
||||
unless you accept storing them in normal Web Service configuration. Use a dedicated non-root PHP-FPM
|
||||
pool identity and keep PHP-FPM off public networks.</p>
|
||||
max_execution_time = 30
|
||||
error_reporting = E_ALL
|
||||
log_errors = On
|
||||
display_errors = Off
|
||||
date.timezone = Europe/Warsaw</code></pre>
|
||||
<p data-i18n="phpDocsValuesText">Enter these in PHP INI overrides (PHP_VALUE), not in FastCGI parameters. Use one directive = value per line (php.ini notation, no [section] headers or PHP tags). Exact support depends on which directives PHP-FPM permits to change. Common sizes use M/G, booleans On/Off and times seconds. Do not duplicate a directive in PHP_ADMIN_VALUE.</p>
|
||||
<h3 data-i18n="phpDocsAdminTitle">PHP_ADMIN_VALUE — administrator-only overrides</h3>
|
||||
<pre><code>open_basedir = /var/www/webmail/:/tmp/:/usr/lib/php/
|
||||
display_errors = Off
|
||||
log_errors = On</code></pre>
|
||||
<p data-i18n="phpDocsAdminText">Put these in PHP administrator INI overrides (PHP_ADMIN_VALUE). open_basedir lists allowed directories separated by ':' on Unix. Set it to paths of THAT application; do not reuse Roundcube restrictions in a WordPress pool. For security-sensitive values, prefer php_admin_value[...] in the configuration of a dedicated PHP-FPM pool. Shared FPM workers can retain request-supplied INI settings.</p>
|
||||
<h3 data-i18n="phpDocsOtherTitle">Storage, validation and troubleshooting</h3>
|
||||
<p data-i18n="phpDocsOtherText">Each INI name may contain letters, digits, '.', '_' and '-' (up to 64 bytes); a value must be nonempty (up to 8192 bytes). Max 64 custom FastCGI entries and 64 INI overrides per map, with a total 128 KiB budget. No control characters or multi-line values. Values are saved in normal configuration: never place secrets there unless that storage is acceptable. A syntax-valid directive can still be rejected/ignored by PHP itself.</p>
|
||||
<p class="fine" data-i18n="phpDocsPoolWarning">Critical: per-service PHP_VALUE/PHP_ADMIN_VALUE do NOT isolate applications when they share one PHP-FPM worker pool. Use different Unix sockets and restart the pools after moving WordPress and Roundcube apart. PATH_INFO does not fix open_basedir isolation.</p>
|
||||
</article>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="vhost-editor-title" class="vhost-dialog" id="vhost-dialog">
|
||||
|
||||
@@ -30,8 +30,30 @@
|
||||
return { read() { return Object.fromEntries([...rows].map(([code, editor]) => [code, editor.read()]).filter(([, page]) => page)); } };
|
||||
}
|
||||
function vhostEditor(parent, values = {}) {
|
||||
const response = pageEditor(parent, "pageStaticHost", values.response), fallback = pageEditor(parent, "pageMissingPath", values.fallback), errors = errorsEditor(parent, values.errors);
|
||||
parent.append(label("p", "pagesSafetyHint", "fine")); return { read: () => ({ response: response.read(), fallback: fallback.read(), errors: errors.read() }) };
|
||||
const response = pageEditor(parent, "pageStaticHost", values.response), fallback = pageEditor(parent, "pageMissingPath", values.fallback);
|
||||
const interception = el("section", undefined, "php-origin-errors");
|
||||
const toggle = el("label", undefined, "check"), enabled = document.createElement("input");
|
||||
enabled.type = "checkbox"; enabled.checked = (values.intercept_statuses || []).length > 0;
|
||||
toggle.append(enabled, label("span", "vhostInterceptOriginErrors"));
|
||||
const codesLabel = el("label"), codesCaption = label("span", "vhostInterceptOriginStatuses"), statuses = document.createElement("input");
|
||||
statuses.type = "text"; statuses.maxLength = 64;
|
||||
statuses.value = (values.intercept_statuses?.length ? values.intercept_statuses : [404]).join(", ");
|
||||
statuses.placeholder = "404, 500, 502, 503, 504";
|
||||
codesLabel.append(codesCaption, statuses);
|
||||
const hint = label("p", "vhostInterceptOriginErrorsHelp", "fine");
|
||||
const sync = () => { statuses.disabled = !enabled.checked; };
|
||||
enabled.addEventListener("change", sync); sync();
|
||||
interception.append(toggle, codesLabel, hint); parent.append(interception);
|
||||
const errors = errorsEditor(parent, values.errors);
|
||||
parent.append(label("p", "pagesSafetyHint", "fine"));
|
||||
return { read: () => {
|
||||
const selectedTokens = enabled.checked ? statuses.value.split(/[\s,]+/).filter(Boolean) : [];
|
||||
const selected = selectedTokens.map(Number);
|
||||
if (enabled.checked && (!selected.length || selectedTokens.some(token => !/^\d{3}$/.test(token)) || selected.length > codes.length || new Set(selected).size !== selected.length || selected.some(code => !codes.includes(code)))) {
|
||||
throw new Error(t("vhostInterceptOriginInvalid"));
|
||||
}
|
||||
return { response: response.read(), fallback: fallback.read(), errors: errors.read(), intercept_statuses: selected };
|
||||
} };
|
||||
}
|
||||
let global = null;
|
||||
function render() { if (!envelope || settingsDirty.has("pages-form")) return; const values = envelope.config.pages || {}, parent = $("global-pages-fields"); parent.replaceChildren(); const ip = pageEditor(parent, "pageDirectIp", values.direct_ip), unknown = pageEditor(parent, "pageUnknownHost", values.unknown_host), errors = errorsEditor(parent, values.errors); global = { read: () => ({ direct_ip: ip.read(), unknown_host: unknown.read(), errors: errors.read() }) }; }
|
||||
|
||||
@@ -9871,3 +9871,51 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
grid-template-columns: 1fr
|
||||
}
|
||||
}
|
||||
|
||||
/* 0.29.7: PHP-FPM local/profile toggle rows with accessible info popovers. */
|
||||
.php-switch-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
min-height: 30px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.php-switch-row .php-switch-label {
|
||||
display: inline-flex !important;
|
||||
flex: 0 1 auto;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
margin: 0 !important;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.php-switch-row .php-switch-label input[type="checkbox"] {
|
||||
position: static;
|
||||
flex: 0 0 var(--ui-check-size);
|
||||
margin: 0;
|
||||
}
|
||||
.php-switch-row .php-switch-label span { min-width: 0; }
|
||||
.php-switch-row .php-field-info { flex: 0 0 auto; }
|
||||
.php-switch-row .php-field-info .metric-info-popover { right: auto; left: 0; }
|
||||
.php-reference pre {
|
||||
overflow-x: auto;
|
||||
white-space: pre;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
background: var(--panel2);
|
||||
font: 12px/1.6 var(--mono);
|
||||
}
|
||||
.php-profile-help-divider {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
margin: 16px 0 12px;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.php-origin-errors { grid-column: 1 / -1; display: grid; gap: 8px; padding: 10px 0; border-top: 1px solid var(--line); }
|
||||
.php-origin-errors > label { max-width: 430px; }
|
||||
.php-origin-errors > .check { margin: 0 !important; }
|
||||
.php-origin-errors .fine { margin: 0; }
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
function activateTab(name) { document.querySelectorAll("#vhost-tabs button").forEach(b => { const active = b.dataset.tab === name; b.classList.toggle("active", active); b.setAttribute("aria-pressed", String(active)); b.setAttribute("aria-selected", String(active)); b.tabIndex = active ? 0 : -1; }); container.querySelectorAll(".vh-group").forEach(g => g.hidden = g.dataset.tab !== name); }
|
||||
function initTabs() { const tabs = $("vhost-tabs"); tabs.replaceChildren(); tabs.setAttribute("role", "tablist"); for (const [name, key] of [["details", "vhDetailsTab"], ["backends", "vhBackendPool"], ["health", "vhHealth"], ["locations", "vhLocations"], ["auth", "vhTabAuth"], ["ssl", "vhSsl"], ["cache", "vhCache"], ["compression", "vhCompression"], ["transform", "vhTransform"], ["security", "vhSecurity"], ["pages", "vhPages"], ["advanced", "advanced"]]) { const b = button(key, () => activateTab(name)); b.dataset.tab = name; b.setAttribute("role", "tab"); tabs.append(b); } activateTab("details"); }
|
||||
function collectCache() { const tri = name => str(name) === "" ? null : str(name) === "on", number = (name, mul = 1) => { if (str(name) === "") return null; const value = Number(str(name)) * mul; if (!Number.isSafeInteger(value)) throw new Error(t("vhJsonInvalid")); return value; }; return { enabled: tri("vh-cache-enabled"), max_ttl_seconds: number("vh-cache-ttl"), default_ttl_seconds: number("vh-cache-default"), max_object_bytes: number("vh-cache-object", 1024), bypass_query: tri("vh-cache-query"), static_ignore_cookies: tri("vh-cache-static-cookie"), static_extensions: yes("vh-cache-static-custom") ? lines(str("vh-cache-static-extensions")).map(v => v.replace(/^\./, "").toLowerCase()) : null, expose_status: tri("vh-cache-status"), expose_hits: tri("vh-cache-hits"), expose_cache_status: tri("vh-cache-rfc"), expose_resident_age: tri("vh-cache-resident-age"), bypass_prefixes: yes("vh-cache-prefix-custom") ? lines(str("vh-cache-prefixes")) : null, unsafe_force: { enabled: yes("vh-cache-force-enabled"), targets: lines(str("vh-cache-force-targets")) } }; }
|
||||
function collectCache() { const tri = name => str(name) === "" ? null : str(name) === "on", number = (name, mul = 1) => { if (str(name) === "") return null; const value = Number(str(name)) * mul; if (!Number.isSafeInteger(value)) throw new Error(t("vhJsonInvalid")); return value; }; return { enabled: tri("vh-cache-enabled"), max_ttl_seconds: number("vh-cache-ttl"), default_ttl_seconds: number("vh-cache-default"), max_object_bytes: number("vh-cache-object", 1024), bypass_query: tri("vh-cache-query"), static_ignore_cookies: tri("vh-cache-static-cookie"), static_extensions: yes("vh-cache-static-custom") ? lines(str("vh-cache-static-extensions")).map(v => v.replace(/^\./, "").toLowerCase()) : null, expose_status: tri("vh-cache-status"), expose_hits: tri("vh-cache-hits"), expose_cache_status: tri("vh-cache-rfc"), expose_resident_age: tri("vh-cache-resident-age"), expose_only_hits: tri("vh-cache-only-hits"), bypass_prefixes: yes("vh-cache-prefix-custom") ? lines(str("vh-cache-prefixes")) : null, unsafe_force: { enabled: yes("vh-cache-force-enabled"), targets: lines(str("vh-cache-force-targets")) } }; }
|
||||
function collectCompression() { const tri = name => str(name) === "" ? null : str(name) === "on"; return { enabled: tri("vh-compression-enabled"), zstd: tri("vh-compression-zstd"), br: tri("vh-compression-br"), gzip: tri("vh-compression-gzip"), deflate: tri("vh-compression-deflate") }; }
|
||||
function collectTransform() {
|
||||
const tri = name => str(name) === "" ? null : str(name) === "on";
|
||||
@@ -204,7 +204,7 @@
|
||||
|
||||
const routing = el("div", undefined, "vh-wide vh-backend-switches"); main.append(routing);
|
||||
const controls = {};
|
||||
for (const [name, key, checked] of [["member-enabled", "vhEnabled", value.enabled !== false], ["member-pool", "locationPoolEligible", value.pool_eligible !== false], ["member-backup", "vhBackup", value.backup], ["member-drain", "vhDrain", value.drain]]) controls[name] = field(routing, name, key, checked, "checkbox");
|
||||
for (const [name, key, checked] of [["member-enabled", "vhEnabled", value.enabled !== false], ["member-pool", "locationPoolEligible", value.pool_eligible !== false], ["member-backup", "vhBackup", value.backup], ["member-drain", "vhDrain", value.drain], ["member-health-enabled", "backendHealthEnabled", value.health_check_enabled !== false]]) controls[name] = field(routing, name, key, checked, "checkbox");
|
||||
const connection = el("div", undefined, "vh-wide vh-backend-switches vh-backend-connection"); main.append(connection);
|
||||
controls["member-private"] = field(connection, "member-private", "vhPrivate", value.allow_private, "checkbox");
|
||||
const preserve = field(connection, "member-host", "vhPreserveHost", value.preserve_host === true, "checkbox");
|
||||
@@ -222,6 +222,25 @@
|
||||
field(extra, "member-max-concurrent", "vhMaxConcurrentRequests", value.max_concurrent_requests ?? 0, "number", { required: true, min: 0, max: 1000000 });
|
||||
field(extra, "member-queue-timeout", "vhBackendQueueTimeout", value.queue_timeout_ms ?? 0, "number", { required: true, min: 0, max: 60000 });
|
||||
note(extra, "vhBackendCapacityHelp");
|
||||
const customHealth = field(extra, "member-health-custom", "backendHealthOverride", value.health_check != null, "checkbox");
|
||||
const health = value.health_check || {};
|
||||
const healthFields = el("div", undefined, "vh-wide vh-grid vh-backend-health-fields"); extra.append(healthFields);
|
||||
field(healthFields, "member-health-mode", "vhHealthMode", health.mode || "tcp", "select", { values: [["off", "vhOff"], ["tcp", "vhTcp"], ["http", "vhHttp"]] });
|
||||
field(healthFields, "member-health-path", "vhHealthPath", health.path ?? "/", "text", { maxLength: 1024 });
|
||||
field(healthFields, "member-health-host", "vhHealthHost", health.host ?? "", "text", { maxLength: 253 });
|
||||
field(healthFields, "member-health-interval", "vhInterval", health.interval_ms ?? 5000, "number", { min: 250, max: 300000, required: true });
|
||||
field(healthFields, "member-health-timeout", "vhTimeout", health.timeout_ms ?? 2000, "number", { min: 50, max: 60000, required: true });
|
||||
field(healthFields, "member-health-slow", "vhSlow", health.slow_response_ms ?? 1500, "number", { min: 0, max: 60000, required: true });
|
||||
field(healthFields, "member-health-fall", "vhFall", health.fall ?? 3, "number", { min: 1, max: 20, required: true });
|
||||
field(healthFields, "member-health-rise", "vhRise", health.rise ?? 2, "number", { min: 1, max: 20, required: true });
|
||||
field(healthFields, "member-health-min", "vhStatusMin", health.expected_status_min ?? 200, "number", { min: 100, max: 599, required: true });
|
||||
field(healthFields, "member-health-max", "vhStatusMax", health.expected_status_max ?? 399, "number", { min: 100, max: 599, required: true });
|
||||
const syncHealth = () => {
|
||||
healthFields.hidden = !customHealth.checked;
|
||||
healthFields.querySelectorAll("input,select").forEach(input => { input.disabled = !customHealth.checked; });
|
||||
};
|
||||
customHealth.addEventListener("change", syncHealth); syncHealth();
|
||||
note(extra, "backendHealthOverridesHelp");
|
||||
row.append(advanced);
|
||||
|
||||
const actions = el("div", undefined, "vh-backend-actions");
|
||||
|
||||
@@ -350,7 +350,7 @@
|
||||
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");
|
||||
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-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");
|
||||
@@ -387,7 +387,21 @@
|
||||
paths = JSON.parse(str("paths")); acl = JSON.parse(str("acl")); headers = JSON.parse(str("headers"));
|
||||
if (!Array.isArray(paths) || (acl !== null && (typeof acl !== "object" || Array.isArray(acl))) || !headers || typeof headers !== "object" || Array.isArray(headers)) throw Error();
|
||||
} catch { throw new Error(t("vhJsonInvalid")); }
|
||||
const members = [...form.querySelectorAll(".vh-member")].map(row => ({ id: str("member-id", row), url: str("member-url", row), weight: num("member-weight", row), enabled: yes("member-enabled", row), pool_eligible: yes("member-pool", row), backup: yes("member-backup", row), drain: yes("member-drain", row), allow_private: yes("member-private", row), accept_local_listener_risk: yes("member-local-risk", row), preserve_host: yes("member-host", row), protocol: str("member-protocol", row), address_allowlist: lines(str("member-allowlist", row)), tls_server_name: nullable("member-tls-name", row), tls_verify: yes("member-tls-verify", row), tls_ca_pem: nullable("member-ca", row), max_concurrent_requests: num("member-max-concurrent", row), queue_timeout_ms: num("member-queue-timeout", row) })).filter(member => member.url);
|
||||
const members = [...form.querySelectorAll(".vh-member")].map(row => ({
|
||||
id: str("member-id", row), url: str("member-url", row), weight: num("member-weight", row), enabled: yes("member-enabled", row),
|
||||
pool_eligible: yes("member-pool", row), backup: yes("member-backup", row), drain: yes("member-drain", row),
|
||||
health_check_enabled: yes("member-health-enabled", row),
|
||||
health_check: yes("member-health-custom", row) ? {
|
||||
mode: str("member-health-mode", row), path: str("member-health-path", row), host: nullable("member-health-host", row),
|
||||
interval_ms: num("member-health-interval", row), timeout_ms: num("member-health-timeout", row), slow_response_ms: num("member-health-slow", row),
|
||||
fall: num("member-health-fall", row), rise: num("member-health-rise", row),
|
||||
expected_status_min: num("member-health-min", row), expected_status_max: num("member-health-max", row)
|
||||
} : null,
|
||||
allow_private: yes("member-private", row), accept_local_listener_risk: yes("member-local-risk", row),
|
||||
preserve_host: yes("member-host", row), protocol: str("member-protocol", row), address_allowlist: lines(str("member-allowlist", row)),
|
||||
tls_server_name: nullable("member-tls-name", row), tls_verify: yes("member-tls-verify", row), tls_ca_pem: nullable("member-ca", row),
|
||||
max_concurrent_requests: num("member-max-concurrent", row), queue_timeout_ms: num("member-queue-timeout", row)
|
||||
})).filter(member => member.url);
|
||||
const bindingSelection = readBindings();
|
||||
const mainSourceType = str("source-type");
|
||||
const mainFallbackTarget = str("main-error-target"), mainFallbackStatuses = lines(str("main-error-statuses")).map(value => Number(value));
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
let up = 0, active = 0, requests = 0, errors = 0, traffic = 0, weightedLatency = 0, latencyWeight = 0;
|
||||
for (const member of members) {
|
||||
const memberTraffic = backendTrafficRate(pool.id, member, trafficSnapshot, trafficNow, seenTraffic);
|
||||
const label = vhostDisabled ? t("vhDisabled") : (pool.health_mode === "off" && member.status === "up" ? t("vhHealthDisabled") : t(statusKeys[member.status] || "vhHealthUnknown"));
|
||||
const label = vhostDisabled ? t("vhDisabled") : (member.health_mode === "off" && member.status === "up" ? t("vhHealthDisabled") : t(statusKeys[member.status] || "vhHealthUnknown"));
|
||||
const activeLimit = Number(member.max_concurrent_requests) > 0 ? `${member.active ?? 0}/${member.max_concurrent_requests}` : `${member.active ?? 0}/∞`;
|
||||
const lastFailure = Number(member.last_failure_at_ms) > 0 ? `${member.last_failure_class || "-"} · ${formatTime(member.last_failure_at_ms)}` : "-";
|
||||
const lastRecovery = Number(member.last_recovered_ms) > 0 ? formatTime(member.last_recovered_ms) : "-";
|
||||
|
||||
@@ -31,6 +31,16 @@ window.rustProxyPhpProfiles = ({ defaults, mapLines, lines, parseFastCgiParams,
|
||||
["php_admin_values", "phpProfileAdminValues", "textarea"],
|
||||
];
|
||||
|
||||
const helpKeys = {
|
||||
enabled: "phpHintEnabled", path_info: "phpHintPathInfo", endpoint: "webServicePhpDocsEndpoint",
|
||||
extensions: "phpHintExtensions", pass_authorization: "phpHintPassAuthorization",
|
||||
connect_timeout_ms: "phpHintConnectTimeout", request_timeout_ms: "phpHintRequestTimeout",
|
||||
max_concurrent_requests: "phpHintConcurrency", max_request_body_bytes: "phpHintBodyLimit",
|
||||
max_response_header_bytes: "phpHintHeaderLimit", stderr_log_bytes: "phpHintStderrLimit",
|
||||
custom_params: "webServicePhpCustomParamsHelp", php_values: "phpDocsValuesText",
|
||||
php_admin_values: "phpHintAdminValues"
|
||||
};
|
||||
|
||||
function profileEditor(profile, creating = false) {
|
||||
const details = document.createElement("details"); details.className = "card";
|
||||
if (creating) details.open = true;
|
||||
@@ -46,17 +56,35 @@ window.rustProxyPhpProfiles = ({ defaults, mapLines, lines, parseFastCgiParams,
|
||||
const nameInput = namedInput("phpProfileName", profile.name);
|
||||
const controls = {};
|
||||
for (const [key, label, kind, min, max] of profileFields) {
|
||||
const labelEl = el("label"); if (kind === "textarea") labelEl.className = "vh-wide";
|
||||
const caption = el("span", t(label)); labelEl.append(caption);
|
||||
const toggle = kind === "checkbox";
|
||||
const wrapper = toggle ? el("div", undefined, "php-switch-row vh-wide") : el("label");
|
||||
if (!toggle && kind === "textarea") wrapper.className = "vh-wide";
|
||||
const labelEl = toggle ? el("label", undefined, "check php-switch-label") : wrapper;
|
||||
const input = document.createElement(kind === "textarea" ? "textarea" : "input");
|
||||
if (kind !== "textarea") input.type = kind;
|
||||
if (kind === "checkbox") input.checked = profile.php?.[key] === true;
|
||||
else if (kind === "textarea") { input.rows = 3; input.maxLength = 65536; input.value = mapLines(profile.php?.[key]); }
|
||||
const caption = el("span", t(label));
|
||||
if (toggle) labelEl.append(input, caption); else labelEl.append(caption, input);
|
||||
if (toggle) input.checked = profile.php?.[key] === true;
|
||||
else if (kind === "textarea") { input.rows = 4; input.maxLength = 65536; input.value = mapLines(profile.php?.[key]); }
|
||||
else if (key === "extensions") input.value = (profile.php?.extensions || ["php"]).join(", ");
|
||||
else input.value = profile.php?.[key] ?? "";
|
||||
if (kind === "number") { input.min = min; input.max = max; input.step = 1; input.required = true; }
|
||||
if (key === "endpoint") { input.required = true; input.maxLength = 512; }
|
||||
labelEl.append(input); editor.append(labelEl); controls[key] = input;
|
||||
if (key === "endpoint") { input.required = true; input.maxLength = 512; input.placeholder = "unix:/run/php/php-fpm.sock"; }
|
||||
if (key === "extensions") input.placeholder = "php, phtml";
|
||||
if (key === "custom_params") input.placeholder = "APP_ENV = production\nAPP_DEBUG = 0";
|
||||
if (key === "php_values") input.placeholder = "memory_limit = 256M\npost_max_size = 64M";
|
||||
if (key === "php_admin_values") input.placeholder = "open_basedir = /var/www/webmail/:/tmp/";
|
||||
if (toggle) {
|
||||
const tip = el("details", undefined, "metric-info php-field-info"), summaryTip = el("summary", "i");
|
||||
summaryTip.setAttribute("aria-label", t(helpKeys[key]));
|
||||
const popover = el("div", undefined, "metric-info-popover");
|
||||
popover.append(el("p", t(helpKeys[key]), "fine")); tip.append(summaryTip, popover);
|
||||
wrapper.append(labelEl, tip);
|
||||
} else {
|
||||
const hint = el("small", t(helpKeys[key]), "fine");
|
||||
labelEl.append(hint);
|
||||
}
|
||||
editor.append(wrapper); controls[key] = input;
|
||||
}
|
||||
const actions = el("div", undefined, "form-actions vh-wide");
|
||||
const save = el("button", t("apply"), "primary"); save.type = "submit";
|
||||
|
||||
@@ -476,6 +476,7 @@
|
||||
$("web-service-php-profiles-open").addEventListener("click", openProfiles);
|
||||
$("php-fpm-profiles-close").addEventListener("click", () => $("php-fpm-profiles-dialog").close());
|
||||
$("php-fpm-profile-add").addEventListener("click", () => renderProfiles(true));
|
||||
$("php-fpm-profiles-help").addEventListener("click", () => $("php-help-dialog").showModal());
|
||||
$("web-service-compression").addEventListener("change", syncCompression);
|
||||
$("web-service-precompressed").addEventListener("change", syncPrecompressed);
|
||||
$("web-service-htaccess-enabled").addEventListener("change", syncHtaccess);
|
||||
|
||||
Reference in new issue
Block a user