v0.29.4
This commit is contained in:
1 parent
f87eec7287
commit
dd8c195600
38 files changed
+5491
-4682
No files matched your search
@@ -89,6 +89,10 @@ async fn update_section(
|
||||
config.web_services =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
}
|
||||
"php-fpm-profiles" => {
|
||||
config.php_fpm_profiles =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
}
|
||||
"redirects" => {
|
||||
config.redirects =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
|
||||
+39
-1
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.29.3",
|
||||
"version": "0.29.4",
|
||||
"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": [
|
||||
@@ -848,6 +848,14 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/WebService"
|
||||
}
|
||||
},
|
||||
"php_fpm_profiles": {
|
||||
"type": "array",
|
||||
"maxItems": 128,
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PhpFpmProfile"
|
||||
},
|
||||
"description": "Named PHP-FPM configurations reusable by Web Services."
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
@@ -5185,6 +5193,12 @@
|
||||
},
|
||||
"compression": {
|
||||
"$ref": "#/components/schemas/CompressionOverrides"
|
||||
},
|
||||
"php_profile_id": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"description": "Optional shared PHP-FPM profile ID; when absent the local php configuration applies.",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -5649,6 +5663,30 @@
|
||||
"target"
|
||||
],
|
||||
"description": "Nginx-style internal error fallback. The original request is internally dispatched without a client redirect."
|
||||
},
|
||||
"PhpFpmProfile": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 64,
|
||||
"description": "ASCII letters, numbers, - and _."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 96
|
||||
},
|
||||
"php": {
|
||||
"$ref": "#/components/schemas/WebService/properties/php"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -230,6 +230,10 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route("/assets/web-server-profiles.js", get(|| async {
|
||||
([ ("content-type", "text/javascript; charset=utf-8") ],
|
||||
include_str!("web/web-server-profiles.js"))
|
||||
}))
|
||||
.route(
|
||||
"/assets/web-server.js",
|
||||
get(|| async {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script defer="" src="/assets/drafts.js"></script>
|
||||
<script defer="" src="/assets/integrations.js"></script>
|
||||
<script defer="" src="/assets/vhosts.js"></script>
|
||||
<script defer="" src="/assets/web-server-profiles.js"></script>
|
||||
<script defer="" src="/assets/web-server.js"></script>
|
||||
<script defer="" src="/assets/docker.js"></script>
|
||||
<script defer="" src="/assets/streams.js"></script>
|
||||
@@ -684,6 +685,8 @@
|
||||
<input autocomplete="off" data-i18n-aria="webServiceSearch"
|
||||
data-i18n-placeholder="webServiceSearchPlaceholder" id="web-service-search"
|
||||
type="search" />
|
||||
<button class="outline" data-i18n="phpProfileToolbar" id="web-service-php-profiles-open"
|
||||
type="button">PHP-FPM profiles</button>
|
||||
<button class="outline" data-i18n="webServiceNew" id="web-service-new"
|
||||
type="button">New Web Service</button>
|
||||
</div>
|
||||
@@ -3271,8 +3274,7 @@
|
||||
placeholder="X-Content-Type-Options = nosniff"></textarea><small class="fine"
|
||||
data-i18n="webServiceResponseHeadersHelp">Fixed headers for this service. Vhost/location
|
||||
response-header rules are applied later and can override them.</small></label>
|
||||
<p class="fine vh-wide" data-i18n="webServiceCacheHelp">Local files use filesystem/OS page cache;
|
||||
this controls browser/proxy caching and does not create a second RustProxy object cache.</p>
|
||||
<p class="fine vh-wide" data-i18n="webServiceCacheHelp">With vhost cache enabled, eligible Web Server responses also use the RustProxy object cache. Private, cookie-bearing and no-store responses are never stored.</p>
|
||||
</div>
|
||||
<div class="vh-grid web-service-page" data-web-service-page="delivery" hidden>
|
||||
<label><span data-i18n="webServiceCompression">Dynamic compression</span><select
|
||||
@@ -3323,6 +3325,11 @@
|
||||
keep validators, single-range requests, 206/416 and streaming delivery.</p>
|
||||
</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>
|
||||
<label class="check"><input id="web-service-php-enabled" type="checkbox" /><span
|
||||
data-i18n="webServicePhpEnabled">Enable PHP-FPM / FastCGI</span></label>
|
||||
<label class="vh-wide"><span data-i18n="webServicePhpEndpoint">PHP-FPM endpoint</span><input
|
||||
@@ -3394,6 +3401,17 @@
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="php-profiles-title" class="vhost-dialog location-help-dialog" id="php-fpm-profiles-dialog">
|
||||
<div class="window-heading">
|
||||
<h2 id="php-profiles-title" data-i18n="phpProfileTitle">Shared PHP-FPM profiles</h2>
|
||||
<button type="button" class="outline" id="php-fpm-profiles-close" data-i18n="close">Close</button>
|
||||
</div>
|
||||
<div class="card location-help-content">
|
||||
<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>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="htaccess-help-title" class="vhost-dialog location-help-dialog" id="htaccess-help-dialog">
|
||||
<div class="window-heading">
|
||||
<h2 data-i18n="webServiceHtaccessDocsTitle" id="htaccess-help-title">.htaccess compatibility</h2>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"use strict";
|
||||
// Shared profile editor is independent of the Web Service editor. Values are
|
||||
// never rendered as HTML and saves use the revisioned configuration API.
|
||||
window.rustProxyPhpProfiles = ({ defaults, mapLines, lines, parseFastCgiParams, parsePhpValues, validatePhpCustomData }) => {
|
||||
function refreshProfileSelect(chosen = $("web-service-php-profile").value) {
|
||||
const select = $("web-service-php-profile");
|
||||
const local = el("option", t("phpProfileLocal")); local.value = "";
|
||||
const profiles = (envelope?.config.php_fpm_profiles || []).map(profile => {
|
||||
const option = el("option", profile.name || profile.id); option.value = profile.id; return option;
|
||||
});
|
||||
select.replaceChildren(local, ...profiles);
|
||||
select.value = chosen;
|
||||
}
|
||||
|
||||
// Use exactly the same validators and FastCGI field semantics for local and
|
||||
// shared configuration. None of the profile values are interpolated as HTML.
|
||||
const profileFields = [
|
||||
["enabled", "webServicePhpEnabled", "checkbox"],
|
||||
["endpoint", "webServicePhpEndpoint", "text"],
|
||||
["extensions", "webServicePhpExtensions", "text"],
|
||||
["path_info", "webServicePhpPathInfo", "checkbox"],
|
||||
["pass_authorization", "webServicePhpPassAuthorization", "checkbox"],
|
||||
["connect_timeout_ms", "webServicePhpConnectTimeout", "number", 50, 60000],
|
||||
["request_timeout_ms", "webServicePhpRequestTimeout", "number", 100, 3600000],
|
||||
["max_concurrent_requests", "webServicePhpConcurrency", "number", 1, 4096],
|
||||
["max_request_body_bytes", "webServicePhpBodyLimit", "number", 0, 8589934592],
|
||||
["max_response_header_bytes", "webServicePhpHeaderLimit", "number", 4096, 1048576],
|
||||
["stderr_log_bytes", "webServicePhpStderrLimit", "number", 0, 65536],
|
||||
["custom_params", "webServicePhpCustomParams", "textarea"],
|
||||
["php_values", "webServicePhpValues", "textarea"],
|
||||
["php_admin_values", "phpProfileAdminValues", "textarea"],
|
||||
];
|
||||
|
||||
function profileEditor(profile, creating = false) {
|
||||
const details = document.createElement("details"); details.className = "card";
|
||||
if (creating) details.open = true;
|
||||
const summary = el("summary", profile.name || profile.id || t("phpProfileNew"));
|
||||
const editor = document.createElement("form"); editor.className = "vh-grid"; editor.noValidate = true;
|
||||
const namedInput = (key, value, readonly = false) => {
|
||||
const wrapper = el("label");
|
||||
wrapper.append(el("span", t(key)));
|
||||
const input = document.createElement("input"); input.value = value || ""; input.maxLength = key === "phpProfileId" ? 64 : 96;
|
||||
input.required = true; input.readOnly = readonly; wrapper.append(input); editor.append(wrapper); return input;
|
||||
};
|
||||
const idInput = namedInput("phpProfileId", profile.id, !creating);
|
||||
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 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]); }
|
||||
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;
|
||||
}
|
||||
const actions = el("div", undefined, "form-actions vh-wide");
|
||||
const save = el("button", t("apply"), "primary"); save.type = "submit";
|
||||
actions.append(save);
|
||||
if (!creating) {
|
||||
const remove = el("button", t("remove"), "quiet"); remove.type = "button";
|
||||
remove.addEventListener("click", async () => {
|
||||
const used = (envelope?.config.web_services || []).filter(service => service.php_profile_id === profile.id).map(service => service.id);
|
||||
if (used.length) return toast(`${t("phpProfileInUse")}: ${used.join(", ")}`, true);
|
||||
if (!(await appConfirm(t("phpProfileDelete"), { danger: true }))) return;
|
||||
const next = (envelope.config.php_fpm_profiles || []).filter(item => item.id !== profile.id);
|
||||
if (await saveSection("php-fpm-profiles", next)) { refreshProfileSelect(); renderProfiles(); }
|
||||
});
|
||||
actions.append(remove);
|
||||
}
|
||||
editor.append(actions);
|
||||
editor.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const id = idInput.value.trim(), name = nameInput.value.trim();
|
||||
if (!/^[A-Za-z0-9_-]{1,64}$/.test(id) || !name || name.length > 96) throw new Error(t("phpProfileInvalid"));
|
||||
for (const input of Object.values(controls)) if (!input.checkValidity()) throw new Error(t("phpProfileInvalid"));
|
||||
const php = Object.fromEntries(profileFields.map(([key, , kind]) => [key,
|
||||
kind === "checkbox" ? controls[key].checked : kind === "number" ? Number(controls[key].value) :
|
||||
key === "extensions" ? lines(controls[key].value).map(item => item.replace(/^\./, "").toLowerCase()) :
|
||||
key === "custom_params" ? parseFastCgiParams(controls[key].value) :
|
||||
key === "php_values" || key === "php_admin_values" ? parsePhpValues(controls[key].value) : controls[key].value.trim()
|
||||
]));
|
||||
validatePhpCustomData(php.custom_params, php.php_values, php.php_admin_values);
|
||||
const next = structuredClone(envelope.config.php_fpm_profiles || []);
|
||||
if (next.some(item => item.id === id && (creating || item.id !== profile.id))) throw new Error(t("phpProfileDuplicate"));
|
||||
const position = next.findIndex(item => item.id === profile.id);
|
||||
if (!creating && position >= 0) next[position] = { id, name, php }; else next.push({ id, name, php });
|
||||
if (await saveSection("php-fpm-profiles", next)) { refreshProfileSelect(); renderProfiles(); }
|
||||
} catch (error) { toast(error.message, true); }
|
||||
});
|
||||
details.append(summary, editor);
|
||||
return details;
|
||||
}
|
||||
|
||||
function renderProfiles(addNew = false) {
|
||||
const profiles = envelope?.config.php_fpm_profiles || [];
|
||||
const list = profiles.map(item => profileEditor(item));
|
||||
if (addNew) list.unshift(profileEditor({ id: "", name: "", php: defaults().php }, true));
|
||||
$("php-fpm-profiles-list").replaceChildren(...list);
|
||||
}
|
||||
function openProfiles() {
|
||||
renderProfiles();
|
||||
$("php-fpm-profiles-dialog").showModal();
|
||||
}
|
||||
|
||||
return { renderProfiles, openProfiles, refreshProfileSelect };
|
||||
};
|
||||
@@ -153,15 +153,22 @@
|
||||
activateTab("general");
|
||||
}
|
||||
|
||||
const { renderProfiles, openProfiles, refreshProfileSelect } = window.rustProxyPhpProfiles({
|
||||
defaults, mapLines, lines, parseFastCgiParams, parsePhpValues, validatePhpCustomData
|
||||
});
|
||||
|
||||
function syncPhp() {
|
||||
const attached = !!$("web-service-php-profile").value;
|
||||
const enabled = $("web-service-php-enabled").checked;
|
||||
$("web-service-php-enabled").disabled = attached;
|
||||
|
||||
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-admin-values"
|
||||
]) $(id).disabled = !enabled;
|
||||
]) $(id).disabled = !enabled || attached;
|
||||
}
|
||||
|
||||
function syncCompression() {
|
||||
@@ -196,7 +203,7 @@
|
||||
id: "", name: "", document_root: "", index: ["index.html", "index.htm"], directory_listing: false,
|
||||
mime_types: {}, default_mime_type: "application/octet-stream", cache_control: null, cache_rules: [], response_headers: {},
|
||||
try_files: [], rewrites: [], compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, precompressed: { enabled: false, brotli: true, gzip: true }, download_extensions: [],
|
||||
symlink_policy: "deny", php: {
|
||||
symlink_policy: "deny", php_profile_id: null, php: {
|
||||
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: {}, php_admin_values: {}
|
||||
@@ -243,6 +250,7 @@
|
||||
$("web-service-precompressed-br").checked = item.precompressed?.brotli !== false;
|
||||
$("web-service-precompressed-gzip").checked = item.precompressed?.gzip !== false;
|
||||
$("web-service-download-extensions").value = (item.download_extensions || []).join("\n");
|
||||
refreshProfileSelect(item.php_profile_id || "");
|
||||
$("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";
|
||||
@@ -373,7 +381,7 @@
|
||||
["webServiceDirectoryListing", t(service.directory_listing ? "enabled" : "notEnabled")],
|
||||
["webServiceTryFiles", (service.try_files || []).length ? String(service.try_files.length) : "-"],
|
||||
["webServiceHtaccessEnabled", t(service.htaccess?.enabled ? "enabled" : "notEnabled")],
|
||||
["webServicePhpEnabled", t(service.php?.enabled ? "enabled" : "notEnabled")],
|
||||
["webServicePhpEnabled", service.php_profile_id ? `${service.php_profile_id} (${t("phpProfileShared")})` : t(service.php?.enabled ? "enabled" : "notEnabled")],
|
||||
["webServiceCompression", service.compression?.enabled === true ? t("enabled") : service.compression?.enabled === false ? t("notEnabled") : t("inherit")],
|
||||
["webServicePrecompressed", t(service.precompressed?.enabled ? "enabled" : "notEnabled")],
|
||||
["webServiceHardeningTitle", t((service.hardening?.block_common_attacks || service.hardening?.strip_unnecessary_headers) ? "enabled" : "notEnabled")],
|
||||
@@ -428,6 +436,7 @@
|
||||
block_common_attacks: $("web-service-hardening-attacks").checked,
|
||||
strip_unnecessary_headers: $("web-service-hardening-headers").checked
|
||||
},
|
||||
php_profile_id: $("web-service-php-profile").value || null,
|
||||
php: {
|
||||
enabled: phpEnabled,
|
||||
path_info: $("web-service-php-path-info").checked,
|
||||
@@ -446,7 +455,7 @@
|
||||
}
|
||||
};
|
||||
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"));
|
||||
if ((phpEnabled || item.php_profile_id && (envelope.config.php_fpm_profiles || []).find(profile => profile.id === item.php_profile_id)?.php?.enabled) && 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);
|
||||
if (duplicate) { toast(t("webServiceDuplicate"), true); return; }
|
||||
@@ -462,6 +471,11 @@
|
||||
form.addEventListener("input", () => { dirty = true; });
|
||||
form.addEventListener("change", () => { dirty = true; });
|
||||
$("web-service-php-enabled").addEventListener("change", syncPhp);
|
||||
$("web-service-php-profile").addEventListener("change", syncPhp);
|
||||
$("web-service-php-profiles").addEventListener("click", openProfiles);
|
||||
$("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));
|
||||
$("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