This commit is contained in:
Mateusz Gruszczyński committed 2026-10-08 22:22:46 +02:00
1 parent 76cb4acef0
commit 68678e98be
84 files changed
+1519 -325

No files matched your search

+1
View File
@@ -409,6 +409,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
"/docs/V0.25.1_AUDIT.md"
)),
),
reader_doc!("docs/V0.29.1_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.29.0_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.28.2_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.28.1_IMPLEMENTATION.md", "technical"),
+77 -2
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.29.0",
"version": "0.29.2",
"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": [
@@ -412,6 +412,19 @@
"target"
],
"description": "Backend-less client redirect for this location."
},
"internal": {
"type": "boolean",
"default": false,
"description": "Internal named location excluded from public URI matching; reachable only by internal dispatch."
},
"error_fallback": {
"allOf": [
{
"$ref": "#/components/schemas/ErrorFallback"
}
],
"nullable": true
}
},
"additionalProperties": false,
@@ -438,7 +451,10 @@
"description": "Claim the direct www/non-www companion of the selected exact DNS hostname and redirect it permanently to that hostname."
},
"www_redirect_target": {
"type": ["string", "null"],
"type": [
"string",
"null"
],
"description": "An exact hostname from frontend.hosts to use as the canonical redirect destination. Null uses the first hostname for compatibility. Other hostnames continue serving the same vhost without redirects."
},
"forward_client_ip": {
@@ -598,6 +614,15 @@
},
"compression": {
"$ref": "#/components/schemas/CompressionOverrides"
},
"error_fallback": {
"allOf": [
{
"$ref": "#/components/schemas/ErrorFallback"
}
],
"nullable": true,
"description": "Error fallback for the main vhost source."
}
},
"additionalProperties": true,
@@ -5084,6 +5109,21 @@
"maxLength": 8192
},
"description": "Trusted php.ini directive/value overrides combined into the FastCGI PHP_VALUE parameter. PHP_ADMIN_VALUE is not exposed."
},
"path_info": {
"type": "boolean",
"default": false,
"description": "Safely split /script.php/extra/path into SCRIPT_FILENAME/SCRIPT_NAME for the existing script plus PATH_INFO."
},
"php_admin_values": {
"type": "object",
"maxProperties": 64,
"default": {},
"additionalProperties": {
"type": "string",
"maxLength": 8192
},
"description": "Trusted operator PHP-FPM administrative php.ini directives packed into PHP_ADMIN_VALUE. Restrict access to FastCGI endpoints."
}
},
"description": "Optional bounded direct FastCGI connection to a PHP-FPM pool."
@@ -5574,6 +5614,41 @@
"updated_files",
"database"
]
},
"ErrorFallback": {
"type": "object",
"additionalProperties": false,
"properties": {
"target": {
"type": "string",
"maxLength": 64,
"description": "Enabled internal Custom Location ID in the same vhost."
},
"statuses": {
"type": "array",
"minItems": 1,
"maxItems": 6,
"uniqueItems": true,
"default": [
404
],
"items": {
"type": "integer",
"enum": [
403,
404,
500,
502,
503,
504
]
}
}
},
"required": [
"target"
],
"description": "Nginx-style internal error fallback. The original request is internally dispatched without a client redirect."
}
}
},
+7 -1
View File
@@ -2,10 +2,16 @@
use crate::{auth::Session, runtime::ApiError};
use axum::{extract::rejection::JsonRejection, response::Html, Extension};
use http::StatusCode;
use std::sync::OnceLock;
static ADMIN_INDEX: OnceLock<String> = OnceLock::new();
pub(super) async fn index(session: Option<Extension<Session>>) -> Html<&'static str> {
if session.is_some() {
Html(include_str!("web/index.html"))
Html(ADMIN_INDEX.get_or_init(|| {
include_str!("web/index.html")
.replace("__RUSTPROXY_VERSION__", env!("CARGO_PKG_VERSION"))
}).as_str())
} else {
Html(include_str!("web/login.html"))
}
+6 -4
View File
@@ -2891,7 +2891,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.29.0</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>__RUSTPROXY_VERSION__</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
@@ -3335,6 +3335,8 @@
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
@@ -3360,8 +3362,8 @@
spellcheck="false"
placeholder="memory_limit = 256M&#10;upload_max_filesize = 64M&#10;post_max_size = 64M"></textarea><small
class="fine" data-i18n="webServicePhpValuesHelp">One php.ini directive = value per line.
RustProxy sends the validated set as PHP_VALUE; PHP_ADMIN_VALUE is never accepted
here.</small></label>
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/&#10;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>
<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
@@ -3635,7 +3637,7 @@ max_execution_time = 30</code></pre>
<article class="card about-card">
<div class="about-product">
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
<div><span class="small-label">RustProxy</span><strong>0.29.0</strong></div>
<div><span class="small-label">RustProxy</span><strong>__RUSTPROXY_VERSION__</strong></div>
</div>
<dl class="about-meta">
<div>
+13 -5
View File
@@ -47,7 +47,7 @@
frontend: { hosts: [], listener_ids: [] },
source: { type: "reverse_proxy" },
backends: [{ id: "origin-1", url: "", weight: 1, enabled: true, pool_eligible: true, allow_private: true, accept_local_listener_risk: false, preserve_host: true, protocol: "http1", tls_verify: false }],
cache: { enabled: false }, compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
error_fallback: null, cache: { enabled: false }, compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
});
function activateTab(name) { document.querySelectorAll("#vhost-tabs button").forEach(b => { const active = b.dataset.tab === name; b.classList.toggle("active", active); b.setAttribute("aria-pressed", String(active)); b.setAttribute("aria-selected", String(active)); b.tabIndex = active ? 0 : -1; }); container.querySelectorAll(".vh-group").forEach(g => g.hidden = g.dataset.tab !== name); }
@@ -313,8 +313,10 @@
return [...rows.children].map(r => {
const action = str("location-action", r), rewriteMode = str("location-rewrite-mode", r);
const handled = action === "proxy" || action === "web_server";
const internal = yes("location-internal", r), fallbackTarget = str("location-error-target", r);
const fallbackStatuses = lines(str("location-error-statuses", r)).map(value => Number(value));
return {
id: str("location-id", r), path_prefix: str("location-path", r), match_type: str("location-match", r),
id: str("location-id", r), internal, path_prefix: internal ? "/" : str("location-path", r), match_type: internal ? "prefix" : str("location-match", r),
source: action === "web_server" ? { type: "web_server", web_service_id: str("location-web-service", r) } : { type: "reverse_proxy" },
backend_id: action === "proxy" ? (str("location-backend", r) || null) : null,
enabled: yes("location-enabled", r), cache: action === "proxy" ? (str("location-cache", r) === "" ? null : str("location-cache", r) === "on") : false,
@@ -325,7 +327,8 @@
headers: r.readHeaders(),
rewrite: { mode: handled ? rewriteMode : "preserve", value: handled && rewriteMode !== "preserve" ? read("location-rewrite-value", r).value.trim() : "" },
response: action === "return" ? { status: Number(str("location-response-status", r)), content_type: str("location-response-type", r), body: read("location-response-body", r).value } : null,
redirect: action === "redirect" ? { status: Number(str("location-redirect-status", r)), target: str("location-redirect-target", r), rewrite_pattern: nullable("location-redirect-pattern", r), preserve_query: yes("location-redirect-query", r) } : null
redirect: action === "redirect" ? { status: Number(str("location-redirect-status", r)), target: str("location-redirect-target", r), rewrite_pattern: nullable("location-redirect-pattern", r), preserve_query: yes("location-redirect-query", r) } : null,
error_fallback: fallbackTarget ? { target: fallbackTarget, statuses: fallbackStatuses.length ? fallbackStatuses : [404] } : null
};
});
}
@@ -339,6 +342,7 @@
field(body, "location-id", "locationId", value.id || `location-${suffix}`, "text", { required: true, pattern: "[A-Za-z0-9_-]{1,64}", maxLength: 64 });
const match = field(body, "location-match", "locationMatchType", value.match_type || "prefix", "select", { values: [["prefix", "locationMatchPrefix"], ["prefix_priority", "locationMatchPriority"], ["exact", "locationMatchExact"], ["regex", "locationMatchRegex"], ["regex_insensitive", "locationMatchRegexI"]] });
const path = field(body, "location-path", "locationPathPattern", value.path_prefix || "/", "text", { required: true, maxLength: 1024 }); path.placeholder = "/api or ^/api/v[0-9]+/";
const internal = field(body, "location-internal", "locationInternal", value.internal === true, "checkbox");
const initialAction = value.response ? "return" : value.redirect ? "redirect" : value.source?.type === "web_server" ? "web_server" : "proxy";
const action = field(body, "location-action", "locationAction", initialAction, "select", { values: [["proxy", "locationProxyAction"], ["web_server", "locationWebServerAction"], ["redirect", "locationRedirectAction"], ["return", "locationReturnAction"]] });
const backend = field(body, "location-backend", "backend", value.backend_id || "", "select"); memberOptions(backend, value.backend_id || "");
@@ -347,6 +351,9 @@
const cache = field(body, "location-cache", "cacheEnabled", value.cache === true ? "on" : value.cache === false ? "off" : "", "select", { values: [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]] });
const handling = el("details", undefined, "vh-wide location-section"), handlingBody = el("div", undefined, "vh-grid"); handling.append(text("summary", "locationHandling"), handlingBody); body.append(handling);
field(handlingBody, "location-error-target", "locationErrorFallbackTarget", value.error_fallback?.target || "", "text", { maxLength: 64, pattern: "[A-Za-z0-9_-]{0,64}" });
field(handlingBody, "location-error-statuses", "locationErrorFallbackStatuses", (value.error_fallback?.statuses || [404]).join(", "), "text", { maxLength: 64 });
note(handlingBody, "locationErrorFallbackHelp");
const rewriteMode = field(handlingBody, "location-rewrite-mode", "locationRewriteMode", value.rewrite?.mode || "preserve", "select", { values: [["preserve", "locationRewritePreserve"], ["strip_prefix", "locationRewriteStrip"], ["replace_prefix", "locationRewriteReplace"], ["regex", "locationRewriteRegex"]] });
const rewriteValue = field(handlingBody, "location-rewrite-value", "locationRewriteValue", value.rewrite?.value || "", "text", { maxLength: 2048 });
const returnBox = el("div", undefined, "vh-wide vh-grid location-return-box"); handlingBody.append(returnBox);
@@ -395,14 +402,15 @@
const syncUi = () => {
const isReturn = action.value === "return", isRedirect = action.value === "redirect", isWeb = action.value === "web_server", isProxy = action.value === "proxy", isRegex = match.value === "regex" || match.value === "regex_insensitive";
match.disabled = path.disabled = internal.checked;
backend.disabled = !isProxy; cache.disabled = !isProxy; dedicated.disabled = !isProxy;
webService.disabled = !isWeb; webService.required = isWeb;
returnBox.hidden = !isReturn; redirectBox.hidden = !isRedirect; rewriteMode.disabled = isReturn || isRedirect; rewriteValue.disabled = isReturn || isRedirect || rewriteMode.value === "preserve" || rewriteMode.value === "strip_prefix";
for (const option of rewriteMode.options) if (option.value === "regex") option.disabled = !isRegex;
if (!isRegex && rewriteMode.value === "regex") rewriteMode.value = "preserve";
path.placeholder = isRegex ? "^/api/v[0-9]+/" : "/api/";
const id = str("location-id", row) || t("locationId"), pattern = str("location-path", row) || "/";
summary.textContent = `${id} · ${t(`locationMatch_${match.value}`)} · ${pattern} · ${t(isReturn ? "locationReturnShort" : isRedirect ? "locationRedirectShort" : isWeb ? "locationWebServerShort" : "locationProxyShort")}`;
const id = str("location-id", row) || t("locationId"), pattern = internal.checked ? `@${id}` : (str("location-path", row) || "/");
summary.textContent = `${id} · ${internal.checked ? t("locationInternal") : t(`locationMatch_${match.value}`)} · ${pattern} · ${t(isReturn ? "locationReturnShort" : isRedirect ? "locationRedirectShort" : isWeb ? "locationWebServerShort" : "locationProxyShort")}`;
previewStatus.hidden = yes("location-enabled", row);
previewValues.match.textContent = `${t(`locationMatch_${match.value}`)} · ${pattern}`;
+7 -2
View File
@@ -257,7 +257,11 @@
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 });
g = group("vhLocations"); locationsReader = locationsEditor(g, value.routes || []);
g = group("vhLocations");
field(g, "main-error-target", "vhErrorFallbackTarget", value.error_fallback?.target || "", "text", { maxLength: 64, pattern: "[A-Za-z0-9_-]{0,64}" });
field(g, "main-error-statuses", "vhErrorFallbackStatuses", (value.error_fallback?.statuses || [404]).join(", "), "text", { maxLength: 64 });
note(g, "vhErrorFallbackHelp");
locationsReader = locationsEditor(g, value.routes || []);
g = group("vhSsl");
const selector = field(g, "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();
@@ -386,6 +390,7 @@
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 bindingSelection = readBindings();
const mainSourceType = str("source-type");
const mainFallbackTarget = str("main-error-target"), mainFallbackStatuses = lines(str("main-error-statuses")).map(value => Number(value));
const needsProxy = mainSourceType === "reverse_proxy" || paths.some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy");
if (!bindingSelection.length) throw new Error(t("vhListenerRequired"));
if (needsProxy && !members.length) throw new Error(t("vhBackendRequired"));
@@ -419,7 +424,7 @@
if (JSON.stringify(selected) !== JSON.stringify(globalPolicy)) throw new Error(t("vhTlsHttp3Conflict"));
}
return {
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"),
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, error_fallback: mainFallbackTarget ? { target: mainFallbackTarget, statuses: mainFallbackStatuses.length ? mainFallbackStatuses : [404] } : null, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"),
proxy_timeouts: { connect_timeout_seconds: num("proxy-connect-timeout"), response_header_timeout_seconds: num("proxy-header-timeout"), read_timeout_seconds: num("proxy-read-timeout"), send_timeout_seconds: num("proxy-send-timeout") },
rate_limit: { enabled: yes("rate-enabled"), requests: num("rate-requests"), window_seconds: num("rate-window"), burst: num("rate-burst"), country_access: { mode: str("country-mode"), countries: lines(str("country-list")).map(value => value.toUpperCase()) }, country_limits: countryRatesReader() },
method_policy: { mode: str("method-mode"), methods: lines(str("method-list")).map(value => value.toUpperCase()) },
+17 -8
View File
@@ -80,8 +80,10 @@
return result;
}
function validatePhpCustomData(customParams, phpValues) {
const size = [...Object.entries(customParams), ...Object.entries(phpValues)]
function validatePhpCustomData(customParams, phpValues, phpAdminValues = {}) {
// Never allow an admin override to conflict with ordinary PHP_VALUE.
if (Object.keys(phpValues).some(name => Object.hasOwn(phpAdminValues, name))) throw new Error(t("webServicePhpValuesInvalid"));
const size = [...Object.entries(customParams), ...Object.entries(phpValues), ...Object.entries(phpAdminValues)]
.reduce((total, [name, value]) => total + name.length + value.length + 2, 0);
if (size > 128 * 1024) throw new Error(t("webServicePhpCustomDataTooLarge"));
}
@@ -156,8 +158,9 @@
for (const id of [
"web-service-php-endpoint", "web-service-php-extensions", "web-service-php-connect-timeout",
"web-service-php-request-timeout", "web-service-php-concurrency", "web-service-php-pass-authorization",
"web-service-php-path-info",
"web-service-php-body-limit", "web-service-php-header-limit", "web-service-php-stderr-limit",
"web-service-php-custom-params", "web-service-php-values"
"web-service-php-custom-params", "web-service-php-values", "web-service-php-admin-values"
]) $(id).disabled = !enabled;
}
@@ -194,9 +197,9 @@
mime_types: {}, default_mime_type: "application/octet-stream", cache_control: null, cache_rules: [], response_headers: {},
try_files: [], rewrites: [], compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, precompressed: { enabled: false, brotli: true, gzip: true }, download_extensions: [],
symlink_policy: "deny", php: {
enabled: false, extensions: ["php"], endpoint: "unix:/run/php/php-fpm.sock", connect_timeout_ms: 1000,
enabled: false, path_info: false, extensions: ["php"], endpoint: "unix:/run/php/php-fpm.sock", connect_timeout_ms: 1000,
request_timeout_ms: 30000, max_concurrent_requests: 64, pass_authorization: true, max_request_body_bytes: 0,
max_response_header_bytes: 65536, stderr_log_bytes: 4096, custom_params: {}, php_values: {}
max_response_header_bytes: 65536, stderr_log_bytes: 4096, custom_params: {}, php_values: {}, php_admin_values: {}
},
htaccess: { enabled: false, filename: ".htaccess", scan_subdirectories: true, strict: false },
hardening: { block_common_attacks: false, strip_unnecessary_headers: false }
@@ -241,6 +244,7 @@
$("web-service-precompressed-gzip").checked = item.precompressed?.gzip !== false;
$("web-service-download-extensions").value = (item.download_extensions || []).join("\n");
$("web-service-php-enabled").checked = item.php?.enabled === true;
$("web-service-php-path-info").checked = item.php?.path_info === true;
$("web-service-php-endpoint").value = item.php?.endpoint || "unix:/run/php/php-fpm.sock";
$("web-service-php-extensions").value = (item.php?.extensions || ["php"]).join(", ");
$("web-service-php-connect-timeout").value = item.php?.connect_timeout_ms ?? 1000;
@@ -252,6 +256,7 @@
$("web-service-php-stderr-limit").value = item.php?.stderr_log_bytes ?? 4096;
$("web-service-php-custom-params").value = mapLines(item.php?.custom_params);
$("web-service-php-values").value = mapLines(item.php?.php_values);
$("web-service-php-admin-values").value = mapLines(item.php?.php_admin_values);
$("web-service-symlink-policy").value = item.symlink_policy || "deny";
$("web-service-htaccess-enabled").checked = item.htaccess?.enabled === true;
$("web-service-htaccess-filename").value = item.htaccess?.filename || ".htaccess";
@@ -280,12 +285,14 @@
preset.php.request_timeout_ms = Number($("web-service-php-request-timeout").value || preset.php.request_timeout_ms);
preset.php.max_concurrent_requests = Number($("web-service-php-concurrency").value || preset.php.max_concurrent_requests);
preset.php.pass_authorization = $("web-service-php-pass-authorization").checked;
preset.php.path_info = $("web-service-php-path-info").checked;
preset.php.max_request_body_bytes = Number($("web-service-php-body-limit").value || 0);
preset.php.max_response_header_bytes = Number($("web-service-php-header-limit").value || preset.php.max_response_header_bytes);
preset.php.stderr_log_bytes = Number($("web-service-php-stderr-limit").value || 0);
preset.php.custom_params = parseFastCgiParams($("web-service-php-custom-params").value);
preset.php.php_values = parsePhpValues($("web-service-php-values").value);
validatePhpCustomData(preset.php.custom_params, preset.php.php_values);
preset.php.php_admin_values = parsePhpValues($("web-service-php-admin-values").value);
validatePhpCustomData(preset.php.custom_params, preset.php.php_values, preset.php.php_admin_values);
preset.hardening.block_common_attacks = $("web-service-hardening-attacks").checked;
preset.hardening.strip_unnecessary_headers = $("web-service-hardening-headers").checked;
populateSettings(preset);
@@ -423,6 +430,7 @@
},
php: {
enabled: phpEnabled,
path_info: $("web-service-php-path-info").checked,
extensions: lines($("web-service-php-extensions").value).map(value => value.replace(/^\./, "").toLowerCase()),
endpoint: $("web-service-php-endpoint").value.trim() || "unix:/run/php/php-fpm.sock",
connect_timeout_ms: Number($("web-service-php-connect-timeout").value || 1000),
@@ -433,10 +441,11 @@
max_response_header_bytes: Number($("web-service-php-header-limit").value || 65536),
stderr_log_bytes: Number($("web-service-php-stderr-limit").value || 0),
custom_params: parseFastCgiParams($("web-service-php-custom-params").value),
php_values: parsePhpValues($("web-service-php-values").value)
php_values: parsePhpValues($("web-service-php-values").value),
php_admin_values: parsePhpValues($("web-service-php-admin-values").value)
}
};
validatePhpCustomData(item.php.custom_params, item.php.php_values);
validatePhpCustomData(item.php.custom_params, item.php.php_values, item.php.php_admin_values);
if (phpEnabled && item.symlink_policy !== "deny") throw new Error(t("webServicePhpSymlinkConflict"));
const next = structuredClone(envelope.config.web_services || []);
const duplicate = next.find(service => service.id === item.id && service.id !== editing);