v0.28.0
This commit is contained in:
1 parent
c24bc4279b
commit
3b3f0db377
84 files changed
+2686
-392
No files matched your search
@@ -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"),
|
||||
|
||||
@@ -70,6 +70,10 @@ async fn update_section(
|
||||
config.prometheus =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
}
|
||||
"geoip" => {
|
||||
config.geoip_updates =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
}
|
||||
"administration" => {
|
||||
config.administration =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, 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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user