v0.25.0
This commit is contained in:
1 parent
2e4aa4df4d
commit
87e571c170
106 files changed
+17952
-6185
No files matched your search
@@ -0,0 +1,438 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let editing = null;
|
||||
const form = $("web-service-form"), dialog = $("web-service-dialog");
|
||||
const lines = value => value.split(/[\n,]+/).map(item => item.trim()).filter(Boolean);
|
||||
const strictLines = value => value.split(/\n+/).map(item => item.trim()).filter(Boolean);
|
||||
const mimeLines = value => Object.entries(value || {}).map(([extension, mime]) => `${extension} = ${mime}`).join("\n");
|
||||
const mapLines = value => Object.entries(value || {}).map(([name, item]) => `${name} = ${item}`).join("\n");
|
||||
const cacheLines = value => (value || []).map(rule => `${rule.matcher} = ${rule.cache_control}`).join("\n");
|
||||
const rewriteLines = value => (value || []).map(rule => `${rule.pattern} => ${rule.status ? `${rule.status}:` : ""}${rule.replacement}`).join("\n");
|
||||
const staticCacheRules = () => [
|
||||
{ matcher: "ext:html", cache_control: "no-cache" },
|
||||
{ matcher: "ext:htm", cache_control: "no-cache" },
|
||||
{ matcher: "ext:css", cache_control: "public, max-age=604800" },
|
||||
{ matcher: "ext:js", cache_control: "public, max-age=604800" },
|
||||
{ matcher: "ext:mjs", cache_control: "public, max-age=604800" },
|
||||
{ matcher: "mime:image/", cache_control: "public, max-age=2592000" },
|
||||
{ matcher: "mime:font/", cache_control: "public, max-age=2592000" },
|
||||
];
|
||||
const phpFrontController = () => ["$uri", "$uri/", "/index.php?$query_string"];
|
||||
const presetHints = {
|
||||
static: "webServicePresetHintStatic", spa: "webServicePresetHintSpa", php_generic: "webServicePresetHintPhpGeneric",
|
||||
wordpress: "webServicePresetHintWordpress", drupal: "webServicePresetHintDrupal", laravel: "webServicePresetHintLaravel",
|
||||
symfony: "webServicePresetHintSymfony", joomla: "webServicePresetHintJoomla", prestashop: "webServicePresetHintPrestashop",
|
||||
downloads: "webServicePresetHintDownloads"
|
||||
};
|
||||
|
||||
function parseMimeTypes(value) {
|
||||
const result = {};
|
||||
for (const line of strictLines(value)) {
|
||||
const match = line.match(/^\.?([A-Za-z0-9_-]{1,32})\s*=\s*(.+)$/);
|
||||
if (!match) throw new Error(t("webServiceMimeInvalid"));
|
||||
const extension = match[1].toLowerCase(), mime = match[2].trim();
|
||||
if (!mime || Object.hasOwn(result, extension)) throw new Error(t("webServiceMimeInvalid"));
|
||||
result[extension] = mime;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseKeyValueLines(value, errorKey, normalizeKey = key => key) {
|
||||
const result = {};
|
||||
for (const line of strictLines(value)) {
|
||||
const split = line.indexOf("=");
|
||||
if (split <= 0) throw new Error(t(errorKey));
|
||||
const key = normalizeKey(line.slice(0, split).trim()), item = line.slice(split + 1).trim();
|
||||
if (!key || !item || Object.hasOwn(result, key)) throw new Error(t(errorKey));
|
||||
result[key] = item;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const reservedFastCgiParams = new Set([
|
||||
"REQUEST_METHOD", "SCRIPT_FILENAME", "SCRIPT_NAME", "QUERY_STRING", "REQUEST_URI", "DOCUMENT_ROOT",
|
||||
"SERVER_PROTOCOL", "SERVER_NAME", "SERVER_PORT", "REMOTE_ADDR", "REMOTE_PORT", "HTTPS",
|
||||
"GATEWAY_INTERFACE", "SERVER_SOFTWARE", "REDIRECT_STATUS", "CONTENT_TYPE", "CONTENT_LENGTH",
|
||||
"PATH_INFO", "PATH_TRANSLATED", "PHP_VALUE", "PHP_ADMIN_VALUE"
|
||||
]);
|
||||
|
||||
function parseFastCgiParams(value) {
|
||||
const result = parseKeyValueLines(value, "webServicePhpCustomParamsInvalid", key => key.toUpperCase());
|
||||
const entries = Object.entries(result);
|
||||
if (entries.length > 64) throw new Error(t("webServicePhpCustomParamsInvalid"));
|
||||
for (const [name, item] of entries) {
|
||||
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(name) || name.startsWith("HTTP_") || reservedFastCgiParams.has(name) || item.length > 8192 || /[\x00-\x1f\x7f]/.test(item)) {
|
||||
throw new Error(t("webServicePhpCustomParamsInvalid"));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parsePhpValues(value) {
|
||||
const result = parseKeyValueLines(value, "webServicePhpValuesInvalid", key => key.toLowerCase());
|
||||
const entries = Object.entries(result);
|
||||
if (entries.length > 64) throw new Error(t("webServicePhpValuesInvalid"));
|
||||
for (const [name, item] of entries) {
|
||||
if (!/^[a-z0-9_.-]{1,64}$/.test(name) || item.length > 8192 || /[\x00-\x1f\x7f]/.test(item)) {
|
||||
throw new Error(t("webServicePhpValuesInvalid"));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function validatePhpCustomData(customParams, phpValues) {
|
||||
const size = [...Object.entries(customParams), ...Object.entries(phpValues)]
|
||||
.reduce((total, [name, value]) => total + name.length + value.length + 2, 0);
|
||||
if (size > 128 * 1024) throw new Error(t("webServicePhpCustomDataTooLarge"));
|
||||
}
|
||||
|
||||
function parseCacheRules(value) {
|
||||
const result = [];
|
||||
for (const line of strictLines(value)) {
|
||||
const split = line.indexOf("=");
|
||||
if (split <= 0) throw new Error(t("webServiceCacheRulesInvalid"));
|
||||
const matcher = line.slice(0, split).trim().toLowerCase(), cache_control = line.slice(split + 1).trim();
|
||||
if (!/^(?:ext:[A-Za-z0-9_-]{1,32}|mime:.+)$/.test(matcher) || !cache_control) throw new Error(t("webServiceCacheRulesInvalid"));
|
||||
result.push({ matcher, cache_control });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseRewrites(value) {
|
||||
const result = [];
|
||||
for (const line of strictLines(value)) {
|
||||
const split = line.indexOf("=>");
|
||||
if (split <= 0) throw new Error(t("webServiceRewritesInvalid"));
|
||||
const pattern = line.slice(0, split).trim();
|
||||
let replacement = line.slice(split + 2).trim(), status = null;
|
||||
const redirect = replacement.match(/^(301|302|307|308):(\/.*)$/);
|
||||
if (redirect) { status = Number(redirect[1]); replacement = redirect[2]; }
|
||||
if (!pattern || !replacement.startsWith("/")) throw new Error(t("webServiceRewritesInvalid"));
|
||||
result.push({ pattern, replacement, status });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const tabDefinitions = [
|
||||
["general", "webServiceTabGeneral"],
|
||||
["routing", "webServiceTabRouting"],
|
||||
["htaccess", "webServiceTabHtaccess"],
|
||||
["mime", "webServiceTabMime"],
|
||||
["cache", "webServiceTabCacheHeaders"],
|
||||
["delivery", "webServiceTabDelivery"],
|
||||
["php", "webServiceTabPhp"],
|
||||
["security", "webServiceTabSecurity"],
|
||||
];
|
||||
|
||||
function activateTab(name) {
|
||||
document.querySelectorAll("#web-service-tabs button").forEach(button => {
|
||||
const active = button.dataset.tab === name;
|
||||
button.classList.toggle("active", active);
|
||||
button.setAttribute("aria-pressed", String(active));
|
||||
button.setAttribute("aria-selected", String(active));
|
||||
button.tabIndex = active ? 0 : -1;
|
||||
});
|
||||
document.querySelectorAll("[data-web-service-page]").forEach(page => { page.hidden = page.dataset.webServicePage !== name; });
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
const tabs = $("web-service-tabs");
|
||||
tabs.replaceChildren();
|
||||
tabs.setAttribute("role", "tablist");
|
||||
for (const [name, key] of tabDefinitions) {
|
||||
const button = el("button", t(key), "quiet");
|
||||
button.type = "button";
|
||||
button.dataset.tab = name;
|
||||
button.dataset.i18n = key;
|
||||
button.setAttribute("role", "tab");
|
||||
button.addEventListener("click", () => activateTab(name));
|
||||
tabs.append(button);
|
||||
}
|
||||
activateTab("general");
|
||||
}
|
||||
|
||||
function syncPhp() {
|
||||
const enabled = $("web-service-php-enabled").checked;
|
||||
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-body-limit", "web-service-php-header-limit", "web-service-php-stderr-limit",
|
||||
"web-service-php-custom-params", "web-service-php-values"
|
||||
]) $(id).disabled = !enabled;
|
||||
}
|
||||
|
||||
function syncPrecompressed() {
|
||||
const enabled = $("web-service-precompressed").checked;
|
||||
$("web-service-precompressed-br").disabled = !enabled;
|
||||
$("web-service-precompressed-gzip").disabled = !enabled;
|
||||
}
|
||||
|
||||
function syncHtaccess() {
|
||||
const enabled = $("web-service-htaccess-enabled").checked;
|
||||
for (const id of ["web-service-htaccess-filename", "web-service-htaccess-recursive", "web-service-htaccess-strict"]) $(id).disabled = !enabled;
|
||||
}
|
||||
|
||||
const sourceId = source => source?.type === "web_server" ? source.web_service_id : null;
|
||||
function usage(id) {
|
||||
const uses = [];
|
||||
for (const vhost of envelope?.config.vhosts || []) {
|
||||
if (sourceId(vhost.source) === id) uses.push(vhost.id);
|
||||
for (const route of vhost.routes || []) if (!route.response && sourceId(route.source) === id) uses.push(`${vhost.id}/${route.id}`);
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
function defaults() {
|
||||
return {
|
||||
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: [], 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,
|
||||
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: {}
|
||||
},
|
||||
htaccess: { enabled: false, filename: ".htaccess", scan_subdirectories: true, strict: false },
|
||||
hardening: { block_common_attacks: false, strip_unnecessary_headers: false }
|
||||
};
|
||||
}
|
||||
|
||||
function presetConfig(name) {
|
||||
const item = defaults();
|
||||
item.precompressed.enabled = true;
|
||||
item.cache_rules = staticCacheRules();
|
||||
if (name === "static") return item;
|
||||
if (name === "spa") { item.index = ["index.html"]; item.try_files = ["$uri", "/index.html"]; return item; }
|
||||
if (name === "downloads") {
|
||||
item.index = []; item.directory_listing = true; item.cache_control = "public, max-age=3600"; item.cache_rules = [];
|
||||
item.precompressed.enabled = false; item.download_extensions = ["zip", "7z", "tar", "gz", "tgz", "bz2", "xz", "pdf", "iso"]; return item;
|
||||
}
|
||||
item.index = ["index.php", "index.html", "index.htm"];
|
||||
item.php.enabled = true;
|
||||
item.php.extensions = ["php"];
|
||||
if (name === "php_generic") { item.try_files = ["$uri", "$uri/", "=404"]; return item; }
|
||||
if (["wordpress", "drupal", "laravel", "symfony", "joomla", "prestashop"].includes(name)) {
|
||||
item.try_files = phpFrontController();
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function populateSettings(item) {
|
||||
$("web-service-index").value = (item.index || []).join("\n");
|
||||
$("web-service-listing").checked = item.directory_listing === true;
|
||||
$("web-service-try-files").value = (item.try_files || []).join("\n");
|
||||
$("web-service-rewrites").value = rewriteLines(item.rewrites);
|
||||
$("web-service-mime-types").value = mimeLines(item.mime_types);
|
||||
$("web-service-default-mime").value = item.default_mime_type || "application/octet-stream";
|
||||
$("web-service-cache-control").value = item.cache_control || "";
|
||||
$("web-service-cache-rules").value = cacheLines(item.cache_rules);
|
||||
$("web-service-response-headers").value = mapLines(item.response_headers);
|
||||
$("web-service-precompressed").checked = item.precompressed?.enabled === true;
|
||||
$("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");
|
||||
$("web-service-php-enabled").checked = item.php?.enabled === 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;
|
||||
$("web-service-php-request-timeout").value = item.php?.request_timeout_ms ?? 30000;
|
||||
$("web-service-php-concurrency").value = item.php?.max_concurrent_requests ?? 64;
|
||||
$("web-service-php-pass-authorization").checked = item.php?.pass_authorization !== false;
|
||||
$("web-service-php-body-limit").value = item.php?.max_request_body_bytes ?? 0;
|
||||
$("web-service-php-header-limit").value = item.php?.max_response_header_bytes ?? 65536;
|
||||
$("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-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";
|
||||
$("web-service-htaccess-recursive").checked = item.htaccess?.scan_subdirectories !== false;
|
||||
$("web-service-htaccess-strict").checked = item.htaccess?.strict === true;
|
||||
$("web-service-hardening-attacks").checked = item.hardening?.block_common_attacks === true;
|
||||
$("web-service-hardening-headers").checked = item.hardening?.strip_unnecessary_headers === true;
|
||||
syncPhp();
|
||||
syncPrecompressed();
|
||||
syncHtaccess();
|
||||
}
|
||||
|
||||
function updatePresetSummary() {
|
||||
const key = presetHints[$("web-service-preset").value];
|
||||
$("web-service-preset-summary").textContent = key ? t(key) : t("webServicePresetHelp");
|
||||
}
|
||||
|
||||
function applyPreset() {
|
||||
try {
|
||||
const preset = presetConfig($("web-service-preset").value);
|
||||
if (!preset) { toast(t("webServicePresetChoose"), true); return; }
|
||||
// Keep deployment-specific FastCGI endpoint/tuning while replacing application policy.
|
||||
preset.php.endpoint = $("web-service-php-endpoint").value.trim() || preset.php.endpoint;
|
||||
preset.php.connect_timeout_ms = Number($("web-service-php-connect-timeout").value || preset.php.connect_timeout_ms);
|
||||
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.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.hardening.block_common_attacks = $("web-service-hardening-attacks").checked;
|
||||
preset.hardening.strip_unnecessary_headers = $("web-service-hardening-headers").checked;
|
||||
populateSettings(preset);
|
||||
updatePresetSummary();
|
||||
toast(t("webServicePresetApplied"));
|
||||
} catch (error) {
|
||||
toast(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function presetChanged() {
|
||||
updatePresetSummary();
|
||||
// Presets are one-shot form generators. Selecting one must immediately
|
||||
// materialize its normal Web Service fields; otherwise a user can select a
|
||||
// CMS, save the form and unknowingly persist the previous/custom values.
|
||||
if ($("web-service-preset").value !== "custom") applyPreset();
|
||||
}
|
||||
|
||||
function open(value = null) {
|
||||
const item = Object.assign(defaults(), structuredClone(value || {}));
|
||||
item.precompressed = Object.assign(defaults().precompressed, item.precompressed || {});
|
||||
item.php = Object.assign(defaults().php, item.php || {});
|
||||
item.htaccess = Object.assign(defaults().htaccess, item.htaccess || {});
|
||||
item.hardening = Object.assign(defaults().hardening, item.hardening || {});
|
||||
editing = value?.id || null;
|
||||
$("web-service-id").value = item.id || "";
|
||||
$("web-service-id").readOnly = !!editing;
|
||||
$("web-service-name").value = item.name || "";
|
||||
$("web-service-root").value = item.document_root || "";
|
||||
populateSettings(item);
|
||||
$("web-service-preset").value = "custom";
|
||||
updatePresetSummary();
|
||||
$("web-service-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
|
||||
activateTab("general");
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
function close() { editing = null; form.reset(); if (dialog.open) dialog.close(); }
|
||||
|
||||
function render() {
|
||||
if (!envelope) return;
|
||||
const query = ($("web-service-search")?.value || "").trim().toLowerCase();
|
||||
const services = (envelope.config.web_services || []).filter(service => !query || [service.id, service.name, service.document_root].join(" ").toLowerCase().includes(query));
|
||||
const cards = services.map(service => {
|
||||
const box = el("article", undefined, "vhost-card");
|
||||
const head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity");
|
||||
const title = el("div", undefined, "vhost-title-line");
|
||||
title.append(el("strong", service.name || service.id, "entity-title"), el("span", t("webServer"), "pill live"));
|
||||
identity.append(title, el("div", service.document_root, "vhost-hosts"));
|
||||
const actions = el("div", undefined, "entity-actions");
|
||||
const edit = el("button", t("edit"), "quiet"); edit.type = "button"; edit.addEventListener("click", () => open(structuredClone(service)));
|
||||
const remove = el("button", t("remove"), "quiet"); remove.type = "button"; remove.addEventListener("click", async () => {
|
||||
const used = usage(service.id);
|
||||
if (used.length) { toast(`${t("webServiceInUse")}: ${used.join(", ")}`, true); return; }
|
||||
if (!(await appConfirm(t("webServiceDelete"), { danger: true }))) return;
|
||||
const next = (envelope.config.web_services || []).filter(item => item.id !== service.id);
|
||||
if (await saveSection("web-services", next)) render();
|
||||
});
|
||||
actions.append(edit, remove); head.append(identity, actions); box.append(head);
|
||||
const details = el("div", undefined, "vhost-detail-strip");
|
||||
const detailItems = [
|
||||
["webServiceId", service.id],
|
||||
["webServiceIndex", (service.index || []).join(", ") || t("webServiceNoIndex")],
|
||||
["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")],
|
||||
["webServicePrecompressed", t(service.precompressed?.enabled ? "enabled" : "notEnabled")],
|
||||
["webServiceHardeningTitle", t((service.hardening?.block_common_attacks || service.hardening?.strip_unnecessary_headers) ? "enabled" : "notEnabled")],
|
||||
["webServiceCacheControl", service.cache_control || t("inherit")],
|
||||
["webServiceUsedBy", usage(service.id).length ? usage(service.id).join(", ") : t("webServiceUnused")],
|
||||
];
|
||||
for (const [key, value] of detailItems) {
|
||||
const item = el("div", undefined, "vhost-detail");
|
||||
item.append(el("span", t(key), "small-label"), el("b", value));
|
||||
details.append(item);
|
||||
}
|
||||
box.append(details); return box;
|
||||
});
|
||||
$("web-service-list").replaceChildren(...(cards.length ? cards : [el("p", t(query ? "webServiceSearchEmpty" : "webServiceEmpty"), "fine")]));
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault(); if (!envelope || busy) return;
|
||||
const invalid = [...form.elements].find(input => input.willValidate && !input.checkValidity()); if (invalid) { activateTab(invalid.closest("[data-web-service-page]")?.dataset.webServicePage || "general"); invalid.reportValidity(); return; }
|
||||
try {
|
||||
const phpEnabled = $("web-service-php-enabled").checked;
|
||||
const item = {
|
||||
id: $("web-service-id").value.trim(),
|
||||
name: $("web-service-name").value.trim(),
|
||||
document_root: $("web-service-root").value.trim(),
|
||||
index: lines($("web-service-index").value),
|
||||
directory_listing: $("web-service-listing").checked,
|
||||
mime_types: parseMimeTypes($("web-service-mime-types").value),
|
||||
default_mime_type: $("web-service-default-mime").value.trim() || "application/octet-stream",
|
||||
cache_control: $("web-service-cache-control").value.trim() || null,
|
||||
cache_rules: parseCacheRules($("web-service-cache-rules").value),
|
||||
response_headers: parseKeyValueLines($("web-service-response-headers").value, "webServiceResponseHeadersInvalid", key => key.toLowerCase()),
|
||||
try_files: strictLines($("web-service-try-files").value),
|
||||
rewrites: parseRewrites($("web-service-rewrites").value),
|
||||
precompressed: { enabled: $("web-service-precompressed").checked, brotli: $("web-service-precompressed-br").checked, gzip: $("web-service-precompressed-gzip").checked },
|
||||
download_extensions: lines($("web-service-download-extensions").value).map(value => value.replace(/^\./, "").toLowerCase()),
|
||||
symlink_policy: $("web-service-symlink-policy").value,
|
||||
htaccess: {
|
||||
enabled: $("web-service-htaccess-enabled").checked,
|
||||
filename: $("web-service-htaccess-filename").value.trim() || ".htaccess",
|
||||
scan_subdirectories: $("web-service-htaccess-recursive").checked,
|
||||
strict: $("web-service-htaccess-strict").checked
|
||||
},
|
||||
hardening: {
|
||||
block_common_attacks: $("web-service-hardening-attacks").checked,
|
||||
strip_unnecessary_headers: $("web-service-hardening-headers").checked
|
||||
},
|
||||
php: {
|
||||
enabled: phpEnabled,
|
||||
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),
|
||||
request_timeout_ms: Number($("web-service-php-request-timeout").value || 30000),
|
||||
max_concurrent_requests: Number($("web-service-php-concurrency").value || 64),
|
||||
pass_authorization: $("web-service-php-pass-authorization").checked,
|
||||
max_request_body_bytes: Number($("web-service-php-body-limit").value || 0),
|
||||
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)
|
||||
}
|
||||
};
|
||||
validatePhpCustomData(item.php.custom_params, item.php.php_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);
|
||||
if (duplicate) { toast(t("webServiceDuplicate"), true); return; }
|
||||
const position = next.findIndex(service => service.id === editing);
|
||||
if (position >= 0) next[position] = item; else next.push(item);
|
||||
if (await saveSection("web-services", next)) { close(); render(); }
|
||||
} catch (error) {
|
||||
toast(error.message, true);
|
||||
}
|
||||
});
|
||||
|
||||
initTabs();
|
||||
$("web-service-php-enabled").addEventListener("change", syncPhp);
|
||||
$("web-service-precompressed").addEventListener("change", syncPrecompressed);
|
||||
$("web-service-htaccess-enabled").addEventListener("change", syncHtaccess);
|
||||
$("web-service-preset").addEventListener("change", presetChanged);
|
||||
$("web-service-preset-apply").addEventListener("click", applyPreset);
|
||||
$("web-service-htaccess-help").addEventListener("click", () => { const help = $("htaccess-help-dialog"); if (!help.open) help.showModal(); });
|
||||
$("htaccess-help-close").addEventListener("click", () => $("htaccess-help-dialog").close());
|
||||
$("web-service-php-help").addEventListener("click", () => { const help = $("php-help-dialog"); if (!help.open) help.showModal(); });
|
||||
$("php-help-close").addEventListener("click", () => $("php-help-dialog").close());
|
||||
$("web-service-new").addEventListener("click", () => open());
|
||||
$("web-service-close").addEventListener("click", close);
|
||||
$("web-service-reset").addEventListener("click", close);
|
||||
$("web-service-search").addEventListener("input", render);
|
||||
dialog.addEventListener("cancel", event => { event.preventDefault(); close(); });
|
||||
window.rustProxyWebServer = { render, reset: close };
|
||||
if (envelope) render();
|
||||
})();
|
||||
Reference in new issue
Block a user