279 lines
26 KiB
JavaScript
279 lines
26 KiB
JavaScript
"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();
|
||
})();
|