v0.34.4
This commit is contained in:
1 parent
1fd81eaf5f
commit
ee174bc283
36 files changed
+908
-183
No files matched your search
@@ -45,7 +45,7 @@ const defaults = () => ({
|
||||
frontend: { hosts: [], listener_ids: [] },
|
||||
source: { type: "reverse_proxy" },
|
||||
backends: [{ id: "origin-1", url: "", weight: 1, enabled: true, pool_eligible: true, allow_private: true, accept_local_listener_risk: false, preserve_host: true, protocol: "http1", tls_verify: false }],
|
||||
error_fallback: null, cache: { enabled: false }, compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
|
||||
error_fallback: null, cache: { enabled: false }, compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl_profile_ids: [], acl: null, allow_certificate_mismatch: false, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
|
||||
});
|
||||
|
||||
function activateTab(name) { document.querySelectorAll("#vhost-tabs button").forEach(b => { const active = b.dataset.tab === name; b.classList.toggle("active", active); b.setAttribute("aria-pressed", String(active)); b.setAttribute("aria-selected", String(active)); b.tabIndex = active ? 0 : -1; }); container.querySelectorAll(".vh-group").forEach(g => g.hidden = g.dataset.tab !== name); }
|
||||
|
||||
@@ -238,8 +238,22 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
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");
|
||||
const selectedProfiles = new Set([value.acl_profile_id, ...(value.acl_profile_ids || [])].filter(Boolean));
|
||||
const profileBox = el("div", undefined, "vh-wide vh-acl-profile-picker");
|
||||
profileBox.append(text("strong", "vhAclProfile"));
|
||||
const choices = el("div", undefined, "vh-acl-profile-choices");
|
||||
for (const profile of envelope?.config.acl_profiles || []) {
|
||||
const label = el("label", undefined, "check vh-acl-profile-choice");
|
||||
const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.name = "acl-profile-choices";
|
||||
checkbox.value = profile.id; checkbox.checked = selectedProfiles.has(profile.id);
|
||||
label.append(checkbox, el("span", profile.name || profile.id)); choices.append(label);
|
||||
}
|
||||
if (!choices.children.length) choices.append(text("span", "vhAclProfileNone", "fine"));
|
||||
profileBox.append(choices);
|
||||
const multiWarning = text("p", "multiAclWarning", "fine acl-stack-warning");
|
||||
const syncMultiWarning = () => { multiWarning.hidden = choices.querySelectorAll('input:checked').length < 2; };
|
||||
choices.addEventListener("change", syncMultiWarning); syncMultiWarning();
|
||||
profileBox.append(multiWarning); g.append(profileBox); 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");
|
||||
@@ -273,6 +287,10 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
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 mismatch = field(g, "allow-certificate-mismatch", "vhCertificateMismatchForce", value.allow_certificate_mismatch === true, "checkbox");
|
||||
const mismatchHint = note(g, "vhCertificateMismatchWarning");
|
||||
const syncMismatch = () => { mismatch.disabled = !selector.value; if (!selector.value) mismatch.checked = false; mismatchHint.hidden = !mismatch.checked; };
|
||||
selector.addEventListener("change", syncMismatch); mismatch.addEventListener("change", syncMismatch); syncMismatch();
|
||||
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");
|
||||
@@ -445,7 +463,8 @@ function collect() {
|
||||
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" }, error_fallback: mainFallbackTarget ? { target: mainFallbackTarget, statuses: mainFallbackStatuses.length ? mainFallbackStatuses : [404] } : null, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"),
|
||||
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, error_fallback: mainFallbackTarget ? { target: mainFallbackTarget, statuses: mainFallbackStatuses.length ? mainFallbackStatuses : [404] } : null, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), allow_certificate_mismatch: yes("allow-certificate-mismatch"), 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: null,
|
||||
acl_profile_ids: [...form.querySelectorAll('input[name="acl-profile-choices"]:checked')].map(input => input.value),
|
||||
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(), user_agent_limits: uaRatesReader() },
|
||||
method_policy: { mode: str("method-mode"), methods: lines(str("method-list")).map(value => value.toUpperCase()) },
|
||||
|
||||
@@ -561,6 +561,15 @@ form.addEventListener("submit", async event => {
|
||||
if (epoch !== draftEpoch || revision !== envelope.revision || !dialog.open) throw new Error(t("stale"));
|
||||
const value = collect(), next = structuredClone(envelope.config); next.vhosts ||= [];
|
||||
const debugIssues = debugOriginIssues(value.backends); if (debugIssues.length && !(await appConfirm(`${t("vhDebugOriginMismatch")}\n\n${debugIssues.map(member => member.id).join(", ")}`, { danger: true }))) return;
|
||||
const oldVhost = (envelope.config.vhosts || []).find(v => v.id === editing);
|
||||
// A previous acceptance must not silently authorize new hostnames or a different certificate.
|
||||
const certificateScope = item => JSON.stringify({
|
||||
certificate: item?.certificate_id || null,
|
||||
hosts: [...(item?.frontend?.hosts || [])].map(v => v.toLowerCase().replace(/\.$/, "")).sort(),
|
||||
wwwRedirect: item?.www_redirect === true,
|
||||
wwwTarget: item?.www_redirect_target || null
|
||||
});
|
||||
if (value.allow_certificate_mismatch && (!oldVhost?.allow_certificate_mismatch || certificateScope(value) !== certificateScope(oldVhost)) && !(await appConfirm(t("vhCertificateMismatchConfirm"), { danger: true }))) return;
|
||||
if (next.vhosts.some(v => v.id === value.id && v.id !== editing)) throw new Error(t("vhConflict"));
|
||||
if (editing && value.id !== editing && !(await appConfirm(t("vhIdRenameConfirm").replace("{old}", editing).replace("{new}", value.id), { danger: true }))) return;
|
||||
const addsCertificate = stagedCertificates.some(c => c.id === value.certificate_id);
|
||||
|
||||
Reference in new issue
Block a user