v0.35.1: performance, AGPL licensing and documentation overhaul
- Web Server reads static files inline; proxy middleware chain flattened - About window with license summary and project links; AGPL-3.0-or-later - Rewritten documentation: feature list, screenshots, performance guide - Fix false spikes in per-Vhost request-rate charts
This commit is contained in:
1 parent
d3b492874b
commit
eba9f89283
135 files changed
+1425
-1064
No files matched your search
@@ -11,10 +11,11 @@ macro_rules! reader_doc {
|
||||
pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
reader_doc!("README.md", "project"),
|
||||
reader_doc!("CHANGELOG.md", "project"),
|
||||
reader_doc!("ROADMAP.md", "project"),
|
||||
reader_doc!("LICENSE", "project"),
|
||||
reader_doc!("docs/README.md", "guides"),
|
||||
reader_doc!("docs/FEATURES.md", "guides"),
|
||||
reader_doc!("docs/SCREENSHOTS.md", "guides"),
|
||||
reader_doc!("docs/INSTALL.md", "guides"),
|
||||
reader_doc!("docs/CONTAINER.md", "guides"),
|
||||
reader_doc!("docs/CONFIGURATION.md", "guides"),
|
||||
reader_doc!("docs/VHOSTS.md", "guides"),
|
||||
reader_doc!("docs/CUSTOM_LOCATIONS.md", "guides"),
|
||||
@@ -24,11 +25,9 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
reader_doc!("docs/CACHE.md", "guides"),
|
||||
reader_doc!("docs/RESPONSE_DELIVERY.md", "guides"),
|
||||
reader_doc!("docs/TRANSPORT.md", "guides"),
|
||||
reader_doc!("docs/TLS_POST_QUANTUM.md", "guides"),
|
||||
reader_doc!("docs/STREAMS.md", "guides"),
|
||||
reader_doc!("docs/ACME_PROVIDERS.md", "guides"),
|
||||
reader_doc!("docs/CERTIFICATES.md", "guides"),
|
||||
reader_doc!("docs/DOCKER_DISCOVERY.md", "guides"),
|
||||
reader_doc!("docs/INTEGRATIONS.md", "guides"),
|
||||
reader_doc!("docs/DASHBOARD.md", "guides"),
|
||||
reader_doc!("docs/MONITORING.md", "guides"),
|
||||
reader_doc!("docs/LOGS.md", "guides"),
|
||||
@@ -36,6 +35,6 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
reader_doc!("docs/MIGRATION.md", "guides"),
|
||||
reader_doc!("docs/SECURITY.md", "guides"),
|
||||
reader_doc!("docs/API.md", "guides"),
|
||||
reader_doc!("docs/API_ACCESS.md", "guides"),
|
||||
reader_doc!("docs/PERFORMANCE.md", "guides"),
|
||||
reader_doc!("docs/TROUBLESHOOTING.md", "guides"),
|
||||
];
|
||||
@@ -12,7 +12,7 @@ mod tests {
|
||||
assert!(documents.iter().any(|doc| doc["path"] == "docs/PHP_FPM.md"));
|
||||
assert!(documents
|
||||
.iter()
|
||||
.any(|doc| doc["path"] == "docs/TLS_POST_QUANTUM.md"));
|
||||
.any(|doc| doc["path"] == "docs/FEATURES.md"));
|
||||
assert!(documents.iter().all(|doc| {
|
||||
doc["path"].as_str().is_some_and(|value| !value.is_empty())
|
||||
&& doc["title"].as_str().is_some_and(|value| !value.is_empty())
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.35.0",
|
||||
"version": "0.35.1",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
@@ -9521,7 +9521,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Maximum body 16 KiB. Staging by default. Production requires explicit consent. Provider fields depend on the selected provider; see docs/ACME_PROVIDERS.md. Poll GET /api/acme.",
|
||||
"description": "Maximum body 16 KiB. Staging by default. Production requires explicit consent. Provider fields depend on the selected provider; see docs/CERTIFICATES.md. Poll GET /api/acme.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Origin",
|
||||
|
||||
+1
-2
@@ -1043,8 +1043,7 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
// GET returns the same standalone section shape accepted by PUT, at the
|
||||
// matching revision and without unrequested configuration fields.
|
||||
// GET returns the same section shape that PUT accepts.
|
||||
let read = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
|
||||
@@ -21,8 +21,7 @@ function syncConfigHistoryRestoreButton() {
|
||||
button.hidden = configHistorySelectedRevision === null;
|
||||
button.disabled = busy || !envelope || !Number.isSafeInteger(revision) || revision >= Number(envelope.revision);
|
||||
}
|
||||
// Compare leaf fields, not serialized object/array blobs. Object IDs are used
|
||||
// for stable array paths so reordering vhosts does not become a huge fake diff.
|
||||
// Leaf-level diff keyed by object IDs, so reordering is not a change.
|
||||
function configDiff(before, after, path = "$", output = [], depth = 0) {
|
||||
if (output.length >= 200 || Object.is(before, after)) return output;
|
||||
if (depth >= 24) {
|
||||
|
||||
@@ -144,9 +144,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
widen.addEventListener("click", () => resize(id, 1));
|
||||
const widthValue = el("span", `${width}/12`, "overview-layout-width-value");
|
||||
widthValue.title = t("overviewLayoutWidth");
|
||||
// The visual editor intentionally keeps uniformly sized previews.
|
||||
// Group sizing separately from ordering/hiding so both sets wrap
|
||||
// inside a narrow dialog rather than spilling into adjacent tiles.
|
||||
// Size and order controls are grouped separately so they wrap in narrow dialogs.
|
||||
const sizeActions = el("span", undefined, "overview-layout-size-actions");
|
||||
sizeActions.append(narrow, widthValue, widen);
|
||||
const orderActions = el("span", undefined, "overview-layout-order-actions");
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// One continuity policy for all browser-side counter-derived rates.
|
||||
// Background tabs cannot supply a valid instantaneous rate for an interval
|
||||
// spanning the pause. Keep the plotted history and rebase on the next sample.
|
||||
// Shared rate sampling: rebase after background-tab pauses instead of plotting a false spike.
|
||||
const rustProxyRateSampling = (() => {
|
||||
let generation = 0;
|
||||
const pause = () => { generation += 1; };
|
||||
@@ -19,6 +17,10 @@ const rustProxyRateSampling = (() => {
|
||||
at - previous.at <= Math.max(10000, seconds * 3000));
|
||||
}
|
||||
function difference(previous, values, at, intervalSeconds = 3) {
|
||||
if (previous && previous.generation === generation && Number.isFinite(at) &&
|
||||
Number.isFinite(previous.at) && at >= previous.at && at - previous.at < 250) {
|
||||
return { next: previous, deltas: null, seconds: null, rapid: true };
|
||||
}
|
||||
const next = { ...stamp(at), values: [...values] };
|
||||
if (!continuous(previous, at, intervalSeconds) ||
|
||||
values.some((value, i) => !Number.isFinite(value) || value < 0 ||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Theme palettes and appearance tokens. Runtime preference variables remain writable by JavaScript.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Theme palettes and appearance tokens */
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Shared reset, typography, controls, forms, tables, cards and overlay primitives.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Shared reset, typography, controls, forms, tables, cards and overlay primitives */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -1304,8 +1302,9 @@ body[data-density=compact] .page-footer {
|
||||
}
|
||||
|
||||
.about-dialog {
|
||||
width: calc(min(560px, 100vw - 24px));
|
||||
overflow: hidden;
|
||||
width: calc(min(640px, 100vw - 24px));
|
||||
max-height: calc(100vh - 24px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.about-dialog .window-heading button {
|
||||
@@ -1370,6 +1369,38 @@ body[data-density=compact] .page-footer {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.about-license {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.about-license h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.about-license p {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.about-license ul {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding-inline-start: 18px;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.about-links {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.about-repository {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
@@ -1381,7 +1412,9 @@ body[data-density=compact] .page-footer {
|
||||
.about-repository span:last-child {
|
||||
color: var(--muted);
|
||||
font: 10px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
html[data-table-density=compact] :is(table th, table td) {
|
||||
@@ -1479,7 +1512,6 @@ html[data-table-density=ultra] :is(table th, table td) {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Keep generated ACL instructions in the original metrics-example code style. */
|
||||
.path-permissions {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -1506,7 +1538,6 @@ html[data-table-density=ultra] :is(table th, table td) {
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
/* Permission guidance belongs to its generated command block, not the path inputs. */
|
||||
#certificate-external-acl-example.path-permissions {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Application startup, authentication, navigation, page chrome and draft dock.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Application startup, authentication, navigation, page chrome and draft dock */
|
||||
|
||||
html.app-loading .shell {
|
||||
visibility: hidden;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Overview metrics, charts, dashboard density, inline layout controls and presets.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Overview metrics, charts, dashboard density, inline layout controls and presets */
|
||||
|
||||
html[data-theme=ultra_dark] :is(.card, .stat, dialog) {
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, .18);
|
||||
@@ -76,8 +74,7 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
stroke-dasharray: var(--chart-grid-dash, 2 5);
|
||||
}
|
||||
|
||||
/* Optional X/Y scale readouts do not change SVG coordinates or chart ranges.
|
||||
* Labels are HTML, not distorted SVG text under preserveAspectRatio="none". */
|
||||
/* Axis labels are HTML overlays, not stretched SVG text. */
|
||||
.rp-chart-axis-host {
|
||||
position: relative;
|
||||
display: flow-root;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Shared service editors: Vhosts, backends, Web Server, Streams, routing and ACL pickers.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Shared service editors: Vhosts, backends, Web Server, Streams, routing and ACL pickers */
|
||||
|
||||
.host-chip-editor.field-invalid,
|
||||
.listener-choices.field-invalid {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Settings, certificates, integrations and Docker agent/service management.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Settings, certificates, integrations and Docker agent/service management */
|
||||
|
||||
.settings-tabs button.has-unsaved:after {
|
||||
content: "";
|
||||
@@ -1205,7 +1203,7 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
/* 0.34.13: global security policy forms, grouped with clear control boundaries. */
|
||||
/* Global security policy forms */
|
||||
.global-policy-form .global-policy-section {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Traffic, cache, transformations, live/stored logs and configuration history.
|
||||
* Order is defined in manifest.json; responsive rules are centralized in 70-responsive.css.
|
||||
*/
|
||||
/* Traffic, cache, transformations, live/stored logs and configuration history */
|
||||
|
||||
.certificate-card .pill,
|
||||
.cache-miss-key,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* Responsive rules. Each viewport, container and reduced-motion condition is declared once.
|
||||
* Historical overrides were resolved before consolidation; preserve the documented order.
|
||||
*/
|
||||
/* Responsive rules */
|
||||
|
||||
@media (min-width:1201px) {
|
||||
#panel-overview .stats-grid {
|
||||
@@ -1052,7 +1050,8 @@
|
||||
}
|
||||
|
||||
.named-listener-row .vh-grid,
|
||||
.about-meta {
|
||||
.about-meta,
|
||||
.about-links {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -1388,7 +1387,6 @@
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
/* The standalone auth heading keeps its later, unconditional font size. */
|
||||
.login-section h1:where(:not(.auth-main h1)) {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
@@ -91,9 +91,3 @@ When replacing a previously built checkout with a source ZIP, include the root
|
||||
the [generated-CSS recovery steps](../../../../scripts/README.md#missing-generated-css-during-rust-compilation).
|
||||
Source preflight does not generate Cargo outputs. The build tracks the generated
|
||||
bundle as well as its inputs, and rejects a missing or empty output.
|
||||
|
||||
The optional [dashboard browser regression runner](../../../../tools/dev/README-dashboard-css.md)
|
||||
assembles the actual HTML and JavaScript against synthetic API fixtures and
|
||||
compares a supplied baseline stylesheet with the built candidate. It includes
|
||||
breakpoint/container boundaries and dynamic editor states. Its dependencies are
|
||||
development-only; they are not part of the application build or deployment.
|
||||
+34
-3
@@ -1,4 +1,4 @@
|
||||
/* A deliberately self-contained documentation layout. */
|
||||
/* Documentation Reader layout. */
|
||||
* {
|
||||
box-sizing: border-box
|
||||
}
|
||||
@@ -325,6 +325,38 @@ main {
|
||||
color: #52606d
|
||||
}
|
||||
|
||||
.reader-table {
|
||||
overflow-x: auto;
|
||||
margin: 14px 0
|
||||
}
|
||||
|
||||
.reader-table table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 14px
|
||||
}
|
||||
|
||||
.reader-table :is(th, td) {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid #e0e5ea;
|
||||
line-height: 1.5
|
||||
}
|
||||
|
||||
.reader-table th {
|
||||
background: #f4f6f8
|
||||
}
|
||||
|
||||
.reader-image {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px dashed #c9d1d9;
|
||||
color: #52606d;
|
||||
font-size: 13px
|
||||
}
|
||||
|
||||
.reader-empty {
|
||||
color: #687581;
|
||||
padding: 10px 8px
|
||||
@@ -344,8 +376,7 @@ main {
|
||||
padding: 18px
|
||||
}
|
||||
}
|
||||
/* Authenticated documentation uses the same stored corner preference as the
|
||||
main panel, while retaining its original flat shape for "Original". */
|
||||
/* Corner style follows the panel preference. */
|
||||
html[data-corner-style=square] {
|
||||
--rp-docs-radius-surface: 0px;
|
||||
--rp-docs-radius-control: 0px;
|
||||
|
||||
+23
-2
@@ -10,6 +10,7 @@
|
||||
let documents = [], docsByPath = new Map(), currentDocument = null, activeTab = "reader";
|
||||
let channel = null; try { channel = new BroadcastChannel("rustproxy-admin-session"); } catch { }
|
||||
const t = key => translations[key] ?? fallback[key] ?? key;
|
||||
const REPOSITORY_RAW = "https://git.linuxiarz.pl/gru/RustProxy/raw/branch/master";
|
||||
|
||||
function docsConfirm(message) {
|
||||
return new Promise(resolve => {
|
||||
@@ -78,12 +79,22 @@
|
||||
}
|
||||
|
||||
function appendInline(parent, text, basePath) {
|
||||
const pattern = /(`[^`]+`|\[[^\]]+\]\([^)]+\))/g; let offset = 0;
|
||||
const pattern = /(`[^`]+`|\*\*[^*]+\*\*|!?\[[^\]]*\]\([^)]+\))/g; let offset = 0;
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
if (match.index > offset) parent.append(document.createTextNode(text.slice(offset, match.index)));
|
||||
const token = match[0];
|
||||
if (token.startsWith("`")) {
|
||||
const code = document.createElement("code"); code.textContent = token.slice(1, -1); parent.append(code);
|
||||
} else if (token.startsWith("**")) {
|
||||
const strong = document.createElement("strong"); appendInline(strong, token.slice(2, -2), basePath); parent.append(strong);
|
||||
} else if (token.startsWith("!")) {
|
||||
const parsed = /^!\[([^\]]*)\]\(([^)]+)\)$/.exec(token);
|
||||
const caption = document.createElement("a"); caption.className = "reader-image";
|
||||
caption.textContent = `${t("docsImage")}: ${parsed[1] || parsed[2]}`;
|
||||
const target = parsed[2].trim();
|
||||
caption.href = /^https?:\/\//i.test(target) ? target : `${REPOSITORY_RAW}/${normalizeDocPath(basePath, target)}`;
|
||||
caption.target = "_blank"; caption.rel = "noreferrer";
|
||||
parent.append(caption);
|
||||
} else {
|
||||
const parsed = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token); const anchor = document.createElement("a");
|
||||
anchor.textContent = parsed[1]; const target = parsed[2].trim();
|
||||
@@ -106,7 +117,7 @@
|
||||
function renderMarkdown(documentInfo) {
|
||||
const target = element("docs-reader-content"); target.replaceChildren();
|
||||
const lines = documentInfo.content.replaceAll("\r\n", "\n").split("\n");
|
||||
let code = null, list = null, paragraph = [];
|
||||
let code = null, list = null, table = null, paragraph = [];
|
||||
const flushParagraph = () => { if (!paragraph.length) return; const p = document.createElement("p"); appendInline(p, paragraph.join(" ").trim(), documentInfo.path); target.append(p); paragraph = []; };
|
||||
const closeList = () => { list = null; };
|
||||
for (const line of lines) {
|
||||
@@ -117,6 +128,16 @@
|
||||
continue;
|
||||
}
|
||||
if (code) { code.lines.push(line); code.node.textContent = code.lines.join("\n"); continue; }
|
||||
if (/^\s*\|.*\|\s*$/.test(line)) {
|
||||
flushParagraph(); closeList();
|
||||
const cells = line.trim().slice(1, -1).split("|").map(cell => cell.trim());
|
||||
if (cells.every(cell => /^:?-{2,}:?$/.test(cell))) continue;
|
||||
if (!table) { table = document.createElement("table"); const wrap = document.createElement("div"); wrap.className = "reader-table"; wrap.append(table); target.append(wrap); }
|
||||
const row = document.createElement("tr");
|
||||
for (const cell of cells) { const node = document.createElement(table.rows.length ? "td" : "th"); appendInline(node, cell, documentInfo.path); row.append(node); }
|
||||
table.append(row); continue;
|
||||
}
|
||||
table = null;
|
||||
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
|
||||
if (heading) { flushParagraph(); closeList(); const h = document.createElement(`h${heading[1].length}`); appendInline(h, heading[2], documentInfo.path); target.append(h); continue; }
|
||||
const bullet = /^\s*[-*]\s+(.+)$/.exec(line), ordered = /^\s*\d+\.\s+(.+)$/.exec(line);
|
||||
|
||||
+39
-11
@@ -3143,7 +3143,7 @@
|
||||
<p class="fine" data-i18n="tlsPqcHelp">TLS 1.3 only: prioritize hybrid
|
||||
X25519MLKEM768 with classic key exchanges as fallback. This is a key exchange
|
||||
group, not a cipher suite. Disabled by default.</p>
|
||||
<a href="/docs#doc=docs%2FTLS_POST_QUANTUM.md" target="_blank"
|
||||
<a href="/docs#doc=docs%2FTRANSPORT.md" target="_blank"
|
||||
rel="noopener noreferrer" data-i18n="tlsPqcDocs">Learn about PQC, compatibility
|
||||
and testing (documentation)</a>
|
||||
</div>
|
||||
@@ -4300,24 +4300,52 @@ log_errors = On</code></pre>
|
||||
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
|
||||
<div><span class="small-label">RustProxy</span><strong>__RUSTPROXY_VERSION__</strong></div>
|
||||
</div>
|
||||
<p data-i18n="aboutTagline">Reverse proxy, web server, certificate manager and traffic monitor in a single
|
||||
service with a built-in administration panel.</p>
|
||||
<dl class="about-meta">
|
||||
<div>
|
||||
<dt data-i18n="aboutAuthor">Author</dt>
|
||||
<dd>Mateusz Gruszczynski @linuxiarz.pl</dd>
|
||||
<dd>Mateusz Gruszczyński · linuxiarz.pl</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="aboutLicense">License</dt>
|
||||
<dd data-i18n="aboutLicenseValue">GNU AGPL v3</dd>
|
||||
<dd data-i18n="aboutLicenseValue">GNU AGPL v3.0 or later</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="aboutCopyright">Copyright</dt>
|
||||
<dd>© 2026 Mateusz Gruszczyński</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="aboutSpdx">SPDX identifier</dt>
|
||||
<dd><code>AGPL-3.0-or-later</code></dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p data-i18n="aboutOpenSourceText">RustProxy is an open-source project. Its source code is publicly
|
||||
available for inspection, development and community contribution.</p>
|
||||
<a class="outline about-repository" href="https://git.linuxiarz.pl/gru/RustProxy" rel="noopener noreferrer"
|
||||
target="_blank"><span data-i18n="aboutRepository">Official source
|
||||
repository</span><span>git.linuxiarz.pl/gru/RustProxy</span></a>
|
||||
<a class="outline about-repository" href="https://rustproxy.linuxiarz.pl/" rel="noopener noreferrer"
|
||||
target="_blank"><span data-i18n="aboutPackagesRepository">Official package
|
||||
repository</span><span>rustproxy.linuxiarz.pl</span></a>
|
||||
<section class="about-license" aria-labelledby="about-license-title">
|
||||
<h3 class="small-label" data-i18n="aboutLicenseTitle" id="about-license-title">License summary</h3>
|
||||
<p data-i18n="aboutOpenSourceText">RustProxy is free and open-source software. You may use, study,
|
||||
modify and redistribute it under the terms of the GNU Affero General Public License, version 3
|
||||
or any later version.</p>
|
||||
<ul>
|
||||
<li data-i18n="aboutLicensePointUse">Use it for any purpose, including commercial deployments.</li>
|
||||
<li data-i18n="aboutLicensePointShare">Distributed copies and modified versions must stay under the
|
||||
same license and include their source code.</li>
|
||||
<li data-i18n="aboutLicensePointNetwork">If you run a modified version as a network service, its
|
||||
users must be able to obtain the modified source code.</li>
|
||||
<li data-i18n="aboutLicensePointWarranty">The software is provided without any warranty.</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div class="about-links">
|
||||
<a class="outline about-repository" href="https://rustproxy.linuxiarz.pl/" rel="noopener noreferrer"
|
||||
target="_blank"><span data-i18n="aboutPackagesRepository">Website and packages</span><span>rustproxy.linuxiarz.pl</span></a>
|
||||
<a class="outline about-repository" href="https://git.linuxiarz.pl/gru/RustProxy"
|
||||
rel="noopener noreferrer" target="_blank"><span data-i18n="aboutRepository">Source
|
||||
code</span><span>git.linuxiarz.pl/gru/RustProxy</span></a>
|
||||
<a class="outline about-repository" href="/docs#doc=docs%2FREADME.md" rel="noopener"
|
||||
target="_blank"><span data-i18n="aboutDocumentation">Documentation and API
|
||||
reference</span><span>/docs</span></a>
|
||||
<a class="outline about-repository" href="/docs#doc=LICENSE" rel="noopener" target="_blank"><span
|
||||
data-i18n="aboutLicenseText">Full license text</span><span>LICENSE</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</dialog>
|
||||
<div aria-live="polite" class="toast" hidden="" id="toast" popover="manual" role="status"></div>
|
||||
|
||||
@@ -24,7 +24,6 @@ const group = (key, advanced = false) => {
|
||||
const body = el("div", undefined, "vh-grid"); box.append(body); container.append(box); return body;
|
||||
};
|
||||
const note = (parent, key) => { const node = text("p", key, "fine vh-wide"); parent.append(node); return node; };
|
||||
// Presentation-only subgroup: all existing inputs remain inside their original form.
|
||||
const subsection = (parent, key) => {
|
||||
const box = el("section", undefined, "vh-wide vh-setting-section");
|
||||
box.append(text("h3", key));
|
||||
@@ -215,8 +214,7 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
const connection = el("div", undefined, "vh-wide vh-backend-switches vh-backend-connection"); main.append(connection);
|
||||
controls["member-private"] = field(connection, "member-private", "vhPrivate", value.allow_private, "checkbox");
|
||||
const preserve = field(connection, "member-host", "vhPreserveHost", value.preserve_host === true, "checkbox");
|
||||
// Preserve Host is a request-authority choice, not an HTTP/1.1-only option.
|
||||
// The Rust forwarder keeps HTTP/2 :authority consistent with this value.
|
||||
// Preserve Host also applies to HTTP/2 :authority.
|
||||
|
||||
const advanced = el("details", undefined, "vh-wide vh-backend-advanced"), extra = el("div", undefined, "vh-grid"); advanced.append(text("summary", "advanced"), extra);
|
||||
field(extra, "member-allowlist", "vhAddressAllowlist", (value.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
|
||||
@@ -230,8 +228,7 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
field(extra, "member-max-concurrent", "vhMaxConcurrentRequests", value.max_concurrent_requests ?? 0, "number", { required: true, min: 0, max: 1000000 });
|
||||
field(extra, "member-queue-timeout", "vhBackendQueueTimeout", value.queue_timeout_ms ?? 0, "number", { required: true, min: 0, max: 60000 });
|
||||
note(extra, "vhBackendCapacityHelp");
|
||||
// The per-member settings live in the Health tab. Keep one persistent form
|
||||
// per backend, so switching the selection never discards unsaved edits.
|
||||
// One persistent form per backend keeps unsaved edits when switching.
|
||||
const healthPanel = el("section", undefined, "vh-grid vh-backend-health-panel");
|
||||
const customHealth = field(healthPanel, "member-health-custom", "backendHealthOverride", value.health_check != null, "checkbox");
|
||||
const health = value.health_check || {};
|
||||
@@ -305,9 +302,7 @@ function debugOriginIssues(members) {
|
||||
let parsed; try { parsed = new URL(member.url); } catch { return false; }
|
||||
const host = parsed.hostname.replace(/^\[|\]$/g, ""), ipHost = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(":");
|
||||
if (parsed.protocol !== "https:" || !ipHost) return false;
|
||||
// The troubleshooting preset is OPTIONAL, not a protocol compatibility rule.
|
||||
// Warn only when IP-host TLS verification may need an explicit identity;
|
||||
// HTTP/1.1, Auto and H2 plus preserve_host are all valid configurations.
|
||||
// Warn only when TLS verification of an IP host needs an explicit server name.
|
||||
return member.tls_verify === true && !String(member.tls_server_name || "").trim();
|
||||
});
|
||||
}
|
||||
@@ -498,9 +493,7 @@ function locationsEditor(parent, values) {
|
||||
row.addEventListener("input", () => { syncUi(); sync(); }); row.addEventListener("change", () => { syncUi(); sync(); });
|
||||
rows.append(row); syncUi();
|
||||
}
|
||||
// The main vhost backend is an internal fallback, not a Custom location.
|
||||
// An empty location list must stay empty so the editor cannot accidentally
|
||||
// delete or recreate the main route as user configuration.
|
||||
// The main backend is an internal fallback, never an editable location.
|
||||
for (const value of values) add(value);
|
||||
const bottom = el("div", undefined, "vh-wide location-toolbar location-toolbar-bottom"); bottom.append(button("locationAdd", () => { add(); sync(); }, "primary")); parent.append(bottom);
|
||||
form.querySelector(".vh-members").addEventListener("input", () => { for (const select of rows.querySelectorAll('[name="location-backend"]')) memberOptions(select); });
|
||||
|
||||
@@ -516,6 +516,7 @@ function renderStatus(status) {
|
||||
errors: metrics ? Number(metrics.errors) || 0 : existing.errors ?? 0,
|
||||
traffic: metrics ? vhostTrafficRate(id, metrics, trafficSnapshot, trafficNow, seenVhostTraffic) : existing.traffic ?? 0,
|
||||
latency: metrics ? Number(metrics.ewma_latency_ms) || 0 : existing.latency ?? 0,
|
||||
measured: Boolean(metrics) || Boolean(existing.measured),
|
||||
});
|
||||
}
|
||||
for (const key of vhostTrafficHistory.keys()) if (!seenVhostTraffic.has(key)) vhostTrafficHistory.delete(key);
|
||||
@@ -533,8 +534,10 @@ function renderStatus(status) {
|
||||
fill("vhostLatency", d => d.latency > 0 ? `${fmt(d.latency, 1)} ms` : "-"); fill("vhostTraffic", d => `${rateBytes(d.traffic)}/s`);
|
||||
const now = performance.now();
|
||||
for (const [id, data] of aggregate) {
|
||||
if (!data.measured) continue;
|
||||
const history = chartHistory.get(id) || { last: null, points: [], bucketSize: 1, pendingSum: 0, pendingCount: 0 };
|
||||
const sample = rustProxyRateSampling.difference(history.last, [data.requests], now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
if (sample.rapid) continue;
|
||||
const pausedRate = history.last !== null && sample.deltas === null;
|
||||
history.last = sample.next;
|
||||
const rate = sample.deltas ? sample.deltas[0] / sample.seconds : 0;
|
||||
|
||||
Reference in new issue
Block a user