v0.25.3
This commit is contained in:
1 parent
174389e5b1
commit
e6604e41fc
73 files changed
+1240
-201
No files matched your search
@@ -0,0 +1,107 @@
|
||||
let configHistoryEntries = [];
|
||||
function historyDate(value, options = {}) {
|
||||
try { return new Intl.DateTimeFormat(locale(), options).format(new Date(Number(value) * 1000)); }
|
||||
catch { return new Date(Number(value) * 1000).toLocaleString(); }
|
||||
}
|
||||
function historyDayKey(value) {
|
||||
const date = new Date(Number(value) * 1000);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
function historyMonthKey(value) { return historyDayKey(value).slice(0, 7); }
|
||||
function historyValue(value) {
|
||||
if (value === undefined) return t("configHistoryMissing");
|
||||
const text = JSON.stringify(value, null, 2);
|
||||
return text.length > 6000 ? `${text.slice(0, 6000)}\n...` : text;
|
||||
}
|
||||
function configDiff(before, after, path = "$", output = [], depth = 0) {
|
||||
if (output.length >= 200) return output;
|
||||
if (Object.is(before, after)) return output;
|
||||
const beforeObject = before && typeof before === "object" && !Array.isArray(before);
|
||||
const afterObject = after && typeof after === "object" && !Array.isArray(after);
|
||||
if (beforeObject && afterObject && depth < 10) {
|
||||
const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
|
||||
for (const key of keys) configDiff(before[key], after[key], `${path}.${key}`, output, depth + 1);
|
||||
return output;
|
||||
}
|
||||
if (JSON.stringify(before) !== JSON.stringify(after)) output.push({ path, before, after });
|
||||
return output;
|
||||
}
|
||||
function renderConfigRevisionDetail(detail) {
|
||||
const entry = detail?.revision;
|
||||
if (!entry) return;
|
||||
$("config-history-detail-title").textContent = `${t("configHistoryRevision")} ${entry.revision}`;
|
||||
$("config-history-detail-meta").textContent = `${historyDate(entry.time, { dateStyle: "medium", timeStyle: "short" })} · ${entry.actor}`;
|
||||
document.querySelectorAll(".config-history-revision").forEach(button => button.classList.toggle("active", Number(button.dataset.revision) === Number(entry.revision)));
|
||||
const diff = configDiff(entry.previous_config, entry.config);
|
||||
const box = $("config-history-diff"); box.replaceChildren();
|
||||
if (!entry.previous_config) { box.append(el("p", t("configHistoryBaseline"), "empty")); return; }
|
||||
if (!diff.length) { box.append(el("p", t("configHistoryNoChanges"), "empty")); return; }
|
||||
for (const change of diff) {
|
||||
const card = el("article", undefined, "config-history-change");
|
||||
card.append(el("strong", change.path, "config-history-change-path"));
|
||||
const values = el("div", undefined, "config-history-change-values");
|
||||
for (const [label, value] of [[t("configHistoryBefore"), change.before], [t("configHistoryAfter"), change.after]]) {
|
||||
const side = el("div"); side.append(el("span", label, "small-label"), el("pre", historyValue(value))); values.append(side);
|
||||
}
|
||||
card.append(values); box.append(card);
|
||||
}
|
||||
if (diff.length >= 200) box.append(el("p", t("configHistoryDiffLimit"), "fine"));
|
||||
}
|
||||
async function loadConfigRevision(revision) {
|
||||
try { renderConfigRevisionDetail(await api(`/api/config/history?revision=${encodeURIComponent(revision)}`)); }
|
||||
catch (error) { toast(error.message, true); }
|
||||
}
|
||||
function selectConfigHistoryDay(dayKey, entries) {
|
||||
document.querySelectorAll(".config-history-day").forEach(button => button.classList.toggle("is-selected", button.dataset.day === dayKey));
|
||||
const revisions = $("config-history-revisions"); revisions.replaceChildren();
|
||||
const sorted = [...entries].sort((a, b) => b.revision - a.revision);
|
||||
for (const entry of sorted) {
|
||||
const button = el("button", `${t("configHistoryRevisionShort")} ${entry.revision}`, "quiet config-history-revision");
|
||||
button.type = "button"; button.dataset.revision = entry.revision; button.title = `${entry.actor} · ${historyDate(entry.time, { dateStyle: "medium", timeStyle: "short" })}`;
|
||||
button.addEventListener("click", () => void loadConfigRevision(entry.revision)); revisions.append(button);
|
||||
}
|
||||
if (sorted[0]) void loadConfigRevision(sorted[0].revision);
|
||||
}
|
||||
function renderConfigHistory() {
|
||||
const calendar = $("config-history-calendar"); calendar.replaceChildren();
|
||||
const byMonth = new Map(), byDay = new Map();
|
||||
for (const entry of configHistoryEntries) {
|
||||
const month = historyMonthKey(entry.time), day = historyDayKey(entry.time);
|
||||
if (!byMonth.has(month)) byMonth.set(month, []); byMonth.get(month).push(entry);
|
||||
if (!byDay.has(day)) byDay.set(day, []); byDay.get(day).push(entry);
|
||||
}
|
||||
$("config-history-summary").textContent = configHistoryEntries.length ? t("configHistorySummary").replace("{count}", configHistoryEntries.length) : t("configHistoryEmpty");
|
||||
const months = [...byMonth.keys()].sort().reverse();
|
||||
for (const key of months) {
|
||||
const [yearText, monthText] = key.split("-"), year = Number(yearText), month = Number(monthText) - 1;
|
||||
const panel = el("section", undefined, "config-history-month");
|
||||
panel.append(el("h4", new Intl.DateTimeFormat(locale(), { month: "long", year: "numeric" }).format(new Date(year, month, 1))));
|
||||
const weekdays = el("div", undefined, "config-history-weekdays");
|
||||
for (let day = 0; day < 7; day++) weekdays.append(el("span", new Intl.DateTimeFormat(locale(), { weekday: "narrow" }).format(new Date(2024, 0, 7 + day))));
|
||||
panel.append(weekdays);
|
||||
const days = el("div", undefined, "config-history-days"), first = new Date(year, month, 1).getDay(), count = new Date(year, month + 1, 0).getDate();
|
||||
for (let blank = 0; blank < first; blank++) days.append(el("span", "", "config-history-day"));
|
||||
for (let day = 1; day <= count; day++) {
|
||||
const dayKey = `${yearText}-${monthText}-${String(day).padStart(2, "0")}`, entries = byDay.get(dayKey) || [];
|
||||
const node = el(entries.length ? "button" : "span", String(day), `config-history-day${entries.length ? " has-revisions" : ""}`);
|
||||
if (entries.length) {
|
||||
node.type = "button"; node.dataset.day = dayKey;
|
||||
node.append(el("small", `${entries.length} ${t("configHistoryEntries")}`, "config-history-day-count"));
|
||||
node.addEventListener("click", () => selectConfigHistoryDay(dayKey, entries));
|
||||
}
|
||||
days.append(node);
|
||||
}
|
||||
panel.append(days); calendar.append(panel);
|
||||
}
|
||||
if (!months.length) calendar.append(el("p", t("configHistoryEmpty"), "empty"));
|
||||
}
|
||||
async function loadConfigHistory() {
|
||||
$("config-history-summary").textContent = t("configHistoryLoading");
|
||||
try {
|
||||
const result = await api("/api/config/history?limit=512"); configHistoryEntries = result.revisions || []; renderConfigHistory();
|
||||
const latest = configHistoryEntries[0]; if (latest) selectConfigHistoryDay(historyDayKey(latest.time), configHistoryEntries.filter(entry => historyDayKey(entry.time) === historyDayKey(latest.time)));
|
||||
} catch (error) { $("config-history-summary").textContent = t("configHistoryFailed"); toast(error.message, true); }
|
||||
}
|
||||
$("config-history-open").addEventListener("click", () => { const dialog = $("config-history-dialog"); if (!dialog.open) dialog.showModal(); void loadConfigHistory(); });
|
||||
$("config-history-close").addEventListener("click", () => $("config-history-dialog").close());
|
||||
$("config-history-reload").addEventListener("click", () => void loadConfigHistory());
|
||||
@@ -6,7 +6,7 @@ function renderSettings() {
|
||||
if (!settingsDirty.has("response-form")) {
|
||||
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
||||
$("expose-request-id").checked = !!diagnostics.request_id;
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.2";
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.3";
|
||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
|
||||
@@ -1472,7 +1472,7 @@
|
||||
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
|
||||
</select>
|
||||
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.2"
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.3"
|
||||
spellcheck="false" />
|
||||
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
|
||||
<div class="switch-grid">
|
||||
@@ -2123,8 +2123,7 @@
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label">DESIRED STATE / SQLITE</span>
|
||||
<h2 data-i18n="manualConfig">Manual config</h2>
|
||||
</div><button class="outline" data-i18n="export" id="export-config">Export
|
||||
JSON</button>
|
||||
</div><div class="manual-config-tools"><button class="quiet" data-i18n="configHistory" id="config-history-open" type="button">Revision history</button><button class="outline" data-i18n="export" id="export-config" type="button">Export JSON</button></div>
|
||||
</div>
|
||||
<p class="fine" data-i18n="configHelp">A valid save creates one new revision. Invalid or
|
||||
stale changes leave the running configuration untouched.</p>
|
||||
@@ -2219,7 +2218,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="page-footer"><span>RustProxy <b>0.25.2</b></span><span id="footer-status">API - /
|
||||
<footer class="page-footer"><span>RustProxy <b>0.25.3</b></span><span id="footer-status">API - /
|
||||
dropped: 0</span></footer>
|
||||
</div>
|
||||
</main>
|
||||
@@ -2355,6 +2354,29 @@
|
||||
<p class="empty" data-i18n="tunnelsEmpty" id="tunnels-empty">No active tunnels.</p>
|
||||
</article>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="config-history-title" class="config-history-dialog" id="config-history-dialog">
|
||||
<div class="window-heading">
|
||||
<div><span class="small-label" data-i18n="configHistoryRetention">SQLITE / LAST 512 REVISIONS</span>
|
||||
<h2 data-i18n="configHistory" id="config-history-title">Revision history</h2>
|
||||
</div>
|
||||
<div class="window-tools"><button class="quiet" data-i18n="reload" id="config-history-reload" type="button">Reload</button><button class="outline" data-i18n="close" id="config-history-close" type="button">Close</button></div>
|
||||
</div>
|
||||
<div class="config-history-layout">
|
||||
<section class="card config-history-browser">
|
||||
<p class="fine" data-i18n="configHistoryHelp">Select a marked day to inspect the saved configuration revisions and their changes.</p>
|
||||
<p class="fine" id="config-history-summary" role="status"></p>
|
||||
<div class="config-history-months" id="config-history-calendar"></div>
|
||||
</section>
|
||||
<aside class="card config-history-detail">
|
||||
<div class="config-history-detail-head">
|
||||
<div><span class="small-label" data-i18n="configHistoryChanges">CHANGES</span><h3 id="config-history-detail-title">-</h3></div>
|
||||
<span class="fine" id="config-history-detail-meta"></span>
|
||||
</div>
|
||||
<div class="config-history-revisions" id="config-history-revisions"></div>
|
||||
<div class="config-history-diff" id="config-history-diff"><p class="empty" data-i18n="configHistorySelectDay">Select a day.</p></div>
|
||||
</aside>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="audit-dialog-title" class="audit-dialog" id="audit-dialog">
|
||||
<div class="window-heading">
|
||||
<div><span class="small-label" data-i18n="auditRetention">SQLITE / LAST 100 / RETAINED 10,000</span>
|
||||
|
||||
@@ -7619,3 +7619,173 @@ body[data-density="compact"] .page-footer {
|
||||
#web-service-dialog .web-service-page {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.manual-config-tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.config-history-dialog {
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
padding: 0;
|
||||
width: min(1180px, calc(100vw - 24px));
|
||||
max-width: calc(100vw - 24px);
|
||||
max-height: calc(100dvh - 28px);
|
||||
overflow: hidden;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 18px 70px var(--shadow-strong);
|
||||
}
|
||||
|
||||
.config-history-dialog::backdrop {
|
||||
background: var(--shadow-strong);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.config-history-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(380px, .95fr) minmax(420px, 1.05fr);
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
max-height: calc(100dvh - 90px);
|
||||
}
|
||||
|
||||
.config-history-browser,
|
||||
.config-history-detail {
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.config-history-months {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.config-history-month {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 9px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.config-history-month h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.config-history-weekdays,
|
||||
.config-history-days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.config-history-weekdays span {
|
||||
color: var(--muted);
|
||||
font: 9px var(--mono);
|
||||
text-align: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.config-history-day {
|
||||
min-height: 34px;
|
||||
padding: 4px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
font: 10px var(--mono);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
button.config-history-day.has-revisions {
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
border-color: var(--line-strong, var(--line));
|
||||
background: var(--surface, var(--bg));
|
||||
}
|
||||
|
||||
button.config-history-day.has-revisions:hover,
|
||||
button.config-history-day.is-selected {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.config-history-day-count {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font-size: 8px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.config-history-detail-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.config-history-detail-head h3 {
|
||||
margin: 3px 0 0;
|
||||
}
|
||||
|
||||
.config-history-revisions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.config-history-revision {
|
||||
font: 10px var(--mono);
|
||||
}
|
||||
|
||||
.config-history-revision.active {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.config-history-diff {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.config-history-change {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.config-history-change-path {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font: 10px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.config-history-change-values {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.config-history-change pre {
|
||||
margin: 3px 0 0;
|
||||
max-height: 160px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
font: 10px var(--mono);
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.config-history-layout { grid-template-columns: 1fr; overflow: auto; }
|
||||
.config-history-browser, .config-history-detail { overflow: visible; }
|
||||
.config-history-change-values { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
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 }],
|
||||
cache: { enabled: false }, transform: { enabled: false }, rate_limit: {}, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
|
||||
cache: { enabled: false }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl: null, 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); }
|
||||
@@ -296,7 +296,11 @@
|
||||
source: action === "web_server" ? { type: "web_server", web_service_id: str("location-web-service", r) } : { type: "reverse_proxy" },
|
||||
backend_id: action === "proxy" ? (str("location-backend", r) || null) : null,
|
||||
enabled: yes("location-enabled", r), cache: action === "proxy" ? (str("location-cache", r) === "" ? null : str("location-cache", r) === "on") : false,
|
||||
acl: r.readAcl(), headers: r.readHeaders(),
|
||||
acl_profile_id: str("location-acl-profile", r) || null,
|
||||
acl: r.readAcl(),
|
||||
country_access: { mode: str("location-country-mode", r), countries: lines(str("location-country-list", r)).map(value => value.toUpperCase()) },
|
||||
method_policy: { mode: str("location-method-mode", r), methods: lines(str("location-method-list", r)).map(value => value.toUpperCase()) },
|
||||
headers: r.readHeaders(),
|
||||
rewrite: { mode: handled ? rewriteMode : "preserve", value: handled && rewriteMode !== "preserve" ? read("location-rewrite-value", r).value.trim() : "" },
|
||||
response: action === "return" ? { status: Number(str("location-response-status", r)), content_type: str("location-response-type", r), body: read("location-response-body", r).value } : null
|
||||
};
|
||||
@@ -328,7 +332,18 @@
|
||||
field(returnBox, "location-response-body", "locationResponseBody", value.response?.body || "", "textarea", { rows: 5, maxLength: 65536 });
|
||||
note(handlingBody, "locationRewriteHelp");
|
||||
|
||||
const extra = el("details", undefined, "vh-wide location-section"), accessBody = el("div", undefined, "vh-grid"); extra.append(text("summary", "locationAccess"), accessBody); body.append(extra); const acl = aclEditor(accessBody, value.acl, sync); row.readAcl = () => acl.read();
|
||||
const extra = el("details", undefined, "vh-wide location-section"), accessBody = el("div", undefined, "vh-grid"); extra.append(text("summary", "locationAccess"), accessBody); body.append(extra);
|
||||
const locationProfile = field(accessBody, "location-acl-profile", "locationAclProfile", value.acl_profile_id || "", "select");
|
||||
const locationNoProfile = text("option", "vhAclProfileNone"); locationNoProfile.value = "";
|
||||
locationProfile.append(locationNoProfile, ...(envelope?.config.acl_profiles || []).map(profile => { const option = el("option", profile.name || profile.id); option.value = profile.id; return option; }));
|
||||
locationProfile.value = value.acl_profile_id || "";
|
||||
field(accessBody, "location-method-mode", "locationMethodMode", value.method_policy?.mode || "off", "select", { values: [["off", "vhOff"], ["allow", "locationMethodAllow"], ["block", "locationMethodBlock"]] });
|
||||
field(accessBody, "location-method-list", "locationMethodList", (value.method_policy?.methods || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
|
||||
note(accessBody, "locationMethodHelp");
|
||||
field(accessBody, "location-country-mode", "locationCountryMode", value.country_access?.mode || "off", "select", { values: [["off", "vhOff"], ["block", "vhCountryBlock"], ["allow", "vhCountryAllow"]] });
|
||||
field(accessBody, "location-country-list", "locationCountryList", (value.country_access?.countries || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
|
||||
note(accessBody, "locationCountryHelp");
|
||||
const acl = aclEditor(accessBody, value.acl, sync); row.readAcl = () => acl.read();
|
||||
const headerBox = el("details", undefined, "vh-wide location-section location-header-editor"), headerBody = el("div", undefined, "vh-grid"); headerBox.append(text("summary", "locationHeaders"), headerBody); body.append(headerBox);
|
||||
const headerToolbar = el("div", undefined, "vh-wide location-header-toolbar"), visualPane = el("div", undefined, "vh-wide vh-grid"), jsonPane = el("div", undefined, "vh-wide vh-grid"); jsonPane.hidden = true;
|
||||
const visualButton = button("headersVisualBuilder", () => { visualPane.hidden = false; jsonPane.hidden = true; }), jsonButton = button("headersAdvancedJson", () => { visualPane.hidden = true; jsonPane.hidden = false; }), docsButton = button("documentation", () => openLocationHelp("headers"));
|
||||
@@ -381,7 +396,12 @@
|
||||
const replacement = read("location-rewrite-value", row).value.trim();
|
||||
previewValues.rewrite.textContent = `${t(rewriteLabels[rewriteMode.value] || "locationRewritePreserve")}${replacement && !["preserve", "strip_prefix"].includes(rewriteMode.value) ? `: ${replacement}` : ""} · ${t("locationPreviewQueryPreserved")}`;
|
||||
}
|
||||
previewValues.access.textContent = yes("acl-use", row) ? t("locationPreviewCustomAcl") : t("locationPreviewVhostAcl");
|
||||
const accessParts = [];
|
||||
const profile = str("location-acl-profile", row); if (profile) accessParts.push(`${t("locationAclProfile")}: ${profile}`);
|
||||
const methodMode = str("location-method-mode", row); if (methodMode !== "off") accessParts.push(`${t("locationMethodMode")}: ${t(methodMode === "allow" ? "locationMethodAllow" : "locationMethodBlock")} ${lines(str("location-method-list", row)).join(", ")}`);
|
||||
const countryMode = str("location-country-mode", row); if (countryMode !== "off") accessParts.push(`${t("locationCountryMode")}: ${t(countryMode === "allow" ? "vhCountryAllow" : "vhCountryBlock")} ${lines(str("location-country-list", row)).join(", ")}`);
|
||||
if (yes("acl-use", row)) accessParts.push(t("locationPreviewCustomAcl"));
|
||||
previewValues.access.textContent = accessParts.length ? accessParts.join(" · ") : t("locationPreviewVhostAcl");
|
||||
try {
|
||||
const headerValue = editor.read(), requestRules = headerValue.request?.length || 0, responseRules = headerValue.response?.length || 0;
|
||||
previewValues.headers.textContent = `${requestRules} ${t("locationPreviewRequestRules")} / ${responseRules} ${t("locationPreviewResponseRules")}`;
|
||||
|
||||
@@ -145,10 +145,13 @@
|
||||
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 || {};
|
||||
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 });
|
||||
@@ -309,6 +312,7 @@
|
||||
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"), 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(), 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,
|
||||
|
||||
Reference in new issue
Block a user