v0.31.5
This commit is contained in:
1 parent
d942c1a35d
commit
75104f87ba
103 files changed
+1344
-370
No files matched your search
@@ -116,7 +116,7 @@ function appConfirm(message, options = {}) {
|
||||
}
|
||||
function apiArea(path) {
|
||||
const section = path.match(/^\/api\/config\/([^/?]+)/)?.[1];
|
||||
if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "geoip": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not_found_hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security" })[section] || "configuration";
|
||||
if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "geoip": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not_found_hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security", "user-agent-blocking": "security" })[section] || "configuration";
|
||||
if (path === "/api/config") return "configuration";
|
||||
if (path.startsWith("/api/traffic")) return "traffic";
|
||||
if (path.startsWith("/api/cache")) return "cache";
|
||||
@@ -345,6 +345,9 @@ function renderConfig() {
|
||||
$("host-policy-list").replaceChildren(...(config.host_policies || []).map((policy, index) => { const box = el("div", undefined, "entity"); box.append(el("span", policy.hosts.join(", "), "entity-title"), el("p", `${t("aclDefault")}: ${t(policy.acl.default || "deny")}\n${rulesText(policy.acl)}`, "entity-details")); const buttons = el("div", undefined, "entity-actions"); buttons.append(action(t("edit"), () => { $("policy-hosts").value = policy.hosts.join(", "); $("policy-default").value = policy.acl.default || "deny"; $("policy-rules").value = rulesText(policy.acl); const editor = $("security-policy-editor"); if (editor) { editor.open = true; editor.scrollIntoView({ block: "nearest" }); } }), action(t("remove"), async () => { if (await appConfirm(t("confirmDelete"), { danger: true })) { const next = structuredClone(config); next.host_policies.splice(index, 1); await saveSection("host-policies", next.host_policies); } })); box.append(buttons); return box; }));
|
||||
if (!config.host_policies?.length) $("host-policy-list").append(el("p", t("noPolicies"), "empty"));
|
||||
if ($("security-policy-count")) $("security-policy-count").textContent = String((config.host_policies || []).length);
|
||||
const uaPolicy = config.user_agent_blocking || {};
|
||||
if ($("global-ua-block-enabled")) $("global-ua-block-enabled").checked = uaPolicy.enabled === true;
|
||||
if ($("global-ua-block-patterns")) $("global-ua-block-patterns").value = (uaPolicy.patterns || []).join("\n");
|
||||
}
|
||||
async function saveRequest(path, payload) { if (busy) { toast(t("busy"), true); return false; } if (!authenticated || !envelope) return false; busy = true; const epoch = authEpoch; try { const value = await api(path, { method: "PUT", body: JSON.stringify(payload) }); if (!authenticated || epoch !== authEpoch) return false; envelope = value; if (path === "/api/config" || path === "/api/config/tls") await refreshCertificateInventory(); if (!authenticated || epoch !== authEpoch || !envelope) return false; renderConfig(); toast(t("saved")); await refresh(true); return true; } catch (e) { toast(e.message, true); if (e.status === 401) lock(); return false; } finally { busy = false; } }
|
||||
async function save(config) { return saveRequest("/api/config", { expected_revision: envelope?.revision, config }); }
|
||||
@@ -464,6 +467,7 @@ document.addEventListener("click", event => { if (dashboardSidebar?.classList.co
|
||||
document.addEventListener("keydown", event => { if (event.key === "Escape" && dashboardSidebar?.classList.contains("mobile-nav-open")) { setMobileMenu(false); mobileMenuToggle?.focus(); } });
|
||||
window.addEventListener("popstate", () => { setPanel(panelFromLocation(), false); setMobileMenu(false); });
|
||||
$("host-policy-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const hosts = [...new Set($("policy-hosts").value.split(",").map(h => h.trim().toLowerCase()).filter(Boolean))]; const acl = { default: $("policy-default").value, rules: parseRules($("policy-rules").value, $("policy-rules")) }; const next = structuredClone(envelope.config); next.host_policies = (next.host_policies || []).map(p => ({ ...p, hosts: p.hosts.filter(h => !hosts.includes(h)) })).filter(p => p.hosts.length); next.host_policies.push({ hosts, acl }); if (await saveSection("host-policies", next.host_policies)) { form.reset(); if ($("security-policy-editor")) $("security-policy-editor").open = false; } }); } catch (e) { toast(e.message, true); } });
|
||||
$("global-ua-block-form")?.addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const patterns = [...new Set($("global-ua-block-patterns").value.split(/\r?\n/).map(value => value.trim()).filter(Boolean))]; await saveSection("user-agent-blocking", { enabled: $("global-ua-block-enabled").checked, patterns }); }); } catch (e) { toast(e.message, true); } });
|
||||
$("password-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; try { await withFormSubmitState(form, event.submitter, async () => { if ($("new-password").value !== $("new-password-confirm").value) throw fieldError("new-password-confirm", t("passwordMismatch")); await api("/api/auth/password", { method: "POST", body: JSON.stringify({ current_password: $("current-password").value, new_password: $("new-password").value }) }); lock(); toast(t("passwordChanged")); }); } catch (e) { toast(e.message, true); } });
|
||||
$("revoke-sessions").addEventListener("click", async () => { if (!(await appConfirm(t("confirmRevoke"), { danger: true }))) return; try { await api("/api/auth/logout-all", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { toast(e.message, true); } });
|
||||
$("security-audit-open").addEventListener("click", () => { const dialog = $("audit-dialog"); if (!dialog.open) dialog.showModal(); void loadAudit(); });
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>RustProxy / Docs</title>
|
||||
<link rel="icon" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/docs/swagger-ui.css">
|
||||
<link rel="stylesheet" href="/docs/style.css">
|
||||
<script defer src="/docs/swagger-ui-bundle.js"></script>
|
||||
<script defer src="/docs/init.js"></script>
|
||||
<link rel="stylesheet" href="/docs/swagger-ui___RUSTPROXY_ASSET_ID__.css">
|
||||
<link rel="stylesheet" href="/docs/style___RUSTPROXY_ASSET_ID__.css">
|
||||
<script defer src="/docs/swagger-ui-bundle___RUSTPROXY_ASSET_ID__.js"></script>
|
||||
<script defer src="/docs/init___RUSTPROXY_ASSET_ID__.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
let inventory = null, offset = 0, lastTotal = 0, busy = false;
|
||||
let currentEntries = [], selectedIndex = null;
|
||||
let preferencesLoaded = false, preferenceSaveRunning = false, preferenceSaveQueued = false;
|
||||
let searchTimer = null;
|
||||
let searchTimer = null, aggregateField = "", activeView = "entries";
|
||||
const defaultColumnIds = ["time", "event", "status", "client_ip", "vhost", "record", "source"];
|
||||
const specialLabels = new Map([
|
||||
["time", "time"], ["event", "fileLogsEvent"], ["status", "status"],
|
||||
@@ -13,17 +13,57 @@
|
||||
let visibleColumns = new Set(defaultColumnIds);
|
||||
|
||||
|
||||
const CONSOLIDATION_STORAGE_KEY = "rp-file-logs-consolidate-vhosts";
|
||||
const consolidationToggle = $("file-log-consolidate-vhosts");
|
||||
try {
|
||||
const saved = localStorage.getItem(CONSOLIDATION_STORAGE_KEY);
|
||||
if (consolidationToggle && (saved === "1" || saved === "0")) consolidationToggle.checked = saved === "1";
|
||||
} catch { /* Browser storage can be unavailable; use the HTML default. */ }
|
||||
|
||||
const sourceSelect = () => $("file-log-source");
|
||||
function setView(view) {
|
||||
activeView = view === "counts" ? "counts" : "entries";
|
||||
for (const name of ["entries", "counts"]) {
|
||||
const selected = name === activeView;
|
||||
$("file-log-tab-" + name).classList.toggle("active", selected);
|
||||
$("file-log-tab-" + name).setAttribute("aria-selected", String(selected));
|
||||
$("file-log-tab-" + name).tabIndex = selected ? 0 : -1;
|
||||
$("file-log-view-" + name).hidden = !selected;
|
||||
}
|
||||
if (activeView === "counts" && aggregateField) void refresh(true);
|
||||
}
|
||||
|
||||
function sourceOption(source) {
|
||||
const label = `${source.label} · ${source.format} · ${bytes(Number(source.size) || 0)}`;
|
||||
const grouped = Number(source.member_count) > 1 ? ` · ${fmt(source.member_count)} ${t("fileLogsFiles")}` : "";
|
||||
const label = `${source.label}${grouped} · ${source.format} · ${bytes(Number(source.size) || 0)}`;
|
||||
const option = el("option", label); option.value = source.id; return option;
|
||||
}
|
||||
function displaySources(sources) {
|
||||
if (!$("file-log-consolidate-vhosts")?.checked) return sources;
|
||||
const output = [], groups = new Map();
|
||||
for (const source of sources) {
|
||||
if (!String(source.group_id || "").startsWith("vhost:")) { output.push(source); continue; }
|
||||
const id = source.group_id;
|
||||
let group = groups.get(id);
|
||||
if (!group) {
|
||||
group = { ...source, id, label: id.slice("vhost:".length), size: 0, member_count: 0, compressed: false };
|
||||
groups.set(id, group); output.push(group);
|
||||
}
|
||||
group.size += Number(source.size) || 0; group.member_count += 1;
|
||||
group.modified_ms = Math.max(Number(group.modified_ms) || 0, Number(source.modified_ms) || 0);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function renderInventory(value) {
|
||||
inventory = value;
|
||||
const select = sourceSelect();
|
||||
const selected = select.value || "all";
|
||||
const sources = Array.isArray(value?.sources) ? value.sources : [];
|
||||
let selected = select.value || "all";
|
||||
const rawSources = Array.isArray(value?.sources) ? value.sources : [];
|
||||
if ($("file-log-consolidate-vhosts")?.checked && selected !== "all") {
|
||||
const member = rawSources.find(source => source.id === selected);
|
||||
if (member?.group_id?.startsWith("vhost:")) selected = member.group_id;
|
||||
}
|
||||
const sources = displaySources(rawSources);
|
||||
const options = [Object.assign(el("option", t("fileLogsAllSources")), { value: "all" }), ...sources.map(sourceOption)];
|
||||
select.replaceChildren(...options);
|
||||
select.value = sources.some(source => source.id === selected) ? selected : "all";
|
||||
@@ -35,6 +75,13 @@
|
||||
}
|
||||
|
||||
const currentLimit = () => Math.max(1, Math.min(500, Number($("file-log-limit").value) || 100));
|
||||
async function copyText(value) {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try { await navigator.clipboard.writeText(value); toast(t("copied")); return; } catch { }
|
||||
}
|
||||
const area = document.createElement("textarea"); area.value = value; area.setAttribute("readonly", "");
|
||||
area.style.position = "fixed"; area.style.opacity = "0"; document.body.append(area); area.select(); document.execCommand("copy"); area.remove(); toast(t("copied"));
|
||||
}
|
||||
|
||||
function entryTime(entry) {
|
||||
if (entry.time_ms) return formatDateTime(Number(entry.time_ms));
|
||||
@@ -89,6 +136,11 @@
|
||||
if (normalized) availableColumns.add(normalized);
|
||||
}
|
||||
for (const id of visibleColumns) availableColumns.add(id);
|
||||
const suggestions = $("file-log-field-suggestions");
|
||||
if (suggestions) {
|
||||
const names = new Set(["user_agent", "client_ip", "vhost", "host", "path", "method", "status", "country", "source", ...fields]);
|
||||
suggestions.replaceChildren(...[...names].filter(Boolean).sort().map(name => Object.assign(el("option"), { value: normalizedField(name) || name })));
|
||||
}
|
||||
syncColumnPicker(); syncHeaders();
|
||||
}
|
||||
function syncHeaders() {
|
||||
@@ -166,6 +218,19 @@
|
||||
raw.append(summary, el("pre", JSON.stringify(entry, null, 2), "metrics-example"));
|
||||
detail.append(heading, fields, raw); cell.append(detail); row.append(cell); return row;
|
||||
}
|
||||
function renderAggregation(aggregation) {
|
||||
const rows = $("file-log-aggregate-rows"), empty = $("file-log-aggregate-empty"), state = $("file-log-aggregate-state");
|
||||
if (!rows || !empty || !state) return;
|
||||
const entries = Array.isArray(aggregation?.entries) ? aggregation.entries : [];
|
||||
rows.replaceChildren(...entries.map(item => {
|
||||
const row = el("tr"), value = String(item.value ?? ""), valueCell = el("td"), button = el("button", value, "quiet file-log-aggregate-copy");
|
||||
button.type = "button"; button.title = t("copyValue"); button.addEventListener("click", () => void copyText(value)); valueCell.append(button);
|
||||
row.append(valueCell, el("td", fmt(Number(item.count) || 0))); return row;
|
||||
}));
|
||||
empty.hidden = !aggregation || entries.length !== 0;
|
||||
state.textContent = aggregation ? `${aggregation.field} · ${entries.length}${aggregation.truncated ? ` · ${t("fileLogsAggregateTruncated")}` : ""}` : "";
|
||||
}
|
||||
|
||||
function renderRows(entries = currentEntries) {
|
||||
currentEntries = entries;
|
||||
const ids = orderedColumns().filter(id => visibleColumns.has(id));
|
||||
@@ -189,6 +254,12 @@
|
||||
const source = sourceSelect().value || "all";
|
||||
if (source !== "all") params.set("source", source);
|
||||
const q = $("file-log-q").value.trim(); if (q) params.set("q", q);
|
||||
if ($("file-log-consolidate-vhosts")?.checked) params.set("consolidate_vhosts", "true");
|
||||
if (activeView === "counts" && aggregateField) {
|
||||
params.set("aggregate_field", aggregateField);
|
||||
params.set("aggregate_min_count", String(Math.max(2, Number($("file-log-aggregate-min")?.value) || 2)));
|
||||
params.set("aggregate_limit", String(Math.max(1, Number($("file-log-aggregate-limit")?.value) || 50)));
|
||||
}
|
||||
params.set("offset", String(offset)); params.set("limit", String(currentLimit()));
|
||||
return params.toString();
|
||||
}
|
||||
@@ -202,6 +273,7 @@
|
||||
lastTotal = Number(value.total) || 0; selectedIndex = null;
|
||||
applyFields(Array.isArray(value.fields) ? value.fields : []);
|
||||
renderRows(Array.isArray(value.entries) ? value.entries : []);
|
||||
renderAggregation(value.aggregation || null);
|
||||
const from = lastTotal ? offset + 1 : 0, to = Math.min(offset + currentLimit(), lastTotal);
|
||||
$("file-log-summary").textContent = t("fileLogsSummary")
|
||||
.replace("{from}", fmt(from)).replace("{to}", fmt(to)).replace("{total}", fmt(lastTotal))
|
||||
@@ -213,12 +285,14 @@
|
||||
finally { busy = false; $("file-logs-refresh").disabled = false; }
|
||||
}
|
||||
function reset() {
|
||||
inventory = null; offset = 0; lastTotal = 0; currentEntries = []; selectedIndex = null;
|
||||
inventory = null; offset = 0; lastTotal = 0; currentEntries = []; selectedIndex = null; aggregateField = "";
|
||||
preferencesLoaded = false; preferenceSaveRunning = false; preferenceSaveQueued = false;
|
||||
availableColumns = new Set(defaultColumnIds); visibleColumns = new Set(defaultColumnIds);
|
||||
sourceSelect().replaceChildren(Object.assign(el("option", t("fileLogsAllSources")), { value: "all" }));
|
||||
$("file-log-rows").replaceChildren(); syncColumnPicker(); syncHeaders();
|
||||
$("file-log-rows").replaceChildren(); $("file-log-aggregate-rows")?.replaceChildren(); renderAggregation(null); syncColumnPicker(); syncHeaders();
|
||||
}
|
||||
$("file-log-tab-entries").addEventListener("click", () => setView("entries"));
|
||||
$("file-log-tab-counts").addEventListener("click", () => setView("counts"));
|
||||
$("file-logs-open").addEventListener("click", () => { $("logs-menu").open = false; setPanel("filelogs"); });
|
||||
$("settings-file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
||||
$("file-logs-refresh").addEventListener("click", () => refresh(true));
|
||||
@@ -226,6 +300,12 @@
|
||||
$("file-log-q").addEventListener("input", () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => void refresh(true), 250); });
|
||||
$("file-log-limit").addEventListener("change", () => refresh(true));
|
||||
sourceSelect().addEventListener("change", () => refresh(true));
|
||||
consolidationToggle?.addEventListener("change", () => {
|
||||
try { localStorage.setItem(CONSOLIDATION_STORAGE_KEY, consolidationToggle.checked ? "1" : "0"); } catch { }
|
||||
if (inventory) renderInventory(inventory);
|
||||
void refresh(true);
|
||||
});
|
||||
$("file-log-aggregate-form")?.addEventListener("submit", event => { event.preventDefault(); aggregateField = $("file-log-aggregate-field").value.trim().toLowerCase(); if (!aggregateField) { renderAggregation(null); return; } void refresh(true); });
|
||||
$("file-logs-prev").addEventListener("click", () => { offset = Math.max(0, offset - currentLimit()); void refresh(); });
|
||||
$("file-logs-next").addEventListener("click", () => { if (offset + currentLimit() < lastTotal) { offset += currentLimit(); void refresh(); } });
|
||||
syncColumnPicker(); syncHeaders();
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<title>RustProxy / Control room</title>
|
||||
<link href="/favicon.svg" rel="icon" />
|
||||
<link href="/assets/style.css?v=__RUSTPROXY_VERSION__" rel="stylesheet" />
|
||||
<link href="/assets/style___RUSTPROXY_ASSET_ID__.css" rel="stylesheet" />
|
||||
<!-- One ordered JS payload: no race between module fetches and session/config API. -->
|
||||
<script defer="" src="/assets/app.js?v=__RUSTPROXY_VERSION__"></script>
|
||||
<script defer="" src="/assets/app___RUSTPROXY_ASSET_ID__.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -1675,6 +1675,7 @@
|
||||
<code>error:"connection reset"</code>
|
||||
</div>
|
||||
</details>
|
||||
<label class="file-log-consolidate" title="Consolidate vhost log rotations"><input checked id="file-log-consolidate-vhosts" type="checkbox" /><span data-i18n="fileLogsConsolidateShort">Merge rotations</span></label>
|
||||
<button class="outline" data-i18n="refresh" id="file-logs-refresh"
|
||||
type="button">Refresh</button>
|
||||
</div>
|
||||
@@ -1697,6 +1698,25 @@
|
||||
<option>500</option>
|
||||
</select></label>
|
||||
</form>
|
||||
<div class="file-log-tabs" role="tablist" aria-label="Stored log views">
|
||||
<button class="file-log-tab active" id="file-log-tab-entries" type="button" role="tab" aria-controls="file-log-view-entries" aria-selected="true" data-i18n="fileLogsEntriesTab">Log entries</button>
|
||||
<button class="file-log-tab" id="file-log-tab-counts" type="button" role="tab" aria-controls="file-log-view-counts" aria-selected="false" data-i18n="fileLogsCountsTab">Repeated values</button>
|
||||
</div>
|
||||
<section class="file-log-advanced-parser" id="file-log-view-counts" role="tabpanel" aria-labelledby="file-log-tab-counts" hidden>
|
||||
<h3 data-i18n="fileLogsAdvancedParser">Advanced parser · repeated values</h3>
|
||||
<form class="file-log-aggregate-form" id="file-log-aggregate-form">
|
||||
<label><span data-i18n="fileLogsAggregateField">Field to count</span><input id="file-log-aggregate-field" list="file-log-field-suggestions" maxlength="128" placeholder="user_agent" /></label>
|
||||
<datalist id="file-log-field-suggestions"></datalist>
|
||||
<label><span data-i18n="fileLogsAggregateMin">Minimum repetitions</span><input id="file-log-aggregate-min" type="number" min="2" max="20000" value="2" /></label>
|
||||
<label><span data-i18n="fileLogsAggregateLimit">Top values</span><select id="file-log-aggregate-limit"><option>20</option><option selected>50</option><option>100</option><option>200</option></select></label>
|
||||
<button class="outline" data-i18n="fileLogsCountRepeats" type="submit">Count repeats</button>
|
||||
</form>
|
||||
<p class="fine" data-i18n="fileLogsAggregateHelp">Counts matching values across the bounded scanned log set, not only the current page. Use fields such as user_agent, client_ip, host, path or any parsed nested field.</p>
|
||||
<div class="table-scroll file-log-aggregate-table"><table><thead><tr><th data-i18n="fileLogsAggregateValue">Value</th><th data-i18n="fileLogsAggregateCount">Count</th></tr></thead><tbody id="file-log-aggregate-rows"></tbody></table></div>
|
||||
<p class="empty" data-i18n="fileLogsAggregateEmpty" id="file-log-aggregate-empty">No repeated values for the selected field.</p>
|
||||
<p class="fine" id="file-log-aggregate-state" role="status"></p>
|
||||
</section>
|
||||
<div id="file-log-view-entries" role="tabpanel" aria-labelledby="file-log-tab-entries">
|
||||
<div class="file-logs-table-toolbar">
|
||||
<p class="fine" id="file-log-summary" role="status"></p>
|
||||
<details class="log-column-picker file-log-column-picker">
|
||||
@@ -1717,6 +1737,7 @@
|
||||
<div class="file-logs-pager"><button class="quiet" data-i18n="previous" disabled
|
||||
id="file-logs-prev" type="button">Previous</button><button class="quiet"
|
||||
data-i18n="next" disabled id="file-logs-next" type="button">Next</button></div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<section class="panel" hidden="" id="panel-settings">
|
||||
@@ -2325,7 +2346,7 @@
|
||||
<summary aria-label="curl examples for HTTP BAN and PURGE" data-i18n-aria="cacheHttpInvalidationCurlLabel">i</summary>
|
||||
<div class="metric-info-popover" role="note">
|
||||
<p class="fine" data-i18n="cacheHttpInvalidationCurlIntro">Run curl from an allowlisted IP against a public vhost:</p>
|
||||
<code>curl -i -X PURGE 'https://site.example/assets/app.js?v=1'</code>
|
||||
<code>curl -i -X PURGE 'https://site.example/assets/app_abcdef0123456789.js'</code>
|
||||
<code>curl -i -X BAN 'https://site.example/assets/'</code>
|
||||
<code>curl -i -X BAN 'https://site.example/' -H 'X-RustProxy-Confirm-Wide: 1'</code>
|
||||
<p class="fine" data-i18n="cacheHttpInvalidationCurlNote">PURGE removes an exact URL; BAN invalidates a path prefix for 1 hour. No admin cookie is needed, but your IP must be allowed. A reverse proxy must forward these methods and trusted client IP information.</p>
|
||||
@@ -2944,6 +2965,20 @@
|
||||
</form>
|
||||
</details>
|
||||
</article>
|
||||
<article class="card security-policy-card">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="userAgentBlocking">USER-AGENT BLOCKING</span>
|
||||
<h2 data-i18n="globalUaBlockingTitle">Global User-Agent blocklist</h2>
|
||||
<p class="fine" data-i18n="globalUaBlockingHelp">Blocks matching User-Agent values on public proxy vhosts before an upstream is opened. Per-vhost lists are configured in Vhosts → Security.</p>
|
||||
</div>
|
||||
</div>
|
||||
<form class="edit-form security-inline-form" id="global-ua-block-form">
|
||||
<label class="check"><input id="global-ua-block-enabled" type="checkbox" /><span data-i18n="enabled">Enabled</span></label>
|
||||
<label><span data-i18n="uaBlockPatterns">Blocked patterns, one per line</span><textarea id="global-ua-block-patterns" rows="6" maxlength="32768" spellcheck="false" placeholder="BadBot curl/* *scanner*"></textarea></label>
|
||||
<p class="fine" data-i18n="uaBlockPatternHelp">Matching is case-insensitive. Plain text matches a substring; * is a wildcard. Requests without User-Agent are not blocked by this rule.</p>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply" type="submit">Validate, save and apply</button></div>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card security-policy-card">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="securityAccessControl">ACCESS
|
||||
@@ -3012,7 +3047,7 @@
|
||||
<option value="cache_key">Cache key</option>
|
||||
</select></div>
|
||||
<div><label data-i18n="cacheInvalidationValue" for="cache-invalidation-value">Value</label><input
|
||||
id="cache-invalidation-value" maxlength="8192" placeholder="/assets/app.js" required="" />
|
||||
id="cache-invalidation-value" maxlength="8192" placeholder="/assets/app___RUSTPROXY_ASSET_ID__.js" required="" />
|
||||
</div>
|
||||
<div><label data-i18n="cacheInvalidationHost" for="cache-invalidation-host">Host
|
||||
(optional)</label><input id="cache-invalidation-host" maxlength="253"
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>RustProxy / Sign in</title>
|
||||
<link rel="icon" href="/favicon.svg">
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<script defer src="/assets/login.js"></script>
|
||||
<link rel="stylesheet" href="/assets/style___RUSTPROXY_ASSET_ID__.css">
|
||||
<script defer src="/assets/login___RUSTPROXY_ASSET_ID__.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="auth-page auth-loading">
|
||||
|
||||
+74
-1
@@ -10191,4 +10191,77 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
|
||||
.cache-maintenance-info .metric-info-popover code+.fine {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
/* Stored Logs: compact header option and distinct analysis workspace. */
|
||||
.file-logs-heading-actions .file-log-consolidate {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .45rem;
|
||||
padding: .35rem .55rem;
|
||||
min-height: 0;
|
||||
color: var(--muted);
|
||||
font-size: .8rem;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-log-consolidate input { width: 14px; height: 14px; margin: 0; accent-color: var(--accent); }
|
||||
.file-log-consolidate:hover { color: var(--text); }
|
||||
.file-log-tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--line); margin: .9rem 0 1rem; }
|
||||
.file-log-tab {
|
||||
appearance: none; background: transparent; border: 0; border-bottom: 2px solid transparent;
|
||||
border-radius: 0; padding: .65rem 1rem; color: var(--muted); cursor: pointer;
|
||||
font: inherit; font-weight: 500;
|
||||
}
|
||||
.file-log-tab:hover { color: var(--text); }
|
||||
.file-log-tab.active { color: var(--text); border-bottom-color: var(--accent); }
|
||||
.file-log-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
#file-log-view-counts[hidden], #file-log-view-entries[hidden] { display: none !important; }
|
||||
.file-log-advanced-parser h3 { margin: 0 0 .7rem; font-size: 1rem; }
|
||||
@media (max-width: 700px) {
|
||||
.file-logs-heading-actions .file-log-consolidate { margin-inline-end: auto; }
|
||||
.file-log-tab { padding: .6rem .7rem; }
|
||||
}
|
||||
/* 0.31.5 traffic/log analysis actions */
|
||||
.traffic-copy-value,
|
||||
.file-log-aggregate-copy {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: copy;
|
||||
}
|
||||
.traffic-copy-value:hover,
|
||||
.traffic-copy-value:focus-visible,
|
||||
.file-log-aggregate-copy:hover,
|
||||
.file-log-aggregate-copy:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.file-log-advanced-parser {
|
||||
margin: 0 0 1rem;
|
||||
padding: .75rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius, 12px);
|
||||
}
|
||||
.file-log-aggregate-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 2fr) minmax(140px, 1fr) minmax(120px, 1fr) auto;
|
||||
gap: .75rem;
|
||||
align-items: end;
|
||||
margin-top: .75rem;
|
||||
}
|
||||
.file-log-aggregate-table {
|
||||
margin-top: .75rem;
|
||||
}
|
||||
.file-log-aggregate-table th:last-child,
|
||||
.file-log-aggregate-table td:last-child {
|
||||
width: 8rem;
|
||||
text-align: right;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.file-log-aggregate-form { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -4,6 +4,17 @@
|
||||
const chartPalette = ["var(--chart-accent)", "var(--chart-secondary)", "var(--chart-tertiary)", "var(--chart-quaternary)", "var(--chart-quinary)", "var(--chart-senary)", "var(--chart-septenary)", "var(--chart-octonary)"];
|
||||
const scope = () => $("traffic-scope")?.value || "*";
|
||||
const number = value => fmt(Number(value) || 0);
|
||||
async function copyText(value) {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
try { await navigator.clipboard.writeText(value); toast(t("copied")); return; } catch { }
|
||||
}
|
||||
const area = document.createElement("textarea"); area.value = value; area.setAttribute("readonly", "");
|
||||
area.style.position = "fixed"; area.style.opacity = "0"; document.body.append(area); area.select(); document.execCommand("copy"); area.remove(); toast(t("copied"));
|
||||
}
|
||||
function copyButton(value, className) {
|
||||
const button = el("button", value || "-", `${className} traffic-copy-value`); button.type = "button"; button.title = t("copyValue");
|
||||
button.addEventListener("click", () => void copyText(value || "-")); return button;
|
||||
}
|
||||
|
||||
function selectTab(name) {
|
||||
activeTab = name;
|
||||
@@ -53,7 +64,7 @@
|
||||
legend.replaceChildren(...shown.map((item, index) => {
|
||||
const row = el("li"), label = el("span"), swatch = el("i", undefined, "traffic-chart-swatch");
|
||||
swatch.style.background = chartPalette[index % chartPalette.length];
|
||||
const text = el("span", item.value || "-", "traffic-chart-label"); text.title = item.value || "-";
|
||||
const text = copyButton(item.value || "-", "traffic-chart-label"); text.title = `${t("copyValue")}: ${item.value || "-"}`;
|
||||
label.append(swatch, text); row.append(label, el("b", `${(item.count / total * 100).toFixed(1)}%`)); return row;
|
||||
}));
|
||||
}
|
||||
@@ -65,7 +76,7 @@
|
||||
if (empty) empty.hidden = values.length > 0;
|
||||
chart.replaceChildren(...values.map(item => {
|
||||
const count = Number(item.count) || 0, row = el("div", undefined, "traffic-bar-row");
|
||||
const label = el("span", item.value || "-", "traffic-bar-label"); label.title = item.value || "-";
|
||||
const label = copyButton(item.value || "-", "traffic-bar-label"); label.title = `${t("copyValue")}: ${item.value || "-"}`;
|
||||
const track = el("span", undefined, "traffic-bar-track"), fill = el("i", undefined, "traffic-bar-fill"); fill.style.width = `${Math.max(1.5, count / peak * 100)}%`; track.append(fill);
|
||||
const value = el("span", formatter(count), "traffic-bar-value"); value.title = total ? `${(count / total * 100).toFixed(1)}%` : "";
|
||||
row.append(label, track, value); return row;
|
||||
|
||||
@@ -256,6 +256,10 @@
|
||||
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 });
|
||||
const uaBlock = value.user_agent_blocking || {};
|
||||
field(g, "ua-block-enabled", "vhUaBlockEnabled", uaBlock.enabled === true, "checkbox");
|
||||
field(g, "ua-block-patterns", "vhUaBlockPatterns", (uaBlock.patterns || []).join("\n"), "textarea", { rows: 4, maxLength: 32768 });
|
||||
note(g, "vhUaBlockHelp");
|
||||
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 });
|
||||
@@ -463,6 +467,7 @@
|
||||
},
|
||||
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") }
|
||||
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") },
|
||||
user_agent_blocking: { enabled: yes("ua-block-enabled"), patterns: lines(str("ua-block-patterns")) }
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user