448 lines
45 KiB
JavaScript
448 lines
45 KiB
JavaScript
function basicAuthEditor(parent, settings = {}) {
|
||
const realm = field(parent, "basic-realm", "basicRealm", settings.realm || "Restricted", "text", { maxLength: 128 });
|
||
const secure = field(parent, "basic-tls", "basicRequireTls", settings.require_tls !== false, "checkbox"), forward = field(parent, "basic-forward", "basicForward", settings.forward_credentials, "checkbox");
|
||
const rows = el("div", undefined, "vh-wide basic-users"); parent.append(rows); let sequence = 0;
|
||
function add(user = {}) {
|
||
if (rows.children.length >= 64) { toast(t("basic.too_many_users"), true); return; } const row = el("div", undefined, "basic-user");
|
||
const username = field(row, "basic-username", "username", user.username || "", "text", { maxLength: 128 });
|
||
const password = field(row, "basic-password", "basicPassword", "", "password", { maxLength: 128 }); password.autocomplete = "new-password"; password.placeholder = user.password_hash ? t("basicKeepPassword") : t("basicPassword");
|
||
const state = el("span", user.password_hash ? t("basicHashSaved") : t("basicHashRequired"), "fine"); row.append(state, button("remove", () => { row.remove(); changed(); })); row.userHash = user.password_hash || ""; row.sequence = ++sequence; rows.append(row);
|
||
row.prepare = async () => { if (!password.value) { if (!row.userHash) throw new Error(t("basicHashRequired")); return; } const name = username.value.trim(), secret = password.value, epoch = draftEpoch; const result = await api("/api/basic-auth/hash", { method: "POST", body: JSON.stringify({ username: name, password: secret }) }); if (epoch !== draftEpoch || !row.isConnected || password.value !== secret || username.value.trim() !== name) throw new Error(t("vhUnsaved")); row.userHash = result.password_hash; password.value = ""; password.placeholder = t("basicKeepPassword"); state.textContent = t("basicHashSaved"); };
|
||
}
|
||
for (const user of settings.users || []) add(user); parent.append(button("basicAddUser", () => { add(); changed(); })); note(parent, "basicHelp");
|
||
return { async prepare() { for (const row of rows.children) await row.prepare(); }, read: () => ({ enabled: str("auth-mode") === "basic", realm: realm.value.trim(), require_tls: secure.checked, forward_credentials: forward.checked, users: [...rows.children].map(row => ({ username: str("basic-username", row), password_hash: row.userHash })) }) };
|
||
}
|
||
|
||
function hostTagEditor(parent, hosts = [], onUpdate = () => {}) {
|
||
const wrapper = el("div", undefined, "vh-field vh-wide host-chip-field"); wrapper.append(text("span", "vhHosts"));
|
||
const hidden = document.createElement("textarea"); hidden.name = "hosts"; hidden.hidden = true;
|
||
const box = el("div", undefined, "host-chip-editor"), input = document.createElement("input"); input.type = "text"; input.maxLength = 253; input.placeholder = "app.example.com / *.example.com"; input.setAttribute("aria-label", t("vhHostAdd"));
|
||
const values = [];
|
||
const normalize = raw => {
|
||
let value = raw.trim().toLowerCase().replace(/\.$/, ""); if (!value) return null; const wildcard = value.startsWith("*."); const base = wildcard ? value.slice(2) : value;
|
||
if (base.length > 253 || !/^[a-z0-9.-]+$/.test(base) || base.startsWith(".") || base.endsWith(".") || base.split(".").some(part => !part || part.length > 63 || part.startsWith("-") || part.endsWith("-"))) throw new Error(t("vhHostInvalid"));
|
||
if (wildcard && !base.includes(".")) throw new Error(t("vhHostInvalid")); return wildcard ? `*.${base}` : base;
|
||
};
|
||
const sync = () => { hidden.value = values.join("\n"); onUpdate([...values]); box.querySelectorAll(".host-chip").forEach(node => node.remove()); for (const value of values) { const chip = el("span", undefined, "host-chip"); chip.append(el("span", value)); const remove = el("button", "×"); remove.type = "button"; remove.setAttribute("aria-label", `${t("remove")} ${value}`); remove.addEventListener("click", () => { values.splice(values.indexOf(value), 1); sync(); changed(); }); chip.append(remove); box.insertBefore(chip, input); } };
|
||
const add = raw => { const value = normalize(raw); if (!value || values.includes(value)) return; values.push(value); sync(); changed(); };
|
||
const consume = raw => { for (const part of raw.split(/[\s,]+/).filter(Boolean)) add(part); input.value = ""; };
|
||
input.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === ",") { event.preventDefault(); try { consume(input.value); } catch (error) { toast(error.message, true); } } });
|
||
input.addEventListener("blur", () => { if (!input.value.trim()) return; try { consume(input.value); } catch (error) { toast(error.message, true); } });
|
||
input.addEventListener("paste", event => { const textValue = event.clipboardData?.getData("text") || ""; if (!/[\s,]/.test(textValue.trim())) return; event.preventDefault(); try { consume(textValue); } catch (error) { toast(error.message, true); } });
|
||
box.append(input); wrapper.append(hidden, box, text("p", "vhHostsHelp", "fine")); parent.append(wrapper); for (const host of hosts) { try { add(host); } catch { } } dirty = false;
|
||
return () => { if (input.value.trim()) consume(input.value); if (!values.length) throw new Error(t("vhHostRequired")); return [...values]; };
|
||
}
|
||
|
||
function load(value = defaults(), migrationIds = [], show = true) {
|
||
draftEpoch++; stagedCertificates = []; uploading = false;
|
||
editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true;
|
||
container.replaceChildren(); $("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
|
||
let g = group("vhEditor"); field(g, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(g, "vhIdRenameHelp");
|
||
let refreshCanonical = () => {};
|
||
const enabledControl = field(g, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
|
||
window.rustProxyEnableControl(enabledControl, "vhEnableHint");
|
||
const mainSource = field(g, "source-type", "sourceType", value.source?.type || "reverse_proxy", "select", { values: [["reverse_proxy", "sourceReverseProxy"], ["web_server", "sourceWebServer"]] });
|
||
const mainWebService = field(g, "source-web-service", "webService", value.source?.web_service_id || "", "select"); fillWebServices(mainWebService, value.source?.web_service_id || "");
|
||
const syncMainSource = () => { const web = mainSource.value === "web_server"; mainWebService.disabled = !web; mainWebService.required = web; }; mainSource.addEventListener("change", syncMainSource); syncMainSource();
|
||
note(g, "sourceHelp");
|
||
const websocketEnabled = field(g, "websockets", "vhWebsockets", value.websockets !== false, "checkbox");
|
||
const websocketModeValue = value.websocket_rfc8441 === true && value.websocket_rfc9220 === true ? "h2h3" : value.websocket_rfc8441 === true ? "h2" : value.websocket_rfc9220 === true ? "h3" : "h1";
|
||
const websocketMode = field(g, "websocket-mode", "vhWebsocketMode", websocketModeValue, "select", { values: [["h1", "vhWebsocketH1"], ["h2", "vhWebsocketH2Fallback"], ["h3", "vhWebsocketH3Fallback"], ["h2h3", "vhWebsocketH2H3Fallback"]] });
|
||
note(g, "vhWebsocketTransportHelp");
|
||
const websocketLegacy = field(g, "websocket-legacy-support", "vhWebsocketLegacySupport", value.websocket_legacy_support === true, "checkbox");
|
||
note(g, "vhWebsocketLegacySupportHelp");
|
||
g = group("vhFrontends");
|
||
// Keep the list of names beside the WWW routing control. Every name in
|
||
// frontend.hosts remains an independent alias of this vhost.
|
||
readHosts = hostTagEditor(g, value.frontend?.hosts || [], hosts => refreshCanonical(hosts));
|
||
const bindingControl = window.rustProxyIntegrations.bindings(g, value.frontend?.listener_ids || [], !editing); readBindings = bindingControl.read;
|
||
note(g, "listenerDefaultPortsHint");
|
||
field(g, "force-ssl", "vhForceSsl", value.force_ssl === true, "checkbox");
|
||
note(g, "vhForceSslHelp");
|
||
const wwwPanel = el("section", undefined, "vh-wide vh-www-panel");
|
||
const wwwHeader = el("div", undefined, "vh-www-header");
|
||
const wwwTitle = text("strong", "vhWwwPanelTitle", "vh-www-title");
|
||
const wwwHostCount = el("span", "", "vh-www-host-count");
|
||
const wwwRedirect = field(wwwHeader, "www-redirect", "vhWwwEnable", value.www_redirect === true, "checkbox");
|
||
wwwRedirect.closest("label").classList.add("vh-www-toggle");
|
||
wwwHeader.prepend(wwwTitle, wwwHostCount);
|
||
const wwwInactive = text("p", "vhWwwInactive", "fine vh-www-inactive");
|
||
const wwwDetails = el("div", undefined, "vh-www-details");
|
||
wwwDetails.id = "vh-www-details";
|
||
wwwRedirect.setAttribute("aria-controls", wwwDetails.id);
|
||
const wwwLabel = text("p", "vhWwwRedirectTarget", "vh-www-option-title");
|
||
const wwwChoices = el("div", undefined, "vh-www-choices");
|
||
wwwChoices.setAttribute("role", "radiogroup");
|
||
wwwChoices.setAttribute("aria-label", t("vhWwwRedirectTarget"));
|
||
const wwwTarget = document.createElement("input");
|
||
wwwTarget.type = "hidden"; wwwTarget.name = "www-redirect-target";
|
||
const wwwNoHosts = text("p", "vhWwwNoHosts", "fine vh-www-no-hosts");
|
||
const wwwOther = el("div", undefined, "vh-www-other");
|
||
const wwwHelp = text("p", "vhWwwRedirectHelp", "fine vh-www-help");
|
||
wwwDetails.append(wwwLabel, wwwChoices, wwwNoHosts, wwwOther, wwwHelp);
|
||
wwwPanel.append(wwwHeader, wwwInactive, wwwDetails, wwwTarget);
|
||
g.append(wwwPanel);
|
||
// Persist only the selected canonical host; choosing the www variant does
|
||
// not require adding an extra hostname to the vhost manually.
|
||
let selectedCanonical = value.www_redirect_target || (value.frontend?.hosts || []).find(host => !host.startsWith("*.")) || "";
|
||
const syncCanonical = () => {
|
||
const allHosts = lines(read("hosts").value);
|
||
const exactHosts = allHosts.filter(host => !host.startsWith("*.") && !/^[\d.:]+$/.test(host));
|
||
const bases = [...new Set(exactHosts.map(host => host.startsWith("www.") ? host.slice(4) : host))]
|
||
.filter(base => !/^[\d.:]+$/.test(base) && `www.${base}`.length <= 253);
|
||
const options = bases.flatMap(base => [base, `www.${base}`]);
|
||
if (!options.includes(selectedCanonical)) selectedCanonical = exactHosts[0] || "";
|
||
wwwTarget.value = selectedCanonical;
|
||
wwwHostCount.textContent = t("vhWwwSharedCount").replace("{count}", String(allHosts.length));
|
||
wwwChoices.replaceChildren();
|
||
for (const base of bases) {
|
||
const pair = el("div", undefined, "vh-www-domain-pair");
|
||
pair.append(el("strong", base, "vh-www-domain-label"));
|
||
const directions = el("div", undefined, "vh-www-pair-directions");
|
||
for (const target of [base, `www.${base}`]) {
|
||
const companion = target === base ? `www.${base}` : base;
|
||
const choice = el("label", undefined, "vh-www-choice");
|
||
if (target === selectedCanonical) choice.classList.add("is-selected");
|
||
const radio = document.createElement("input"); radio.type = "radio"; radio.name = "www-canonical-choice";
|
||
radio.value = target; radio.checked = target === selectedCanonical;
|
||
const copy = el("span", undefined, "vh-www-choice-copy");
|
||
copy.append(el("strong", target), el("span", `${companion} \u2192 ${target}`, "vh-www-route"));
|
||
choice.append(radio, copy);
|
||
radio.addEventListener("change", () => {
|
||
if (!radio.checked) return;
|
||
selectedCanonical = target;
|
||
syncCanonical();
|
||
changed();
|
||
});
|
||
directions.append(choice);
|
||
}
|
||
pair.append(directions); wwwChoices.append(pair);
|
||
}
|
||
const companion = selectedCanonical.startsWith("www.") ? selectedCanonical.slice(4) : `www.${selectedCanonical}`;
|
||
const aliases = allHosts.filter(host => host !== selectedCanonical && host !== companion);
|
||
wwwOther.replaceChildren();
|
||
if (aliases.length) {
|
||
wwwOther.append(text("span", "vhWwwOtherDomains"));
|
||
for (const host of aliases) wwwOther.append(el("code", host, "vh-www-alias"));
|
||
}
|
||
wwwNoHosts.hidden = bases.length > 0;
|
||
wwwRedirect.disabled = !bases.length;
|
||
if (!bases.length) wwwRedirect.checked = false;
|
||
// Never hide the destination choices: the unchecked state is a preview
|
||
// that the operator can configure before enabling the rule.
|
||
wwwDetails.hidden = false;
|
||
wwwInactive.hidden = wwwRedirect.checked;
|
||
wwwRedirect.setAttribute("aria-expanded", "true");
|
||
wwwPanel.classList.toggle("is-enabled", wwwRedirect.checked);
|
||
};
|
||
refreshCanonical = syncCanonical;
|
||
wwwRedirect.addEventListener("change", () => { syncCanonical(); changed(); });
|
||
syncCanonical();
|
||
field(g, "forward-client-ip", "vhForwardClientIp", value.forward_client_ip !== false, "checkbox");
|
||
note(g, "vhForwardClientIpHelp");
|
||
field(g, "cloudflare-client-ip", "vhCloudflareClientIp", value.cloudflare_client_ip === true, "checkbox");
|
||
const protocolRow = el("div", undefined, "vh-wide protocol-toggle-row"); g.append(protocolRow);
|
||
const h2Enabled = field(protocolRow, "http2-enabled", "vhHttp2Enabled", value.http2_enabled !== false, "checkbox");
|
||
const h3Enabled = field(protocolRow, "http3-enabled", "vhHttp3Enabled", value.http3_enabled !== false, "checkbox");
|
||
note(g, "vhFrontendProtocolsHelp"); note(g, "vhFrontendSelectionOnly");
|
||
const syncFrontendTransports = () => {
|
||
const capabilities = bindingControl.capabilities();
|
||
h2Enabled.disabled = !capabilities.http2; h3Enabled.disabled = !capabilities.http3;
|
||
if (!capabilities.http2) h2Enabled.checked = false;
|
||
if (!capabilities.http3) h3Enabled.checked = false;
|
||
const h2Available = capabilities.http2 && h2Enabled.checked;
|
||
const h3Available = capabilities.http3 && h3Enabled.checked;
|
||
websocketMode.disabled = !websocketEnabled.checked;
|
||
websocketLegacy.disabled = !websocketEnabled.checked;
|
||
const optionH2 = websocketMode.querySelector('option[value="h2"]');
|
||
const optionH3 = websocketMode.querySelector('option[value="h3"]');
|
||
const optionH2H3 = websocketMode.querySelector('option[value="h2h3"]');
|
||
optionH2.disabled = !h2Available;
|
||
optionH3.disabled = !h3Available;
|
||
optionH2H3.disabled = !h2Available || !h3Available;
|
||
const selected = websocketMode.value;
|
||
if ((selected === "h2" && !h2Available) || (selected === "h3" && !h3Available) || (selected === "h2h3" && (!h2Available || !h3Available))) {
|
||
websocketMode.value = selected === "h2h3" && h2Available ? "h2" : selected === "h2h3" && h3Available ? "h3" : "h1";
|
||
}
|
||
};
|
||
websocketEnabled.addEventListener("change", syncFrontendTransports);
|
||
bindingControl.onChange(syncFrontendTransports);
|
||
h2Enabled.addEventListener("change", syncFrontendTransports);
|
||
h3Enabled.addEventListener("change", syncFrontendTransports);
|
||
syncFrontendTransports();
|
||
g = group("vhBackendPool"); const members = el("div", undefined, "vh-wide vh-members"); g.append(members);
|
||
(value.backends || []).forEach(m => memberRow(members, m)); g.append(button("vhAddBackend", () => { memberRow(members); changed(); }));
|
||
g = group("vhBackendDiagnostics");
|
||
field(g, "timing-headers", "vhTimingHeaders", value.timing_headers === true, "checkbox");
|
||
field(g, "backend-header", "vhBackendHeader", value.backend_header === true, "checkbox");
|
||
field(g, "backend-timing-header", "vhBackendTimingHeader", value.backend_timing_header === true, "checkbox");
|
||
note(g, "vhBackendDiagnosticsHelp");
|
||
const proxyTimeouts = { ...defaultProxyTimeouts(), ...(value.proxy_timeouts || {}) };
|
||
g = group("vhProxyTimeouts");
|
||
field(g, "proxy-connect-timeout", "vhProxyConnectTimeout", proxyTimeouts.connect_timeout_seconds ?? 10, "number", { required: true, min: 1, max: 300 });
|
||
field(g, "proxy-header-timeout", "vhProxyHeaderTimeout", proxyTimeouts.response_header_timeout_seconds ?? 120, "number", { required: true, min: 1, max: 86400 });
|
||
field(g, "proxy-read-timeout", "vhProxyReadTimeout", proxyTimeouts.read_timeout_seconds ?? 60, "number", { required: true, min: 1, max: 86400 });
|
||
field(g, "proxy-send-timeout", "vhProxySendTimeout", proxyTimeouts.send_timeout_seconds ?? 60, "number", { required: true, min: 1, max: 86400 });
|
||
note(g, "vhProxyTimeoutsHelp");
|
||
const settings = value.load_balancing || {}, health = settings.health_check || {}, keepalive = settings.keepalive || {}, retry = settings.retry || {}, sticky = settings.sticky || {};
|
||
g = group("vhBalancing"); field(g, "algorithm", "vhAlgorithm", settings.algorithm || "least_connections", "select", { values: [["round_robin", "vhRoundRobin"], ["least_connections", "vhLeastConnections"], ["least_traffic", "vhLeastTraffic"], ["adaptive", "vhAdaptive"], ["ip_hash", "vhIpHash"]] });
|
||
field(g, "slow-start", "vhSlowStart", settings.slow_start_seconds ?? 10, "number", { required: true, min: 0, max: 300 });
|
||
field(g, "retry-max", "vhRetryMax", retry.max_retries ?? 1, "number", { required: true, min: 0, max: 3 });
|
||
field(g, "retry-connect", "vhRetryConnect", retry.retry_connect_errors !== false, "checkbox");
|
||
field(g, "retry-timeouts", "vhRetryTimeouts", retry.retry_timeouts !== false, "checkbox");
|
||
field(g, "retry-statuses", "vhRetryStatuses", (retry.retry_statuses || [502, 503, 504]).join(", "), "text", { maxLength: 64 });
|
||
note(g, "vhRetryHelp");
|
||
field(g, "keepalive", "vhKeepalive", keepalive.enabled !== false, "checkbox");
|
||
field(g, "idle-timeout", "vhIdleTimeout", keepalive.idle_timeout_seconds ?? 30, "number", { required: true, min: 1, max: 3600 });
|
||
field(g, "max-idle", "vhMaxIdle", keepalive.max_idle_per_host ?? 4, "number", { required: true, min: 1, max: 64 });
|
||
field(g, "tcp-keepalive", "vhTcpKeepalive", keepalive.tcp_keepalive_seconds ?? 30, "number", { required: true, min: 1, max: 3600 });
|
||
field(g, "pool-recycle-requests", "vhPoolRecycleRequests", keepalive.pool_recycle_requests ?? 25000, "number", { required: true, min: 0, max: 10000000 });
|
||
field(g, "pool-recycle-seconds", "vhPoolRecycleSeconds", keepalive.pool_recycle_seconds ?? 3600, "number", { required: true, min: 0, max: 604800 });
|
||
field(g, "http1-max-buffer", "vhHttp1MaxBuffer", keepalive.http1_max_buffer_bytes ?? 131072, "number", { required: true, min: 8192, max: 1048576 });
|
||
note(g, "vhPoolRecycleHelp");
|
||
g = group("vhHealth"); const healthMode = field(g, "health-mode", "vhHealthMode", health.mode || "tcp", "select", { values: [["off", "vhOff"], ["tcp", "vhTcp"], ["http", "vhHttp"]] });
|
||
const healthOffWarning = note(g, "vhHealthOffWarning"); const syncHealthWarning = () => { healthOffWarning.hidden = healthMode.value !== "off"; }; healthMode.addEventListener("change", syncHealthWarning); syncHealthWarning();
|
||
field(g, "health-path", "vhHealthPath", health.path || "/", "text", { required: true, maxLength: 1024 });
|
||
field(g, "health-host", "vhHealthHost", health.host || "", "text", { maxLength: 260 });
|
||
for (const [name, key, val, min, max] of [["interval", "vhInterval", health.interval_ms ?? 5000, 250, 300000], ["timeout", "vhTimeout", health.timeout_ms ?? 2000, 50, 60000], ["slow", "vhSlow", health.slow_response_ms ?? 1500, 0, 60000], ["fall", "vhFall", health.fall ?? 3, 1, 20], ["rise", "vhRise", health.rise ?? 2, 1, 20], ["status-min", "vhStatusMin", health.expected_status_min ?? 200, 100, 599], ["status-max", "vhStatusMax", health.expected_status_max ?? 399, 100, 599]]) field(g, name, key, val, "number", { required: true, min, max }); note(g, "vhHealthNote");
|
||
const passive = settings.passive_health || {};
|
||
g = group("vhPassiveHealth");
|
||
const passiveEnabled = field(g, "passive-enabled", "vhPassiveEnabled", passive.enabled === true, "checkbox");
|
||
note(g, "vhPassiveHelp");
|
||
const passiveFields = [
|
||
field(g, "passive-connect", "vhPassiveConnect", passive.count_connect_errors !== false, "checkbox"),
|
||
field(g, "passive-timeouts", "vhPassiveTimeouts", passive.count_timeouts !== false, "checkbox"),
|
||
field(g, "passive-io", "vhPassiveIo", passive.count_io_errors !== false, "checkbox"),
|
||
field(g, "passive-5xx", "vhPassive5xx", passive.count_http_5xx !== false, "checkbox"),
|
||
field(g, "passive-slow", "vhPassiveSlow", passive.count_slow_responses === true, "checkbox"),
|
||
field(g, "passive-fall", "vhPassiveFall", passive.fall ?? "", "number", { min: 1, max: 20 }),
|
||
field(g, "passive-rise", "vhPassiveRise", passive.rise ?? "", "number", { min: 1, max: 20 })
|
||
];
|
||
const passive5xx = passiveFields[3], passiveSlow = passiveFields[4];
|
||
const passive5xxWarning = note(g, "vhPassive5xxWarning");
|
||
const passiveSlowWarning = note(g, "vhPassiveSlowWarning");
|
||
const syncPassive = () => {
|
||
passiveFields.forEach(control => { control.disabled = !passiveEnabled.checked; const label = control.closest("label"); if (label) label.hidden = !passiveEnabled.checked; });
|
||
passive5xxWarning.hidden = !passiveEnabled.checked || !passive5xx.checked;
|
||
passiveSlowWarning.hidden = !passiveEnabled.checked || !passiveSlow.checked;
|
||
};
|
||
passiveEnabled.addEventListener("change", syncPassive);
|
||
passive5xx.addEventListener("change", syncPassive);
|
||
passiveSlow.addEventListener("change", syncPassive);
|
||
syncPassive();
|
||
g = group("vhSticky"); field(g, "sticky-mode", "vhStickyMode", sticky.mode || "off", "select", { values: [["off", "vhOff"], ["cookie", "vhCookie"], ["ip_hash", "vhIpHash"]] });
|
||
field(g, "cookie-name", "vhCookieName", sticky.cookie_name || "", "text", { maxLength: 96, pattern: "[A-Za-z0-9_-]*" });
|
||
field(g, "cookie-ttl", "vhCookieTtl", sticky.ttl_seconds ?? 3600, "number", { required: true, min: 60, max: 604800 }); note(g, "vhStickyNote");
|
||
const rate = value.rate_limit || {}, access = rate.country_access || {}, methodPolicy = value.method_policy || {};
|
||
g = group("vhTrafficPolicy");
|
||
const aclProfile = field(g, "acl-profile-id", "vhAclProfile", value.acl_profile_id || "", "select");
|
||
const noProfile = text("option", "vhAclProfileNone"); noProfile.value = ""; aclProfile.append(noProfile, ...(envelope?.config.acl_profiles || []).map(profile => { const option = el("option", profile.name || profile.id); option.value = profile.id; return option; })); aclProfile.value = value.acl_profile_id || ""; note(g, "vhAclProfileHelp");
|
||
field(g, "method-mode", "vhMethodMode", methodPolicy.mode || "off", "select", { values: [["off", "vhOff"], ["allow", "locationMethodAllow"], ["block", "locationMethodBlock"]] });
|
||
field(g, "method-list", "vhMethodList", (methodPolicy.methods || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
|
||
note(g, "vhMethodHelp");
|
||
field(g, "rate-enabled", "vhRateEnabled", rate.enabled === true, "checkbox");
|
||
field(g, "rate-requests", "vhRateRequests", rate.requests ?? 120, "number", { required: true, min: 1, max: 1000000 });
|
||
field(g, "rate-window", "vhRateWindow", rate.window_seconds ?? 60, "number", { required: true, min: 1, max: 3600 });
|
||
field(g, "rate-burst", "vhRateBurst", rate.burst ?? 30, "number", { required: true, min: 0, max: 1000000 });
|
||
field(g, "country-mode", "vhCountryMode", access.mode || "off", "select", { values: [["off", "vhOff"], ["block", "vhCountryBlock"], ["allow", "vhCountryAllow"]] });
|
||
field(g, "country-list", "vhCountryList", (access.countries || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
|
||
note(g, "vhCountryPolicyHelp"); countryRatesReader = countryRateEditor(g, rate.country_limits || []); note(g, "vhRateHelp");
|
||
const security = value.security_headers || {}, hsts = security.hsts || {};
|
||
g = group("vhSecurity"); field(g, "hsts", "vhHsts", hsts.enabled, "checkbox"); field(g, "hsts-age", "vhHstsAge", hsts.max_age_seconds ?? 31536000, "number", { required: true, min: 0, max: 63072000 });
|
||
field(g, "hsts-sub", "vhHstsSub", hsts.include_subdomains, "checkbox"); field(g, "hsts-preload", "vhHstsPreload", hsts.preload, "checkbox"); field(g, "nosniff", "vhNosniff", security.nosniff, "checkbox");
|
||
field(g, "frame", "vhFrame", security.frame_options || "preserve", "select", { values: [["preserve", "vhPreserve"], ["deny", "vhDeny"], ["sameorigin", "vhSameorigin"]] });
|
||
field(g, "referrer", "vhReferrer", security.referrer_policy || "", "text", { maxLength: 8192 });
|
||
field(g, "csp", "vhCsp", security.content_security_policy || "", "textarea", { rows: 2, maxLength: 8192 });
|
||
field(g, "permissions", "vhPermissions", security.permissions_policy || "", "text", { maxLength: 8192 });
|
||
g = group("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("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();
|
||
g.append(button("certManage", async () => { if (!(await closeEditor())) return; setPanel("tls"); })); note(g, "certSelectHint");
|
||
const inheritedTlsPolicy = envelope?.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] };
|
||
const tlsPolicy = value.tls_policy || inheritedTlsPolicy;
|
||
const customTlsPolicy = field(g, "tls-policy-custom", "vhTlsOverride", value.tls_policy != null, "checkbox");
|
||
const tls12 = field(g, "tls-policy-12", "tls12", tlsPolicy.tls12 !== false, "checkbox");
|
||
const tls13 = field(g, "tls-policy-13", "tls13", tlsPolicy.tls13 !== false, "checkbox");
|
||
const tlsCiphers = field(g, "tls-policy-ciphers", "tlsCiphers", (tlsPolicy.cipher_suites || []).join("\n"), "textarea", { rows: 7, maxLength: 8192 });
|
||
const syncTlsPolicy = () => { for (const input of [tls12, tls13, tlsCiphers]) input.disabled = !customTlsPolicy.checked; };
|
||
customTlsPolicy.addEventListener("change", syncTlsPolicy); syncTlsPolicy(); note(g, "vhTlsOverrideHelp");
|
||
g = group("vhTinyauth"); const auth = value.tinyauth || {}, authRoot = g;
|
||
const authMode = field(g, "auth-mode", "authMode", value.basic_auth?.enabled ? "basic" : auth.enabled ? "tinyauth" : "none", "select", { values: [["none", "authNone"], ["tinyauth", "authTinyauth"], ["basic", "authBasic"]] });
|
||
note(g, "authModeHelp");
|
||
|
||
const aclBody = el("section", undefined, "vh-wide auth-subcard auth-ip-card"); authRoot.append(aclBody);
|
||
aclBody.append(text("h3", "vhIpAccess"));
|
||
aclControl = aclEditor(aclBody, value.acl, () => { if (aclControl) read("acl").value = JSON.stringify(aclControl.read(), null, 2); });
|
||
|
||
const taBody = el("section", undefined, "vh-wide auth-subcard"), basicBody = el("section", undefined, "vh-wide auth-subcard"); authRoot.append(taBody, basicBody);
|
||
taBody.append(text("h3", "authTinyauth"));
|
||
const taPrimary = el("div", undefined, "vh-grid"); taBody.append(taPrimary);
|
||
const authEnable = field(taPrimary, "auth-enabled", "taEnable", auth.enabled, "checkbox");
|
||
const authUrl = field(taPrimary, "auth-url", "taUrl", auth.url || "", "url", { maxLength: 2048 });
|
||
const loginUrl = field(taPrimary, "auth-login", "taLogin", auth.login_url || "", "url", { maxLength: 2048 });
|
||
authUrl.placeholder = "http://10.87.7.99:3000"; loginUrl.placeholder = "https://auth.example.com/login";
|
||
field(taPrimary, "auth-private", "vhPrivate", auth.allow_private, "checkbox");
|
||
field(taPrimary, "auth-redirect-forbidden", "taRedirectForbidden", auth.redirect_forbidden !== false, "checkbox");
|
||
authEnable.closest("label").hidden = true;
|
||
note(taBody, "taHelp");
|
||
|
||
const taAdvanced = el("details", undefined, "vh-wide auth-advanced"); taAdvanced.append(text("summary", "taAdvanced"));
|
||
const taAdvancedGrid = el("div", undefined, "vh-grid"); taAdvanced.append(taAdvancedGrid); taBody.append(taAdvanced);
|
||
field(taAdvancedGrid, "auth-connect", "taConnect", auth.connect_timeout_seconds ?? 5, "number", { required: true, min: 1, max: 30 });
|
||
field(taAdvancedGrid, "auth-timeout", "taTimeout", auth.timeout_seconds ?? 10, "number", { required: true, min: 1, max: 60 });
|
||
field(taAdvancedGrid, "auth-request-headers", "taRequestHeaders", (auth.request_headers || ["cookie", "authorization", "x-api-key", "user-agent", "accept"]).join("\n"), "textarea", { rows: 3, maxLength: 8192 });
|
||
field(taAdvancedGrid, "auth-response-headers", "taResponseHeaders", (auth.response_headers || ["remote-user", "remote-email"]).join("\n"), "textarea", { rows: 3, maxLength: 8192 });
|
||
field(taAdvancedGrid, "auth-allowlist", "vhAddressAllowlist", (auth.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
|
||
field(taAdvancedGrid, "auth-ca", "vhCa", auth.tls_ca_pem || "", "textarea", { rows: 2, maxLength: 65536 });
|
||
|
||
basicBody.append(text("h3", "authBasic"));
|
||
const basicGrid = el("div", undefined, "vh-grid"); basicBody.append(basicGrid);
|
||
basicEditor = basicAuthEditor(basicGrid, value.basic_auth || {});
|
||
const syncAuth = () => {
|
||
authEnable.checked = authMode.value === "tinyauth";
|
||
taBody.hidden = !authEnable.checked;
|
||
basicBody.hidden = authMode.value !== "basic";
|
||
authUrl.required = loginUrl.required = authEnable.checked;
|
||
};
|
||
authMode.addEventListener("change", syncAuth); syncAuth();
|
||
g = group("vhResponsePolicy"); const server = value.server_header, diagnostics = value.diagnostic_headers;
|
||
const serverMode = field(g, "server-mode", "serverHeader", server?.mode || "inherit", "select", { values: [["inherit", "vhInherit"], ["preserve", "serverPreserve"], ["hide", "serverHide"], ["custom", "serverCustom"]] });
|
||
const serverValue = field(g, "server-value", "serverValue", server?.value || "RustProxy", "text", { maxLength: 256 });
|
||
const syncServer = () => { serverValue.disabled = serverMode.value !== "custom"; serverValue.required = !serverValue.disabled; }; serverMode.addEventListener("change", syncServer); syncServer();
|
||
const inheritDiagnostics = field(g, "diag-inherit", "diagInherit", !diagnostics, "checkbox"), diagInputs = [];
|
||
for (const [name, key] of [["request_id", "diagRequestId"]]) diagInputs.push(field(g, `diag-${name}`, key, diagnostics?.[name] ?? envelope?.config.diagnostic_headers?.[name], "checkbox"));
|
||
const syncDiag = () => { for (const input of diagInputs) input.disabled = inheritDiagnostics.checked; }; inheritDiagnostics.addEventListener("change", syncDiag); syncDiag(); note(g, "diagHelp");
|
||
g = group("vhCompression"); const compression = value.compression || {};
|
||
const compressionTri = (parent, name, key, current) => field(parent, name, key, current === true ? "on" : current === false ? "off" : "", "select", { values: [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]] });
|
||
const compressionToggle = compressionTri(g, "vh-compression-enabled", "compressionEnabled", compression.enabled);
|
||
const compressionOptions = el("div", undefined, "vh-wide vh-grid vh-compression-options"); g.append(compressionOptions);
|
||
const compressionCodecInputs = [
|
||
compressionTri(compressionOptions, "vh-compression-zstd", "compressionZstd", compression.zstd),
|
||
compressionTri(compressionOptions, "vh-compression-br", "compressionBr", compression.br),
|
||
compressionTri(compressionOptions, "vh-compression-gzip", "compressionGzip", compression.gzip),
|
||
compressionTri(compressionOptions, "vh-compression-deflate", "compressionDeflate", compression.deflate)
|
||
];
|
||
const syncCompressionVisibility = () => {
|
||
const disabled = compressionToggle.value === "off";
|
||
compressionOptions.hidden = disabled;
|
||
for (const input of compressionCodecInputs) input.disabled = disabled;
|
||
};
|
||
compressionToggle.addEventListener("change", syncCompressionVisibility); syncCompressionVisibility();
|
||
note(g, "vhCompressionHelp");
|
||
g = group("vhCache"); const cache = value.cache || {};
|
||
const tri = (parent, name, key, current) => field(parent, name, key, current === true ? "on" : current === false ? "off" : "", "select", { values: [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]] });
|
||
const cacheToggle = tri(g, "vh-cache-enabled", "cacheEnabled", cache.enabled);
|
||
if (envelope?.config.cache?.enabled !== true) note(g, "vhCacheGlobalDisabled");
|
||
const cacheOptions = el("div", undefined, "vh-wide vh-grid vh-cache-options"); g.append(cacheOptions);
|
||
field(cacheOptions, "vh-cache-ttl", "vhCacheTtl", cache.max_ttl_seconds ?? "", "number", { min: 1, max: 604800 });
|
||
field(cacheOptions, "vh-cache-default", "vhCacheDefault", cache.default_ttl_seconds ?? "", "number", { min: 0, max: 604800 });
|
||
field(cacheOptions, "vh-cache-object", "vhCacheObject", cache.max_object_bytes == null ? "" : cache.max_object_bytes / 1024, "number", { min: 1, step: "any", max: (envelope?.config.cache?.max_object_bytes ?? 1048576) / 1024 });
|
||
tri(cacheOptions, "vh-cache-query", "cacheBypassQuery", cache.bypass_query);
|
||
tri(cacheOptions, "vh-cache-static-cookie", "vhCacheStaticCookie", cache.static_ignore_cookies);
|
||
const customStatic = field(cacheOptions, "vh-cache-static-custom", "vhCacheStaticCustom", cache.static_extensions != null, "checkbox");
|
||
const staticExtensions = field(cacheOptions, "vh-cache-static-extensions", "vhCacheStaticExtensions", (cache.static_extensions || envelope?.config.cache?.static_extensions || []).join("\n"), "textarea", { rows: 5, maxLength: 2048 });
|
||
const syncStatic = () => { staticExtensions.disabled = !customStatic.checked; }; customStatic.addEventListener("change", syncStatic); syncStatic(); note(cacheOptions, "vhCacheStaticHelp");
|
||
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);
|
||
field(cacheOptions, "vh-cache-prefix-custom", "vhCacheCustomPrefixes", cache.bypass_prefixes != null, "checkbox");
|
||
field(cacheOptions, "vh-cache-prefixes", "cacheBypassPrefixes", (cache.bypass_prefixes || []).join("\n"), "textarea", { rows: 2, maxLength: 32768 }); note(cacheOptions, "vhCacheHint");
|
||
const forceCache = field(cacheOptions, "vh-cache-force-enabled", "vhCacheForceEnabled", cache.unsafe_force?.enabled === true, "checkbox");
|
||
const forceTargets = field(cacheOptions, "vh-cache-force-targets", "vhCacheForceTargets", (cache.unsafe_force?.targets || []).join("\n"), "textarea", { rows: 3, maxLength: 65536 });
|
||
const syncForceCache = () => { forceTargets.disabled = !forceCache.checked; }; forceCache.addEventListener("change", async () => { if (forceCache.checked && !(await appConfirm(t("vhCacheForceConfirm"), { danger: true }))) forceCache.checked = false; syncForceCache(); }); syncForceCache();
|
||
note(cacheOptions, "vhCacheForceHelp");
|
||
if (editing) cacheOptions.append(button("vhPurgeCache", () => purge(editing)));
|
||
const syncCacheVisibility = () => {
|
||
const enabled = envelope?.config.cache?.enabled === true && cacheToggle.value !== "off";
|
||
cacheOptions.hidden = !enabled;
|
||
};
|
||
cacheToggle.addEventListener("change", syncCacheVisibility); syncCacheVisibility();
|
||
g = group("vhTransform"); const transform = value.transform || { enabled: false };
|
||
const transformEnabled = field(g, "vh-transform-enabled", "vhTransformEnable", transform.enabled === true, "checkbox");
|
||
const transformOptions = el("div", undefined, "vh-wide vh-grid vh-transform-options"); g.append(transformOptions);
|
||
for (const [name, key] of [["javascript", "transformJavascript"], ["css", "transformCss"], ["html", "transformHtml"], ["svg", "transformSvg"], ["json", "transformJson"]]) tri(transformOptions, `vh-transform-${name}`, key, transform[name]);
|
||
const customTransformExtensions = field(transformOptions, "vh-transform-extensions-custom", "vhTransformExtensionsCustom", transform.extensions != null, "checkbox");
|
||
const transformExtensions = field(transformOptions, "vh-transform-extensions", "transformExtensions", (transform.extensions || envelope?.config.transform?.extensions || []).join("\n"), "textarea", { rows: 5, maxLength: 2048 });
|
||
const syncTransform = () => {
|
||
transformOptions.hidden = !transformEnabled.checked;
|
||
transformExtensions.disabled = !customTransformExtensions.checked;
|
||
};
|
||
transformEnabled.addEventListener("change", syncTransform); customTransformExtensions.addEventListener("change", syncTransform); syncTransform();
|
||
note(transformOptions, "vhTransformHelp");
|
||
g = group("vhHeaders"); headerEditors(g, value.headers || {});
|
||
g = group("vhPages"); g.classList.add("vh-pages-grid"); pagesEditor = window.rustProxyPages.vhostEditor(g, value.pages || {});
|
||
initTabs(); dirty = false; if (latestStatus) renderStatus(latestStatus); if (show && !dialog.open) dialog.showModal();
|
||
|
||
}
|
||
function collect() {
|
||
locationsReader(); // Validate every visual row; never silently save an earlier JSON value.
|
||
let paths, acl, headers;
|
||
try {
|
||
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 bindingSelection = readBindings();
|
||
const mainSourceType = str("source-type");
|
||
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"));
|
||
if (yes("www-redirect") && !str("www-redirect-target")) throw new Error(t("vhWwwNoHosts"));
|
||
if (mainSourceType === "web_server" && !str("source-web-service")) throw new Error(t("webServiceRequired"));
|
||
if (paths.some(route => !route.response && route.source?.type === "web_server" && !route.source.web_service_id)) throw new Error(t("webServiceRequired"));
|
||
if (yes("force-ssl")) {
|
||
const tlsPorts = new Set(); let tlsSelected = false, httpSelected = false;
|
||
for (const id of bindingSelection) {
|
||
const endpoint = envelope?.config.listeners?.endpoints?.find(item => item.id === id);
|
||
if (!endpoint || endpoint.enabled === false) continue;
|
||
if (!endpoint.binding?.tls) { httpSelected = true; continue; }
|
||
tlsSelected = true;
|
||
for (const address of [endpoint.binding?.address, ...(endpoint.additional_addresses || [])].filter(Boolean)) {
|
||
const match = String(address).match(/:(\d+)$/); if (match) tlsPorts.add(Number(match[1]));
|
||
}
|
||
}
|
||
if (!httpSelected) throw new Error(t("vhForceSslNeedsHttp"));
|
||
if (!tlsSelected || !tlsPorts.size) throw new Error(t("vhForceSslNeedsTls"));
|
||
if (!tlsPorts.has(443) && tlsPorts.size !== 1) throw new Error(t("vhForceSslPortConflict"));
|
||
}
|
||
if (yes("tls-policy-custom") && !yes("tls-policy-12") && !yes("tls-policy-13")) throw new Error(t("tlsPolicyVersionRequired"));
|
||
const selectedUsesHttp3 = bindingSelection.some(id => {
|
||
const endpoint = envelope?.config.listeners?.endpoints?.find(item => item.id === id);
|
||
return !!endpoint?.binding?.http3;
|
||
});
|
||
if (yes("http3-enabled") && selectedUsesHttp3 && yes("tls-policy-custom")) {
|
||
const selected = { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")).sort() };
|
||
const inherited = envelope?.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] };
|
||
const globalPolicy = { tls12: inherited.tls12 !== false, tls13: inherited.tls13 !== false, cipher_suites: [...(inherited.cipher_suites || [])].sort() };
|
||
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"),
|
||
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()) },
|
||
server_header: str("server-mode") === "inherit" ? null : { mode: str("server-mode"), value: str("server-value") },
|
||
diagnostic_headers: yes("diag-inherit") ? null : { request_id: yes("diag-request_id") },
|
||
tinyauth: { enabled: yes("auth-enabled"), url: str("auth-url"), login_url: str("auth-login"), allow_private: yes("auth-private"), redirect_forbidden: yes("auth-redirect-forbidden"), connect_timeout_seconds: num("auth-connect"), timeout_seconds: num("auth-timeout"), request_headers: lines(str("auth-request-headers")), response_headers: lines(str("auth-response-headers")), address_allowlist: lines(str("auth-allowlist")), tls_ca_pem: nullable("auth-ca") }, backends: members, routes: paths, acl, headers,
|
||
load_balancing: {
|
||
algorithm: str("algorithm"), slow_start_seconds: num("slow-start"), keepalive: { enabled: yes("keepalive"), idle_timeout_seconds: num("idle-timeout"), max_idle_per_host: num("max-idle"), tcp_keepalive_seconds: num("tcp-keepalive"), pool_recycle_requests: num("pool-recycle-requests"), pool_recycle_seconds: num("pool-recycle-seconds"), http1_max_buffer_bytes: num("http1-max-buffer") },
|
||
retry: { max_retries: num("retry-max"), retry_connect_errors: yes("retry-connect"), retry_timeouts: yes("retry-timeouts"), retry_statuses: str("retry-statuses").split(/[\s,]+/).filter(Boolean).map(value => Number(value)) },
|
||
health_check: { mode: str("health-mode"), path: str("health-path"), host: nullable("health-host"), interval_ms: num("interval"), timeout_ms: num("timeout"), slow_response_ms: num("slow"), fall: num("fall"), rise: num("rise"), expected_status_min: num("status-min"), expected_status_max: num("status-max") },
|
||
passive_health: {
|
||
enabled: yes("passive-enabled"),
|
||
count_connect_errors: yes("passive-connect"),
|
||
count_timeouts: yes("passive-timeouts"),
|
||
count_io_errors: yes("passive-io"),
|
||
count_http_5xx: yes("passive-5xx"),
|
||
count_slow_responses: yes("passive-slow"),
|
||
fall: str("passive-fall") === "" ? null : num("passive-fall"),
|
||
rise: str("passive-rise") === "" ? null : num("passive-rise")
|
||
},
|
||
sticky: { mode: str("sticky-mode"), cookie_name: nullable("cookie-name"), ttl_seconds: num("cookie-ttl") }
|
||
},
|
||
security_headers: { hsts: { enabled: yes("hsts"), max_age_seconds: num("hsts-age"), include_subdomains: yes("hsts-sub"), preload: yes("hsts-preload") }, nosniff: yes("nosniff"), frame_options: str("frame"), referrer_policy: nullable("referrer"), content_security_policy: nullable("csp"), permissions_policy: nullable("permissions") }
|
||
};
|
||
}
|