This commit is contained in:
Mateusz Gruszczyński committed 2026-10-08 09:42:10 +02:00
1 parent c24bc4279b
commit 3b3f0db377
84 files changed
+2686 -392

No files matched your search

+1
View File
@@ -409,6 +409,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
"/docs/V0.25.1_AUDIT.md"
)),
),
reader_doc!("docs/V0.28.0_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.27.3_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.27.2_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.27.1_IMPLEMENTATION.md", "historical"),
+5 -1
View File
@@ -70,6 +70,10 @@ async fn update_section(
config.prometheus =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"geoip" => {
config.geoip_updates =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
}
"administration" => {
config.administration =
serde_json::from_value(value).map_err(|e| invalid_section(&section, e))?
@@ -375,7 +379,7 @@ async fn traffic_status(
"capture_user_agent": snapshot.envelope.config.logging.capture_user_agent,
"capture_country": snapshot.envelope.config.logging.capture_country
},
"geoip": state.geoip.view(),
"geoip": state.geoip.load().view(),
"analytics": state.traffic.view(Some(scope)),
"vhosts": vhosts
})))
+272 -10
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.27.3",
"version": "0.28.0",
"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."
},
"servers": [
@@ -606,6 +606,9 @@
"prometheus": {
"$ref": "#/components/schemas/PrometheusSettings"
},
"geoip_updates": {
"$ref": "#/components/schemas/GeoIpUpdateSettings"
},
"host_policies": {
"type": "array",
"items": {
@@ -1593,6 +1596,13 @@
"expose_resident_age": {
"type": "boolean"
},
"via_header": {
"type": "string",
"nullable": true,
"maxLength": 256,
"default": "RustProxy",
"description": "Optional response Via field value appended by RustProxy cache handling. Null disables the header. This response-only setting does not invalidate cached objects."
},
"cold_max_hot_seconds": {
"type": "integer",
"minimum": 5,
@@ -2258,6 +2268,103 @@
"frontend"
]
},
"GeoIpUpdateSettings": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true
},
"format": {
"type": "string",
"enum": [
"csv_pair",
"mmdb"
],
"default": "csv_pair",
"description": "GeoIP database format. csv_pair uses separate IPv4/IPv6 range files; mmdb uses one native MaxMind DB file."
},
"ipv4_url": {
"type": "string",
"format": "uri",
"default": "https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv",
"description": "Direct HTTPS URL for the IPv4 CSV database."
},
"ipv6_url": {
"type": "string",
"format": "uri",
"default": "https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv6.csv",
"description": "Direct HTTPS URL for the IPv6 CSV database."
},
"mmdb_url": {
"type": "string",
"default": "https://github.com/sapics/ip-location-db/releases/download/latest/user-country.mmdb",
"description": "Direct HTTPS URL for a native MaxMind DB. May be empty only when MMDB automatic updates are disabled and country.mmdb is managed locally."
},
"ipv4_checksum_url": {
"type": "string",
"format": "uri",
"nullable": true,
"default": null,
"description": "Optional HTTPS URL containing the publisher SHA-256 for the IPv4 CSV."
},
"ipv6_checksum_url": {
"type": "string",
"format": "uri",
"nullable": true,
"default": null,
"description": "Optional HTTPS URL containing the publisher SHA-256 for the IPv6 CSV."
},
"mmdb_checksum_url": {
"type": "string",
"format": "uri",
"nullable": true,
"default": null,
"description": "Optional HTTPS URL containing the publisher SHA-256 for the MMDB file."
},
"max_bytes": {
"type": "integer",
"format": "int64",
"minimum": 1048576,
"maximum": 268435456,
"default": 268435456
},
"interval_seconds": {
"type": "integer",
"format": "int64",
"minimum": 60,
"maximum": 2678400,
"default": 86400
},
"release_url": {
"type": "string",
"format": "uri",
"nullable": true,
"deprecated": true,
"description": "Legacy release base URL accepted for migration from pre-direct-source GeoIP settings."
},
"checksum_url": {
"type": "string",
"format": "uri",
"nullable": true,
"deprecated": true,
"description": "Legacy checksum base URL accepted for migration. The former sapics checksum default is migrated to optional/no publisher checksum."
}
},
"required": [
"enabled",
"format",
"ipv4_url",
"ipv6_url",
"mmdb_url",
"ipv4_checksum_url",
"ipv6_checksum_url",
"mmdb_checksum_url",
"max_bytes",
"interval_seconds"
]
},
"PrometheusSettings": {
"type": "object",
"additionalProperties": false,
@@ -4472,15 +4579,49 @@
"type": "object",
"description": "Python/pip/Certbot capability used by managed certificate automation.",
"properties": {
"status": { "type": "string", "enum": ["checking", "initializing", "ready", "unavailable"] },
"mode": { "type": "string", "enum": ["none", "managed", "system"] },
"message": { "type": "string" },
"managed_directory": { "type": "string", "nullable": true },
"python": { "type": "boolean" },
"pip": { "type": "boolean" },
"certbot": { "type": "boolean" }
"status": {
"type": "string",
"enum": [
"checking",
"initializing",
"ready",
"unavailable"
]
},
"mode": {
"type": "string",
"enum": [
"none",
"managed",
"system"
]
},
"message": {
"type": "string"
},
"managed_directory": {
"type": "string",
"nullable": true
},
"python": {
"type": "boolean"
},
"pip": {
"type": "boolean"
},
"certbot": {
"type": "boolean"
}
},
"required": ["status", "mode", "message", "managed_directory", "python", "pip", "certbot"]
"required": [
"status",
"mode",
"message",
"managed_directory",
"python",
"pip",
"certbot"
]
}
},
"required": [
@@ -5276,6 +5417,68 @@
}
},
"description": "Scoped dynamic-compression policy. The enabled switch and individual codecs inherit by default; global Response codec switches are the hard availability ceiling and min_bytes remains global."
},
"GeoIpView": {
"type": "object",
"additionalProperties": false,
"properties": {
"loaded": {
"type": "boolean"
},
"ipv4_ranges": {
"type": "integer",
"minimum": 0
},
"ipv6_ranges": {
"type": "integer",
"minimum": 0
},
"source": {
"type": "string"
},
"format": {
"type": "string",
"enum": [
"csv_pair",
"mmdb"
]
}
},
"required": [
"loaded",
"ipv4_ranges",
"ipv6_ranges",
"source",
"format"
]
},
"GeoIpUpdateResult": {
"type": "object",
"additionalProperties": false,
"properties": {
"outcome": {
"type": "string",
"enum": [
"updated",
"current",
"busy"
]
},
"updated_files": {
"type": "array",
"items": {
"type": "string"
}
},
"database": {
"$ref": "#/components/schemas/GeoIpView"
}
},
"required": [
"outcome",
"updated_files",
"database"
]
}
}
},
@@ -7290,6 +7493,64 @@
}
}
},
"/api/geoip/update": {
"post": {
"summary": "Check, download and hot-reload GeoIP country databases now",
"description": "Download the configured direct GeoIP source(s), optionally verify publisher SHA-256 values, validate the selected CSV/MMDB format, atomically publish changed files and hot-reload future GeoIP lookups.",
"tags": [
"Settings"
],
"operationId": "post_api_geoip_update",
"responses": {
"200": {
"description": "GeoIP check completed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GeoIpUpdateResult"
}
}
}
},
"401": {
"description": "Authentication required or revoked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Host, origin or CSRF validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"502": {
"description": "Publisher check, download, validation or reload failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"security": [
{
"AdminSession": [],
"CsrfToken": []
}
]
}
},
"/api/cache/purge": {
"post": {
"summary": "Purge all cache entries or one vhost",
@@ -7956,7 +8217,7 @@
}
}
},
"description": "Preferred UI/control-plane save path. Replaces only the selected section while preserving every other section from the current revision. This is not a merge patch. Cache identity is independent from the global configuration revision: logging, listeners, TLS inventory, metrics and administration saves do not purge cached objects. Changes which can alter origin requests or cached responses (cache/transform/response/forwarding/vhosts) rotate the cache compatibility identity and purge incompatible hot/cold objects. Supported section names include docker-agents, vhosts, redirects, not_found_hosts, streams, listeners, http2, tls, pages, cache, transform, logging, forwarding, headers, prometheus, administration, acl-profiles, host-policies and response.",
"description": "Preferred UI/control-plane save path. Replaces only the selected section while preserving every other section from the current revision. This is not a merge patch. Cache identity is independent from the global configuration revision: logging, listeners, TLS inventory, metrics and administration saves do not purge cached objects. Changes which can alter origin requests or cached responses (cache/transform/response/forwarding/vhosts) rotate the cache compatibility identity and purge incompatible hot/cold objects. Supported section names include docker-agents, vhosts, redirects, not_found_hosts, streams, listeners, http2, tls, pages, cache, transform, logging, forwarding, headers, prometheus, geoip, administration, acl-profiles, host-policies and response.",
"parameters": [
{
"name": "section",
@@ -7975,6 +8236,7 @@
"forwarding",
"headers",
"prometheus",
"geoip",
"administration",
"http2",
"tls",
+31
View File
@@ -1,3 +1,34 @@
async fn geoip_update(
State(state): State<Arc<App>>,
Extension(session): Extension<Session>,
) -> Result<Json<crate::geoip::UpdateResult>, ApiError> {
let result = crate::geoip::update_now(&state).await.map_err(|error| {
tracing::warn!(error = %format!("{error:#}"), "manual GeoIP update failed");
ApiError(
StatusCode::BAD_GATEWAY,
format!("GeoIP update failed: {error:#}"),
)
})?;
state
.store
.audit(
&session.username,
"geoip.update",
&format!(
"outcome={}; files={}",
result.outcome,
if result.updated_files.is_empty() {
"none".to_string()
} else {
result.updated_files.join(",")
}
),
)
.await
.map_err(auth::internal)?;
Ok(Json(result))
}
async fn transform_status(State(state): State<Arc<App>>) -> Json<Value> {
let snapshot = state.config.load_full();
let global = &snapshot.envelope.config.transform;
+1
View File
@@ -342,6 +342,7 @@ pub fn router(state: Arc<App>) -> Router {
.route("/api/traffic", get(traffic_status))
.route("/api/cache", get(cache_status))
.route("/api/transform", get(transform_status))
.route("/api/geoip/update", post(geoip_update))
.route("/api/cache/purge", post(cache_purge))
.route(
"/api/cache/invalidate",
+1 -1
View File
@@ -156,7 +156,7 @@ fn status_payload(state: &App) -> Value {
let transform_resources = crate::transform::resource_usage(&snapshot.envelope.config.transform);
payload["cache"] = json!(&cache_view);
payload["transform_resources"] = json!(&transform_resources);
payload["geoip"] = json!(state.geoip.view());
payload["geoip"] = json!(state.geoip.load().view());
let system = state.diagnostics.view();
let cache_ram_bytes =
(cache_view.hot_allocated_bytes as u64).saturating_add(cache_view.transient_bytes);
+1
View File
@@ -396,6 +396,7 @@
("/api/acme/issue", "POST"),
("/api/acme/renew", "POST"),
("/api/docker/refresh", "POST"),
("/api/geoip/update", "POST"),
("/api/cache/purge", "POST"),
("/api/cache/invalidate", "POST"),
("/api/cache/bans", "GET"),
+1 -1
View File
@@ -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", "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" })[section] || "configuration";
if (path === "/api/config") return "configuration";
if (path.startsWith("/api/traffic")) return "traffic";
if (path.startsWith("/api/cache")) return "cache";
+22 -1
View File
@@ -359,7 +359,28 @@ function renderDiagnostics(status, appendSample = true) {
const l = status.listeners || {};
$("listener-pending").hidden = !l.restart_required;
$("listener-pending").textContent = t("restartPending");
$("listener-active").textContent = `${t("activeListeners")}: ${l.active_endpoints?.map(e => `${e.tls ? "HTTPS" : "HTTP"} ${e.address}${e.http3 ? " + H3/UDP" : ""}`).join(" | ") || `HTTP ${status.proxy_listen} / HTTPS ${status.tls_listen || t("notEnabled")}`}`;
const activeListeners = $("listener-active"), listenerItems = el("div", undefined, "listener-active-items");
let endpoints = Array.isArray(l.active_endpoints) ? l.active_endpoints : [];
if (!endpoints.length) {
endpoints = [];
if (status.proxy_listen) endpoints.push({ tls: false, address: status.proxy_listen, http3: false });
if (status.tls_listen) endpoints.push({ tls: true, address: status.tls_listen, http3: false });
}
for (const endpoint of endpoints) {
const item = el("div", undefined, "listener-active-item");
item.append(el("strong", endpoint.tls ? "HTTPS" : "HTTP", "listener-active-protocol"), el("code", endpoint.address || "-", "listener-active-address"));
const protocols = ["H1"];
if (endpoint.tls) {
if (endpoint.http2) protocols.push("H2");
if (endpoint.http3) protocols.push("H3/UDP");
} else if (endpoint.h2c) {
protocols.push("H2C");
}
for (const protocol of protocols) item.append(el("span", protocol, "listener-active-feature"));
listenerItems.append(item);
}
if (!endpoints.length) listenerItems.append(el("span", t("notEnabled"), "fine"));
activeListeners.replaceChildren(el("strong", t("activeListeners"), "listener-active-title"), listenerItems);
}
function renderHeaderRules(direction, rules) {
const target = $(`${direction}-header-rules`); target.replaceChildren();
+8 -1
View File
@@ -30,6 +30,8 @@ function renderSettings() {
$("cache-query").checked = !!c.bypass_query; $("cache-prefixes").value = (c.bypass_prefixes || []).join("\n");
$("cache-static-cookie").checked = c.static_ignore_cookies !== false; $("cache-static-extensions").value = (c.static_extensions || []).join("\n");
$("cache-status-header").checked = !!c.expose_status; $("cache-hits-header").checked = !!c.expose_hits; $("cache-rfc-header").checked = !!c.expose_cache_status; $("cache-resident-age-header").checked = !!c.expose_resident_age;
const viaHeader = c.via_header === undefined ? "RustProxy" : c.via_header;
$("cache-via-enabled").checked = viaHeader !== null; $("cache-via-value").value = viaHeader || "RustProxy"; syncCacheVia();
setCacheMemoryPresetSelection(["conservative", "recommended", "performance"].includes(c.memory_preset) ? c.memory_preset : null);
setCacheColdPresetSelection(c.cold_preset === "recommended" ? "recommended" : null);
syncColdCachePolicy();
@@ -170,6 +172,10 @@ function applyCacheMemoryPreset(kind) {
setCacheMemoryPresetSelection(kind);
setSettingsDirty("cache-form"); updateCacheMemorySizing();
}
function syncCacheVia() {
const enabled = $("cache-via-enabled")?.checked;
if ($("cache-via-value")) $("cache-via-value").disabled = !enabled;
}
function applyRecommendedColdCachePreset() {
const transient = Number($("cache-transient-memory").value);
const queue = Number.isFinite(transient) && transient > 0 ? Math.max(0.0625, Math.floor((transient * .5) / 8) * 8 || Math.min(transient, 8)) : 32;
@@ -279,7 +285,7 @@ bindSettingsForm("response-form", next => {
next.compression = { min_bytes: Number($("compress-min").value) }; for (const name of ["gzip", "br", "zstd", "deflate"]) next.compression[name] = $(`compress-${name}`).checked;
}, "response", next => ({ server_header: next.server_header, diagnostic_headers: next.diagnostic_headers, compression: next.compression }));
bindSettingsForm("cache-form", next => {
next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked };
next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked, via_header: $("cache-via-enabled").checked ? $("cache-via-value").value.trim() : null };
}, "cache");
bindSettingsForm("transform-form", next => {
next.transform = {
@@ -384,6 +390,7 @@ $("server-mode").addEventListener("change", syncServerMode);
$("log-file-enabled").addEventListener("change", syncLoggingFileMode);
$("log-file-per-vhost").addEventListener("change", syncLoggingFileMode);
for (const id of ["cache-cold-enabled", "cache-cold-on-idle", "cache-cold-on-age"]) $(id).addEventListener("change", syncColdCachePolicy);
$("cache-via-enabled").addEventListener("change", syncCacheVia);
document.querySelectorAll("[data-cache-memory-preset]").forEach(button => button.addEventListener("click", () => applyCacheMemoryPreset(button.dataset.cacheMemoryPreset)));
document.querySelectorAll("[data-cache-cold-preset]").forEach(button => button.addEventListener("click", applyRecommendedColdCachePreset));
document.querySelectorAll("[data-transform-preset]").forEach(button => button.addEventListener("click", () => applyTransformPreset(button.dataset.transformPreset)));
+61 -6
View File
@@ -1749,9 +1749,12 @@
data-settings-tab="pages" id="settings-tab-pages" role="tab" tabindex="-1"
type="button"></button><button aria-controls="settings-page-logging" aria-selected="false"
data-i18n="logging" data-settings-tab="logging" id="settings-tab-logging" role="tab"
tabindex="-1" type="button"></button><button aria-controls="settings-page-prometheus"
aria-selected="false" data-i18n="prometheusTitle" data-settings-tab="prometheus"
id="settings-tab-prometheus" role="tab" tabindex="-1" type="button"></button><button
tabindex="-1" type="button"></button><button aria-controls="settings-page-geoip"
aria-selected="false" data-i18n="geoipUpdatesTitle" data-settings-tab="geoip"
id="settings-tab-geoip" role="tab" tabindex="-1" type="button"></button><button
aria-controls="settings-page-prometheus" aria-selected="false" data-i18n="prometheusTitle"
data-settings-tab="prometheus" id="settings-tab-prometheus" role="tab" tabindex="-1"
type="button"></button><button
aria-controls="settings-page-administration" aria-selected="false"
data-i18n="administration" data-settings-tab="administration"
id="settings-tab-administration" role="tab" tabindex="-1" type="button"></button><button
@@ -1915,7 +1918,7 @@
<p class="fine" data-i18n="lowPortsHelp">Privileged ports may need CAP_NET_BIND_SERVICE.
Do not run the whole application as root.</p>
<p class="pending-note" hidden="" id="listener-pending"></p>
<p class="fine" id="listener-active"></p>
<div class="listener-active-summary" id="listener-active" role="status"></div>
<div class="form-actions"><button class="primary" data-i18n="saveListeners"
type="submit">Save listeners</button></div>
</form>
@@ -2264,6 +2267,15 @@
type="checkbox" /><span>Cache-Status (RFC 9211)</span></label><label
class="check"><input id="cache-resident-age-header"
type="checkbox" /><span>X-Cache-Resident-Age</span></label></div>
<div class="cache-via-setting">
<label class="check"><input id="cache-via-enabled" type="checkbox" /><span
data-i18n="cacheViaEnable">Add Via response header</span></label>
<label><span data-i18n="cacheViaValue">Via header value</span><input
id="cache-via-value" maxlength="256" placeholder="RustProxy"
required="" type="text" /></label>
</div>
<p class="fine" data-i18n="cacheViaHelp">Defaults to Via: RustProxy. Changing this
response-only header does not purge cached objects.</p>
<p class="fine" data-i18n="cacheAgeHelp">Age is sent on cache hits unless an
explicit response-header rule removes or replaces it. Such Age rules are
wire-only and do not change cache freshness.</p>
@@ -2548,6 +2560,49 @@
</form>
</div>
</div>
<div aria-labelledby="settings-tab-geoip" class="settings-page"
data-settings-page="geoip" hidden="" id="settings-page-geoip" role="tabpanel">
<form class="card edit-form" id="geoip-form">
<div class="card-heading">
<h2 data-i18n="geoipUpdatesTitle"></h2><span class="pill">AUTO</span>
</div>
<p class="fine" data-i18n="geoipUpdatesHelp"></p>
<label class="check"><input id="geoip-enabled" type="checkbox" /><span
data-i18n="geoipUpdatesEnable"></span></label>
<div class="field-grid">
<label><span data-i18n="geoipFormat"></span><select id="geoip-format">
<option data-i18n="geoipFormatCsvPair" value="csv_pair"></option>
<option data-i18n="geoipFormatMmdb" value="mmdb"></option>
</select></label>
<label><span data-i18n="geoipMaxFileMiB"></span><input id="geoip-max-mib" min="1"
max="256" required="" type="number" /></label>
<label><span data-i18n="geoipIntervalHours"></span><input id="geoip-interval-hours"
min="0.0166667" max="744" step="any" required="" type="number" /></label>
</div>
<div class="field-grid" id="geoip-csv-fields">
<label><span data-i18n="geoipIpv4Url"></span><input id="geoip-ipv4-url"
maxlength="2048" required="" type="url" /></label>
<label><span data-i18n="geoipIpv4ChecksumUrl"></span><input id="geoip-ipv4-checksum-url"
maxlength="2048" type="url" /></label>
<label><span data-i18n="geoipIpv6Url"></span><input id="geoip-ipv6-url"
maxlength="2048" required="" type="url" /></label>
<label><span data-i18n="geoipIpv6ChecksumUrl"></span><input id="geoip-ipv6-checksum-url"
maxlength="2048" type="url" /></label>
</div>
<div class="field-grid" id="geoip-mmdb-fields" hidden="">
<label><span data-i18n="geoipMmdbUrl"></span><input id="geoip-mmdb-url"
maxlength="2048" type="url" /></label>
<label><span data-i18n="geoipMmdbChecksumUrl"></span><input id="geoip-mmdb-checksum-url"
maxlength="2048" type="url" /></label>
</div>
<p class="fine" data-i18n="geoipSourceHelp"></p>
<p class="fine" id="geoip-status" role="status"></p>
<div class="form-actions"><button class="primary" data-i18n="apply"
type="submit"></button><button class="outline" data-i18n="geoipUpdateNow"
id="geoip-update-now" type="button"></button></div>
<p class="fine" data-i18n="geoipUpdateNowHelp"></p>
</form>
</div>
<div aria-labelledby="settings-tab-prometheus" class="settings-page"
data-settings-page="prometheus" hidden="" id="settings-page-prometheus" role="tabpanel">
<form class="card edit-form" id="prometheus-form">
@@ -2823,7 +2878,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.27.3</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>0.28.0</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
@@ -3567,7 +3622,7 @@ max_execution_time = 30</code></pre>
<article class="card about-card">
<div class="about-product">
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
<div><span class="small-label">RustProxy</span><strong>0.27.3</strong></div>
<div><span class="small-label">RustProxy</span><strong>0.28.0</strong></div>
</div>
<dl class="about-meta">
<div>
+1 -1
View File
@@ -5,7 +5,7 @@
const themes = new Set(["dark", "light"]);
try { const cached = localStorage.getItem("rp-theme"); document.documentElement.dataset.theme = themes.has(cached) ? cached : "dark"; } catch { document.documentElement.dataset.theme = "dark"; }
const panelPaths = new Set(["/overview", "/vhosts", "/docker-vhosts", "/redirects", "/404-hosts", "/streams", "/acl-profiles", "/configuration", "/tls", "/traffic", "/cache", "/transform", "/logging", "/logs", "/settings"]);
const settingsTabs = new Set(["appearance", "listeners", "connections", "docker-agents", "response", "cache", "transform", "headers", "forwarding", "pages", "logging", "prometheus", "administration", "tls-policy", "manual-config", "security"]);
const settingsTabs = new Set(["appearance", "listeners", "connections", "docker-agents", "response", "cache", "transform", "headers", "forwarding", "pages", "logging", "geoip", "prometheus", "administration", "tls-policy", "manual-config", "security"]);
let packs = new Map(), fallback = null, language = null, setupRequired = false, ready = false;
try { language = localStorage.getItem("rp-language"); } catch { }
const t = key => packs.get(language)?.translations?.[key] ?? packs.get(fallback)?.translations?.[key] ?? key;
+63
View File
@@ -142,6 +142,29 @@ curl --fail --show-error --request POST \
finally { certificateApiLoading = false; }
}
const defaults = { enabled: false, listen: "127.0.0.1:9191", path: "/metrics", allowed_cidrs: ["127.0.0.0/8", "::1/128"], bearer_token_env: null, allow_trusted_network_without_token: false, include_system: true, include_vhosts: true, include_traffic: true, include_backends: true, include_docker_vhosts: true, include_cache_domains: true, include_streams: true, include_logging: true };
const geoipDefaults = {
enabled: true, format: "csv_pair",
ipv4_url: "https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv",
ipv6_url: "https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv6.csv",
mmdb_url: "https://github.com/sapics/ip-location-db/releases/download/latest/user-country.mmdb", ipv4_checksum_url: null, ipv6_checksum_url: null, mmdb_checksum_url: null,
max_bytes: 268435456, interval_seconds: 86400
};
const legacyGeoipRelease = "https://github.com/sapics/ip-location-db/releases/download/latest/";
const legacyGeoipChecksum = "https://github.com/sapics/ip-location-db/releases/download/checksum/";
function normalizedGeoip(raw = {}) {
const geoip = { ...geoipDefaults, ...raw };
if (!raw.ipv4_url && raw.release_url && raw.release_url !== legacyGeoipRelease) geoip.ipv4_url = `${raw.release_url.replace(/\/?$/, "/")}user-country-ipv4.csv`;
if (!raw.ipv6_url && raw.release_url && raw.release_url !== legacyGeoipRelease) geoip.ipv6_url = `${raw.release_url.replace(/\/?$/, "/")}user-country-ipv6.csv`;
if (!raw.ipv4_checksum_url && raw.checksum_url && raw.checksum_url !== legacyGeoipChecksum) geoip.ipv4_checksum_url = `${raw.checksum_url.replace(/\/?$/, "/")}user-country-ipv4.csv.sha256`;
if (!raw.ipv6_checksum_url && raw.checksum_url && raw.checksum_url !== legacyGeoipChecksum) geoip.ipv6_checksum_url = `${raw.checksum_url.replace(/\/?$/, "/")}user-country-ipv6.csv.sha256`;
return geoip;
}
function syncGeoipFormat() {
const mmdb = $("geoip-format").value === "mmdb";
$("geoip-csv-fields").hidden = mmdb; $("geoip-mmdb-fields").hidden = !mmdb;
$("geoip-ipv4-url").required = !mmdb; $("geoip-ipv6-url").required = !mmdb;
$("geoip-mmdb-url").required = mmdb && $("geoip-enabled").checked;
}
function example() {
const addr = $("prom-listen").value.trim(), path = $("prom-path").value.trim(), env = $("prom-token-env").value.trim();
$("prom-scrape-example").textContent = `scrape_configs:\n - job_name: rustproxy\n metrics_path: ${JSON.stringify(path)}\n static_configs:\n - targets: [${JSON.stringify(addr)}]${env ? "\n authorization:\n credentials_file: /etc/prometheus/rustproxy.token" : ""}`;
@@ -156,6 +179,22 @@ curl --fail --show-error --request POST \
const status = latestStatus?.prometheus;
$("prom-status").textContent = status?.restart_required ? t("restartPending") : status?.active?.enabled ? `${t("prometheusActive")}: ${status.active.listen}${p.path}` : t("prometheusDisabled"); example();
const geoip = normalizedGeoip(envelope.config.geoip_updates || {});
if (!settingsDirty.has("geoip-form")) {
$("geoip-enabled").checked = geoip.enabled !== false; $("geoip-format").value = geoip.format || "csv_pair";
$("geoip-ipv4-url").value = geoip.ipv4_url || ""; $("geoip-ipv6-url").value = geoip.ipv6_url || "";
$("geoip-mmdb-url").value = geoip.mmdb_url || "";
$("geoip-ipv4-checksum-url").value = geoip.ipv4_checksum_url || ""; $("geoip-ipv6-checksum-url").value = geoip.ipv6_checksum_url || "";
$("geoip-mmdb-checksum-url").value = geoip.mmdb_checksum_url || "";
$("geoip-max-mib").value = Number(geoip.max_bytes || geoipDefaults.max_bytes) / 1048576;
$("geoip-interval-hours").value = Number(geoip.interval_seconds || geoipDefaults.interval_seconds) / 3600;
syncGeoipFormat();
}
const geoStatus = latestStatus?.geoip;
$("geoip-status").textContent = geoStatus?.loaded
? geoStatus.format === "mmdb" ? `${t("geoipStatusLoaded")}: ${geoStatus.source}` : `${t("geoipStatusLoaded")}: IPv4 ${fmt(geoStatus.ipv4_ranges)} · IPv6 ${fmt(geoStatus.ipv6_ranges)}`
: geoip.enabled ? t("geoipStatusMissing") : t("geoipStatusDisabled");
const admin = envelope.config.administration || {}, adminStatus = latestStatus?.administration;
if (!settingsDirty.has("administration-form")) {
$("admin-listen").value = adminStatus?.locked_listen || admin.listen || adminStatus?.desired?.listen || latestStatus?.admin_listen || "127.0.0.1:9090";
@@ -186,6 +225,30 @@ curl --fail --show-error --request POST \
$("tls-policy-available").textContent = available.length ? `${t("tlsAvailableCiphers")}: ${available.join(", ")}` : "";
renderCertificateApi(); loadCertificateApi();
}
$("geoip-format").addEventListener("change", syncGeoipFormat); $("geoip-enabled").addEventListener("change", syncGeoipFormat);
bindSettingsForm("geoip-form", next => {
next.geoip_updates = {
enabled: $("geoip-enabled").checked, format: $("geoip-format").value,
ipv4_url: $("geoip-ipv4-url").value.trim(), ipv6_url: $("geoip-ipv6-url").value.trim(), mmdb_url: $("geoip-mmdb-url").value.trim(),
ipv4_checksum_url: $("geoip-ipv4-checksum-url").value.trim() || null,
ipv6_checksum_url: $("geoip-ipv6-checksum-url").value.trim() || null,
mmdb_checksum_url: $("geoip-mmdb-checksum-url").value.trim() || null,
max_bytes: Math.round(Number($("geoip-max-mib").value) * 1048576),
interval_seconds: Math.round(Number($("geoip-interval-hours").value) * 3600)
};
}, "geoip", next => next.geoip_updates);
$("geoip-update-now").addEventListener("click", async () => {
if (settingsDirty.has("geoip-form")) { toast(t("geoipUpdateSaveFirst"), true); return; }
const button = $("geoip-update-now"); button.disabled = true;
$("geoip-status").textContent = t("geoipUpdateWorking");
try {
const result = await api("/api/geoip/update", { method: "POST" });
if (result.database) latestStatus = { ...(latestStatus || {}), geoip: result.database };
render();
toast(t(result.outcome === "updated" ? "geoipUpdateUpdated" : result.outcome === "busy" ? "geoipUpdateBusy" : "geoipUpdateCurrent"));
} catch (error) { toast(error.message, true); render(); }
finally { button.disabled = false; }
});
bindSettingsForm("prometheus-form", next => {
next.prometheus = {
enabled: $("prom-enabled").checked, listen: $("prom-listen").value.trim(), path: $("prom-path").value.trim(), allowed_cidrs: $("prom-cidrs").value.split(/[\n,]/).map(v => v.trim()).filter(Boolean), bearer_token_env: $("prom-token-env").value.trim() || null, allow_trusted_network_without_token: $("prom-trusted-no-token").checked,
+69 -3
View File
@@ -247,13 +247,13 @@ h3 {
.shell {
display: grid;
grid-template-columns: 218px minmax(0, 1fr);
min-height: 100vh
min-height: calc(100vh / var(--ui-scale, 1))
}
.sidebar {
position: sticky;
top: 0;
height: 100vh;
height: calc(100vh / var(--ui-scale, 1));
background: var(--sidebar-bg);
border-right: 1px solid var(--line);
padding: 34px 22px;
@@ -9646,4 +9646,70 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
.acl-policy-step-head>button {
width: 100%
}
}
}
.listener-active-summary {
margin-top: 10px;
padding: 10px 12px;
border: 1px solid var(--line);
background: var(--panel2);
display: grid;
gap: 8px
}
.listener-active-title {
font-size: 10px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .45px
}
.listener-active-items {
display: flex;
flex-wrap: wrap;
gap: 8px
}
.listener-active-item {
display: inline-flex;
align-items: center;
gap: 7px;
min-width: 0;
padding: 7px 9px;
border: 1px solid var(--line);
background: var(--panel)
}
.listener-active-protocol {
font: 600 10px var(--mono);
color: var(--accent)
}
.listener-active-address {
overflow-wrap: anywhere
}
.listener-active-feature {
padding: 2px 5px;
border: 1px solid var(--line);
font: 600 9px var(--mono);
color: var(--muted)
}
.cache-via-setting {
display: grid;
grid-template-columns: minmax(180px, .8fr) minmax(220px, 1.2fr);
gap: 8px 14px;
align-items: end;
margin-top: 10px
}
.location-header-editor .vh-header-rule-head > button {
margin-left: auto;
flex: 0 0 auto
}
@media(max-width:650px) {
.cache-via-setting {
grid-template-columns: 1fr
}
}
+1 -1
View File
@@ -97,7 +97,7 @@
target.value = JSON.stringify(result, null, 2); target.setCustomValidity(""); onChange();
};
for (const direction of ["request", "response"]) {
const block = el("section", undefined, "vh-wide location-header-direction"); block.dataset.headerDirection = direction;
const block = el("section", undefined, "vh-wide location-header-direction location-header-editor"); block.dataset.headerDirection = direction;
const heading = el("div", undefined, "location-header-direction-head");
heading.append(text("h3", direction === "response" ? "locationResponseHeaders" : "locationProxyHeaders"), text("p", "headerRuleHelp", "fine"));
block.append(heading);