Files
RustProxy/src/admin/web/integrations.js
T
2026-11-08 23:23:21 +01:00

279 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
(() => {
let uploadBusy = false;
const label = (tag, key, cls) => { const node = el(tag, t(key), cls); node.dataset.i18n = key; return node; };
function field(parent, name, key, value, type = "text", options = {}) {
const wrapper = el("label", undefined, type === "checkbox" ? "check" : "vh-field");
const input = el(type === "select" ? "select" : type === "textarea" ? "textarea" : "input"); input.name = name;
if (type === "select") for (const [value, key] of options.values || []) { const option = label("option", key); option.value = value; input.append(option); }
else if (type !== "textarea") input.type = type;
if (type === "checkbox") input.checked = !!value; else input.value = value ?? "";
for (const key of ["min", "max", "maxLength", "required", "pattern", "rows", "step"]) if (Object.hasOwn(options, key)) input[key] = options[key];
if (type === "checkbox") wrapper.append(input, label("span", key)); else wrapper.append(label("span", key), input);
parent.append(wrapper); return input;
}
const button = (parent, key, run) => { const node = label("button", key, "quiet"); node.type = "button"; node.addEventListener("click", run); parent.append(node); return node; };
function hostnames(parent, initial = [], options = {}) {
const wrapper = el("div", undefined, "vh-field vh-wide host-chip-field");
wrapper.append(label("span", options.labelKey || "vhHosts"));
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(options.addKey || "vhHostAdd"));
const values = [];
const notify = () => { if (typeof options.onChange === "function") options.onChange(); };
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 = () => {
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(); notify(); });
chip.append(remove); box.insertBefore(chip, input);
}
};
const add = (raw, changed = true) => { const value = normalize(raw); if (!value || values.includes(value)) return; values.push(value); sync(); if (changed) notify(); };
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(box, label("p", options.helpKey || "vhHostsHelp", "fine")); parent.append(wrapper);
for (const host of initial) { try { add(host, false); } catch { } }
return () => { if (input.value.trim()) consume(input.value); if (!values.length) throw new Error(t(options.requiredKey || "vhHostRequired")); return [...values]; };
}
function addressTagEditor(parent, initial = [], lockedValues = []) {
const wrapper = el("div", undefined, "vh-field vh-wide host-chip-field"); wrapper.append(label("span", "listenerAddresses"));
const box = el("div", undefined, "host-chip-editor listener-address-chip-editor"), input = document.createElement("input");
input.type = "text"; input.maxLength = 128; input.placeholder = "127.0.0.1:8080 / [::1]:8080"; input.setAttribute("aria-label", t("listenerAddressAdd"));
const values = [], locked = new Set(lockedValues);
const normalize = raw => { const value = raw.trim(); if (!value) return null; if (value.length > 128 || /[\s,]/.test(value)) throw fieldError(input, t("listenerAddressesInvalid")); return value; };
const sync = () => { 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"; const fixed = locked.has(value); remove.disabled = fixed; remove.title = fixed ? t("listenerStartupLocked") : ""; remove.setAttribute("aria-label", `${t("remove")} ${value}`); remove.addEventListener("click", () => { if (fixed) return; values.splice(values.indexOf(value), 1); sync(); setSettingsDirty("listeners-form"); }); chip.append(remove); box.insertBefore(chip, input); } };
const add = (raw, dirty = true) => { const value = normalize(raw); if (!value || values.includes(value)) return; if (values.length >= 16) throw fieldError(input, t("listenerAddressesInvalid")); values.push(value); sync(); if (dirty) setSettingsDirty("listeners-form"); };
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 value = event.clipboardData?.getData("text") || ""; if (!/[\s,]/.test(value.trim())) return; event.preventDefault(); try { consume(value); } catch (error) { toast(error.message, true); } });
box.append(input); wrapper.append(box, label("p", locked.size ? "listenerStartupLocked" : "listenerAddressesHelp", "fine")); parent.append(wrapper);
for (const value of initial) { try { add(value, false); } catch { } }
const read = () => { if (input.value.trim()) consume(input.value); return [...values]; }; read.input = input; return read;
}
const h2Defaults = { idle_timeout_seconds: 300, max_connection_lifetime_seconds: 0, drain_timeout_seconds: 10 };
const effectiveHttp2Settings = binding => ({ ...h2Defaults, ...(envelope?.config.http2 || {}), ...(binding?.http2_settings || {}) });
function http2SettingsControls(parent, binding) {
const box = el("div", undefined, "h3-editor vh-wide"); parent.append(box);
const grid = el("div", undefined, "vh-grid"); box.append(grid);
const current = effectiveHttp2Settings(binding), inputs = {};
for (const [name, min, max, key] of [
["idle_timeout_seconds", 1, 86400, "http2IdleTimeout"],
["max_connection_lifetime_seconds", 0, 2592000, "http2MaxLifetime"],
["drain_timeout_seconds", 1, 3600, "http2DrainTimeout"],
]) inputs[name] = field(grid, name, key, current[name], "number", { min, max, required: true });
box.append(label("p", "http2DrainHelp", "fine"));
const sync = (blocked = false) => { for (const input of Object.values(inputs)) input.disabled = blocked; };
const read = () => {
const value = {};
for (const [name, input] of Object.entries(inputs)) {
value[name] = Number(input.value);
if (!Number.isSafeInteger(value[name])) throw fieldError(input, t("vhJsonInvalid"));
}
return value;
};
return { read, sync };
}
const h3Defaults = { alt_svc_max_age: 3600, max_concurrent_connections: 512, max_concurrent_bidistream: 100, max_concurrent_unistream: 100, max_idle_timeout: 10, udp_receive_buffer_bytes: 4194304, udp_send_buffer_bytes: 4194304 };
function http3SettingsControls(parent, binding) {
const box = el("div", undefined, "h3-editor vh-wide"); parent.append(box);
const grid = el("div", undefined, "vh-grid"); box.append(grid); const inputs = {};
for (const [name, min, max] of [["alt_svc_max_age", 0, 604800], ["max_concurrent_connections", 1, 4096], ["max_concurrent_bidistream", 1, 1024], ["max_concurrent_unistream", 3, 1024], ["max_idle_timeout", 0, 86400]]) inputs[name] = field(grid, name, `h3_${name}`, binding.http3?.[name] ?? h3Defaults[name], "number", { min, max, required: true });
for (const direction of ["receive", "send"]) {
const name = `udp_${direction}_buffer_bytes`;
const mib = (binding.http3?.[name] ?? h3Defaults[name]) / 1048576;
inputs[name] = field(grid, name, `h3_udp_${direction}_buffer_mib`, mib, "number", { min: 0, max: 16, step: "any", required: true });
}
box.append(label("p", "h3TuningHelp", "fine"));
const sync = (blocked = false) => { for (const input of Object.values(inputs)) input.disabled = blocked; };
sync(false);
const read = () => {
const value = {};
for (const [name, input] of Object.entries(inputs)) {
let number = Number(input.value);
if (!input.value.trim() || !Number.isFinite(number)) throw fieldError(input, t("vhJsonInvalid"));
if (name.startsWith("udp_") && name.endsWith("_buffer_bytes")) {
number = Math.round(number * 1048576);
if (number !== 0 && (number < 65536 || number > 16777216)) throw fieldError(input, t("vhJsonInvalid"));
}
if (!Number.isSafeInteger(number)) throw fieldError(input, t("vhJsonInvalid"));
value[name] = number;
}
return value;
};
return { read, sync };
}
function addRow(item = { id: `listener-${$("named-listener-rows").children.length + 1}`, name: "", scope: "internal", enabled: true, binding: { address: "127.0.0.1:8181", tls: false, http2: false, http2_settings: null, h2c: false, http3: null } }) {
if ($("named-listener-rows").children.length >= 64) { toast(t("listenerLimit"), true); return; }
const row = el("fieldset", undefined, "named-listener-row"); row.append(label("legend", "listenerEndpoint")); const grid = el("div", undefined, "vh-grid"); row.append(grid); const b = item.binding || {};
const id = field(grid, "listener-id", "listenerId", item.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_\\-]{1,64}" });
const name = field(grid, "listener-name", "listenerName", item.name, "text", { maxLength: 96 });
const scope = field(grid, "listener-scope", "listenerScope", item.scope || "internal", "select", { values: [["internal", "listenerInternal"], ["public", "listenerPublic"]] });
const currentAddresses = [b.address, ...(item.additional_addresses || [])].filter(Boolean);
const startupAddress = b.tls ? latestStatus?.listeners?.locked_https : latestStatus?.listeners?.locked_http;
const startupLocked = !!startupAddress && currentAddresses.includes(startupAddress);
const readAddresses = addressTagEditor(grid, currentAddresses, startupLocked ? [startupAddress] : []);
const enabled = field(grid, "listener-enabled", "vhEnabled", item.enabled !== false, "checkbox");
const tls = field(grid, "listener-tls", "vhTls", !!b.tls, "checkbox"); const h2 = field(grid, "listener-h2", "vhHttp2", b.tls && b.http2 !== false, "checkbox"); const h2c = field(grid, "listener-h2c", "listenerH2c", !b.tls && !!b.h2c, "checkbox");
const preservedHttp2 = effectiveHttp2Settings(b);
const protocolActions = el("div", undefined, "listener-protocol-settings-actions vh-wide"); row.append(protocolActions);
const h2Settings = button(protocolActions, "h2Configure", () => {
window.rustProxySettings?.select("connections");
window.rustProxySettings?.selectHttpTab?.("http2");
document.querySelector('[data-http-settings-page="http2"]')?.scrollIntoView({ block: "start" });
});
h2Settings.classList.add("listener-h2-settings-link"); h2Settings.dataset.uiIconName = "settings";
const preservedHttp3 = Object.hasOwn(b, "http3") ? b.http3 : (b.tls ? { ...h3Defaults } : null);
const h3 = field(grid, "listener-h3", "h3Enable", b.tls && !!preservedHttp3, "checkbox");
const h3Settings = button(protocolActions, "h3Configure", () => {
window.rustProxySettings?.select("connections");
window.rustProxySettings?.selectHttpTab?.("http3");
document.querySelector('[data-http-settings-page="http3"]')?.scrollIntoView({ block: "start" });
});
h3Settings.classList.add("listener-h3-settings-link"); h3Settings.dataset.uiIconName = "settings";
const sync = () => {
h2.disabled = !tls.checked; h2c.disabled = tls.checked; h3.disabled = !tls.checked;
h2Settings.hidden = !((tls.checked && h2.checked) || (!tls.checked && h2c.checked));
h3Settings.hidden = !tls.checked || !h3.checked;
};
for (const input of [tls, h2, h2c, h3]) input.addEventListener("change", sync);
sync();
const removeButton = button(row, "remove", () => { if (startupLocked) return; const users = (envelope?.config.vhosts || []).filter(v => v.frontend.listener_ids?.includes(id.value.trim())); if (users.length) { toast(`${t("listenerInUse")}: ${users.map(v => v.id).join(", ")}`, true); return; } row.remove(); setSettingsDirty("listeners-form"); });
if (startupLocked) {
row.classList.add("listener-startup-locked");
enabled.disabled = true;
tls.disabled = true;
removeButton.disabled = true;
removeButton.title = t("listenerStartupLocked");
row.append(label("p", "listenerStartupLocked", "fine"));
}
row.read = () => {
const addresses = readAddresses();
if (!addresses.length || addresses.length > 16 || new Set(addresses).size !== addresses.length) throw fieldError(readAddresses.input, t("listenerAddressesInvalid"));
if (addresses.length > 1 && addresses.some(value => /^0\.0\.0\.0:/.test(value) || /^\[?::\]?:/.test(value))) throw fieldError(readAddresses.input, t("listenerAddressesExplicit"));
const http2Enabled = (tls.checked && h2.checked) || (!tls.checked && h2c.checked);
return { id: id.value.trim(), name: name.value.trim(), scope: scope.value, enabled: enabled.checked, binding: { address: addresses[0], tls: tls.checked, http2: tls.checked && h2.checked, http2_settings: http2Enabled ? preservedHttp2 : null, h2c: !tls.checked && h2c.checked, http3: tls.checked && h3.checked ? (preservedHttp3 || { ...h3Defaults }) : null }, additional_addresses: addresses.slice(1) };
};
$("named-listener-rows").append(row);
}
$("add-named-listener").addEventListener("click", () => { addRow(); setSettingsDirty("listeners-form"); });
function renderListeners() { const items = envelope?.config.listeners?.endpoints || []; $("named-listener-rows").replaceChildren(); for (const item of items) addRow(item); }
function collectListeners() { const rows = [...$("named-listener-rows").children], endpoints = rows.map(row => row.read()), seen = new Set(); for (let index = 0; index < endpoints.length; index++) { const id = endpoints[index].id; if (seen.has(id)) throw fieldError(rows[index].querySelector('[name="listener-id"]'), t("listenerDuplicate")); seen.add(id); } return { endpoints }; }
function renderHttp2Settings() {
const root = $("http2-listener-rows"); if (!root) return; root.replaceChildren();
const items = (envelope?.config.listeners?.endpoints || []).filter(endpoint => {
const binding = endpoint.binding || {};
return (binding.tls && binding.http2 !== false) || (!binding.tls && !!binding.h2c);
});
const empty = $("http2-no-listeners"); empty.hidden = items.length !== 0;
for (const endpoint of items) {
const row = el("fieldset", undefined, "named-listener-row http2-listener-row"); row.dataset.listenerId = endpoint.id;
const addresses = [endpoint.binding?.address, ...(endpoint.additional_addresses || [])].filter(Boolean).join(", ");
const legend = el("legend", `${endpoint.name || endpoint.id} · ${addresses}`); row.append(legend);
const mode = endpoint.binding?.tls ? "H2 / TLS" : "h2c";
const meta = el("p", `${endpoint.id} · ${mode}${endpoint.enabled === false ? ` · ${t("vhDisabled")}` : ""}`, "fine"); row.append(meta);
const controls = http2SettingsControls(row, endpoint.binding || {}); row.read = controls.read; row.sync = controls.sync; root.append(row);
}
syncHttp2Availability();
}
function collectHttp2Listeners() {
const listeners = structuredClone(envelope?.config.listeners || { endpoints: [] });
const rows = new Map([...$("http2-listener-rows").children].map(row => [row.dataset.listenerId, row]));
for (const endpoint of listeners.endpoints || []) {
const binding = endpoint.binding || {}, row = rows.get(endpoint.id);
const enabled = (binding.tls && binding.http2 !== false) || (!binding.tls && !!binding.h2c);
if (!row || !enabled) continue;
endpoint.binding.http2_settings = row.read();
}
return listeners;
}
function syncHttp2Availability() {
const root = $("http2-listener-rows"), note = $("http2-listeners-dirty"), save = $("http2-save"); if (!root || !note || !save) return;
const blocked = settingsDirty.has("listeners-form"); note.hidden = !blocked;
for (const row of root.children) row.sync?.(blocked);
save.disabled = blocked || root.children.length === 0;
}
function renderHttp3Settings() {
const root = $("http3-listener-rows"); if (!root) return; root.replaceChildren();
const tlsItems = (envelope?.config.listeners?.endpoints || []).filter(endpoint => !!endpoint.binding?.tls);
const items = tlsItems.filter(endpoint => !!endpoint.binding?.http3);
const empty = $("http3-no-listeners");
empty.hidden = items.length !== 0;
if (!items.length) { empty.textContent = t(tlsItems.length ? "http3NoEnabledListeners" : "http3NoTlsListeners"); delete empty.dataset.i18n; }
for (const endpoint of items) {
const row = el("fieldset", undefined, "named-listener-row http3-listener-row"); row.dataset.listenerId = endpoint.id;
const addresses = [endpoint.binding?.address, ...(endpoint.additional_addresses || [])].filter(Boolean).join(", ");
const legend = el("legend", `${endpoint.name || endpoint.id} · ${addresses}`); row.append(legend);
const meta = el("p", `${endpoint.id}${endpoint.enabled === false ? ` · ${t("vhDisabled")}` : ""}`, "fine"); row.append(meta);
const controls = http3SettingsControls(row, endpoint.binding || {}); row.read = controls.read; row.sync = controls.sync; root.append(row);
}
syncHttp3Availability();
}
function collectHttp3Listeners() {
const listeners = structuredClone(envelope?.config.listeners || { endpoints: [] });
const rows = new Map([...$("http3-listener-rows").children].map(row => [row.dataset.listenerId, row]));
for (const endpoint of listeners.endpoints || []) { const row = rows.get(endpoint.id); if (!row || !endpoint.binding?.tls || !endpoint.binding?.http3) continue; endpoint.binding.http3 = row.read(); }
return listeners;
}
function syncHttp3Availability() {
const root = $("http3-listener-rows"), note = $("http3-listeners-dirty"), save = $("http3-save"); if (!root || !note || !save) return;
const blocked = settingsDirty.has("listeners-form"); note.hidden = !blocked;
for (const row of root.children) row.sync?.(blocked);
save.disabled = blocked || root.children.length === 0;
}
function bindings(parent, selected = [], selectDefault = false) {
const choices = new Map(), listeners = new Map(), watchers = [], box = el("div", undefined, "listener-choices vh-wide"); parent.append(box); box.append(label("p", "listenerChooseHint", "fine"));
const items = envelope?.config.listeners?.endpoints || [], autoIds = new Set();
if (selectDefault && !selected.length) {
const hasPort = (endpoint, port) => [endpoint.binding?.address, ...(endpoint.additional_addresses || [])].filter(Boolean).some(address => String(address).match(/:(\d+)$/)?.[1] === String(port));
const candidates = items.filter(endpoint => endpoint.enabled !== false);
const prefer = (tls, port) => candidates.find(endpoint => endpoint.scope === "public" && !!endpoint.binding?.tls === tls && hasPort(endpoint, port)) || candidates.find(endpoint => !!endpoint.binding?.tls === tls && hasPort(endpoint, port));
const http80 = prefer(false, 80);
if (http80) autoIds.add(http80.id);
if (!autoIds.size) { const fallback = candidates.find(endpoint => !endpoint.binding?.tls) || candidates[0]; if (fallback) autoIds.add(fallback.id); }
}
const notify = () => watchers.forEach(run => run());
for (const endpoint of items) {
const checked = selected.includes(endpoint.id) || autoIds.has(endpoint.id); const input = field(box, `bind-${endpoint.id.replace(/[^A-Za-z0-9_-]/g, "-")}`, "listenerEndpoint", checked, "checkbox");
const binding = endpoint.binding || {}, protocols = [binding.tls ? "HTTPS" : "HTTP"]; if (binding.tls && binding.http2 !== false) protocols.push("H2"); if (!binding.tls && binding.h2c) protocols.push("H2C"); if (binding.tls && (!Object.hasOwn(binding, "http3") || binding.http3 !== null)) protocols.push("H3");
const addresses = [endpoint.binding?.address, ...(endpoint.additional_addresses || [])].filter(Boolean).join(", ");
input.nextElementSibling.textContent = `${endpoint.name || endpoint.id} / ${protocols.join("+")} ${addresses} / ${t(endpoint.scope === "public" ? "listenerPublic" : "listenerInternal")}${endpoint.enabled === false ? ` / ${t("vhDisabled")}` : ""}`; delete input.nextElementSibling.dataset.i18n; choices.set(endpoint.id, input); listeners.set(endpoint.id, endpoint); input.addEventListener("change", notify);
}
for (const id of selected) if (!items.some(e => e.id === id)) { const input = field(box, `bind-${id}`, "listenerMissing", true, "checkbox"); input.nextElementSibling.textContent = `${t("listenerMissing")}: ${id}`; choices.set(id, input); input.addEventListener("change", notify); }
const read = () => [...choices].filter(([, input]) => input.checked).map(([id]) => id);
const capabilities = () => { const selectedIds = new Set(read()); let http2 = false, http3 = false; for (const [id, endpoint] of listeners) { if (!selectedIds.has(id) || endpoint.enabled === false) continue; const binding = endpoint.binding || {}; http2 ||= !!((binding.tls && binding.http2 !== false) || (!binding.tls && binding.h2c)); http3 ||= !!(binding.tls && (!Object.hasOwn(binding, "http3") || binding.http3 !== null)); } return { http2, http3 }; };
return { read, capabilities, onChange(run) { watchers.push(run); run(); } };
}
$("certificate-upload-form").addEventListener("submit", async event => {
event.preventDefault(); if (uploadBusy || !envelope || !authenticated || busy) return;
const form = event.currentTarget, epoch = authEpoch, revision = envelope.revision; let keyPem = ""; uploadBusy = true;
try {
await withFormSubmitState(form, event.submitter, async () => {
const cert = $("cert-import-pem").files[0], key = $("cert-import-key").files[0], id = $("cert-import-name").value.trim();
if ((envelope.config.tls?.certificates || []).some(c => c.id === id)) throw fieldError($("cert-import-name"), t("certDuplicate"));
if (!cert) throw fieldError($("cert-import-pem"), t("vhSelectCertificateFiles"));
if (!key) throw fieldError($("cert-import-key"), t("vhSelectCertificateFiles"));
if (cert.size > 131072) throw fieldError($("cert-import-pem"), t("vhCertificateTooLarge"));
if (key.size > 131072) throw fieldError($("cert-import-key"), t("vhCertificateTooLarge"));
keyPem = await key.text(); const uploaded = await api("/api/certificates/upload", { method: "POST", body: JSON.stringify({ cert_pem: await cert.text(), key_pem: keyPem, hosts: $("cert-import-domains").value.split(/[\n,]/).map(s => s.trim()).filter(Boolean) }) });
if (!authenticated || epoch !== authEpoch) return; if (envelope.revision !== revision) throw new Error(t("stale"));
const next = structuredClone(envelope.config); next.tls ||= {}; next.tls.certificates ||= []; next.tls.certificates.push({ ...uploaded.certificate, id, hosts: [] }); if ($("cert-import-default").checked) next.tls.default_certificate = id;
if (await saveSection("tls", next.tls || {})) { form.reset(); window.rustProxyCertificates?.close(); }
});
} catch (error) { if (epoch === authEpoch) toast(error.message, true); } finally { keyPem = ""; uploadBusy = false; }
});
window.rustProxyIntegrations = { bindings, hostnames, renderListeners, collectListeners, renderHttp2Settings, collectHttp2Listeners, syncHttp2Availability, renderHttp3Settings, collectHttp3Listeners, syncHttp3Availability, reset() { $("certificate-upload-form").reset(); $("named-listener-rows").replaceChildren(); $("http2-listener-rows")?.replaceChildren(); $("http3-listener-rows")?.replaceChildren(); } };
if (envelope) renderSettings();
})();