v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,370 @@
|
||||
fn status_payload(state: &App) -> Value {
|
||||
let snapshot = state.config.load_full();
|
||||
let listeners = json!({
|
||||
"default_http":state.startup_listeners.0.to_string(),
|
||||
"default_https":state.startup_listeners.1.map(|v| v.to_string()),
|
||||
"locked_http":state.startup_locks.http.map(|v| v.to_string()),
|
||||
"locked_https":state.startup_locks.https.map(|v| v.to_string()),
|
||||
"active_endpoints":state.active_listeners,
|
||||
"desired_endpoints":snapshot.listeners,
|
||||
"named_endpoints":snapshot.envelope.config.listeners.endpoints,
|
||||
"restart_required":snapshot.listeners != state.active_listeners
|
||||
});
|
||||
let mut features = serde_json::Map::new();
|
||||
for name in [
|
||||
"http1",
|
||||
"streaming",
|
||||
"hot_reload",
|
||||
"sqlite",
|
||||
"admin_accounts",
|
||||
"acl",
|
||||
"headers",
|
||||
"live_sse",
|
||||
"http2",
|
||||
"tls",
|
||||
"websocket",
|
||||
"dns_backends",
|
||||
"https_backends",
|
||||
"cache",
|
||||
"compression",
|
||||
"system_diagnostics",
|
||||
"language_packs",
|
||||
"vhosts",
|
||||
"redirect_hosts",
|
||||
"not_found_hosts",
|
||||
"ip_bindings",
|
||||
"backend_pools",
|
||||
"health_checks",
|
||||
"sticky_sessions",
|
||||
"security_headers",
|
||||
"http3",
|
||||
"tinyauth",
|
||||
"trusted_forwarding",
|
||||
"named_listeners",
|
||||
"stream_proxy",
|
||||
"certificate_library",
|
||||
"vhost_server_header",
|
||||
"diagnostic_header_controls",
|
||||
"cache_dashboard",
|
||||
"transform_dashboard",
|
||||
"cache_reasons",
|
||||
"acme_http01",
|
||||
"acme_cloudflare",
|
||||
"acme_ovh",
|
||||
"prometheus_exporter",
|
||||
"vhosts_only",
|
||||
"acme_rfc2136",
|
||||
"certificate_upload",
|
||||
"vhost_cache",
|
||||
"selective_purge",
|
||||
"custom_pages",
|
||||
"rate_limit",
|
||||
"country_access",
|
||||
"cloudflare_client_ip",
|
||||
"server_timing",
|
||||
"static_cookie_cache",
|
||||
"per_vhost_http3",
|
||||
"structured_log_sinks",
|
||||
"admin_https",
|
||||
"tls_policy",
|
||||
"cache_resident_age",
|
||||
] {
|
||||
features.insert(name.into(), Value::Bool(true));
|
||||
}
|
||||
features.insert(
|
||||
"tls_enabled".into(),
|
||||
Value::Bool(state.active_listeners.iter().any(|l| l.tls)),
|
||||
);
|
||||
features.insert("docker".into(), Value::Bool(true));
|
||||
let mut payload = json!({"version":env!("CARGO_PKG_VERSION"), "project":"RustProxy", "mode":"standalone",
|
||||
"auth":"argon2id_cookie_session", "uptime_seconds":state.started.elapsed().as_secs(),
|
||||
"revision":snapshot.envelope.revision, "proxy_listen":state.policy.proxy_addr.to_string(),
|
||||
"admin_listen":state.policy.admin_addr.to_string(), "tls_listen":state.policy.tls_addr.map(|v| v.to_string()),
|
||||
"certificate_bindings":snapshot.envelope.config.tls.certificates.len(),
|
||||
"vhosts":snapshot.envelope.config.vhosts.len(),
|
||||
"redirect_hosts":snapshot.envelope.config.redirects.len(),
|
||||
"not_found_hosts":snapshot.envelope.config.not_found_hosts.len()});
|
||||
payload["listeners"] = listeners;
|
||||
let stream_metrics = state
|
||||
.active_streams
|
||||
.iter()
|
||||
.filter_map(|stream| {
|
||||
state
|
||||
.stream_metrics
|
||||
.get(&stream.id)
|
||||
.map(|metrics| (stream.id.clone(), metrics.view()))
|
||||
})
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
payload["streams"] = json!({
|
||||
"active": state.active_streams,
|
||||
"desired": snapshot.envelope.config.streams,
|
||||
"restart_required": snapshot.envelope.config.streams != state.active_streams,
|
||||
"metrics": stream_metrics
|
||||
});
|
||||
payload["prometheus"] = json!({"active": state.active_prometheus,
|
||||
"desired": snapshot.envelope.config.prometheus,
|
||||
"restart_required": snapshot.envelope.config.prometheus.restart_differs(&state.active_prometheus)});
|
||||
let mut desired_admin = snapshot.envelope.config.administration.clone();
|
||||
desired_admin.listen = Some(desired_admin.resolved_listen(state.policy.admin_addr));
|
||||
if let Some(id) = desired_admin.certificate_id.as_deref() {
|
||||
if let Some(certificate) = snapshot
|
||||
.envelope
|
||||
.config
|
||||
.tls
|
||||
.certificates
|
||||
.iter()
|
||||
.find(|certificate| certificate.id == id)
|
||||
{
|
||||
desired_admin
|
||||
.allowed_hosts
|
||||
.extend(certificate.hosts.clone());
|
||||
desired_admin.allowed_hosts.sort();
|
||||
desired_admin.allowed_hosts.dedup();
|
||||
}
|
||||
}
|
||||
let desired_admin_certificate_files = desired_admin
|
||||
.certificate_id
|
||||
.as_deref()
|
||||
.and_then(|id| {
|
||||
snapshot
|
||||
.envelope
|
||||
.config
|
||||
.tls
|
||||
.certificates
|
||||
.iter()
|
||||
.find(|certificate| certificate.id == id)
|
||||
})
|
||||
.map(|certificate| (certificate.cert_file.clone(), certificate.key_file.clone()));
|
||||
let administration_restart_required = desired_admin
|
||||
.restart_differs(&state.active_administration)
|
||||
|| (state.active_administration.tls
|
||||
&& snapshot.envelope.config.tls.policy != state.active_administration_tls_policy)
|
||||
|| (state.active_administration.tls
|
||||
&& desired_admin_certificate_files != state.active_administration_certificate_files);
|
||||
payload["administration"] = json!({
|
||||
"active": state.active_administration,
|
||||
"desired": desired_admin,
|
||||
"restart_required": administration_restart_required,
|
||||
"listen_locked": state.startup_locks.administration.is_some(),
|
||||
"locked_listen": state.startup_locks.administration.map(|value| value.to_string())
|
||||
});
|
||||
payload["features"] = Value::Object(features);
|
||||
let cache_view = state.cache.view();
|
||||
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());
|
||||
let system = state.diagnostics.view();
|
||||
let cache_ram_bytes = (cache_view.hot_allocated_bytes as u64)
|
||||
.saturating_add(cache_view.transient_bytes);
|
||||
let non_cache_rss_bytes = system
|
||||
.process_rss_bytes
|
||||
.map(|rss| rss.saturating_sub(cache_ram_bytes));
|
||||
payload["memory_accounting"] = json!({
|
||||
"process_rss_bytes": system.process_rss_bytes,
|
||||
"cache_hot_bytes": cache_view.hot_allocated_bytes,
|
||||
"cache_hot_limit_bytes": cache_view.hot_limit_bytes,
|
||||
"cache_transient_bytes": cache_view.transient_bytes,
|
||||
"cache_transient_limit_bytes": cache_view.transient_limit_bytes,
|
||||
"cache_total_bytes": cache_ram_bytes,
|
||||
"transform_reserved_bytes": transform_resources.memory_reserved_bytes,
|
||||
"transform_budget_bytes": transform_resources.memory_budget_bytes,
|
||||
"transform_inflight": transform_resources.inflight,
|
||||
"transform_max_inflight": transform_resources.max_inflight,
|
||||
"non_cache_rss_bytes": non_cache_rss_bytes,
|
||||
"non_cache_rss_includes_transform": true,
|
||||
"method": "rss_minus_accounted_cache; transform reservation is an overlapping estimate"
|
||||
});
|
||||
payload["runtime"] = json!({
|
||||
"platform": system.platform,
|
||||
"statistics_source": system.source,
|
||||
"data_directory": state.policy.data_dir.as_ref().map(|path| path.display().to_string()),
|
||||
"database_path": state.policy.data_dir.as_ref().map(|path| path.join("config.sqlite3").display().to_string())
|
||||
});
|
||||
payload["system"] = json!(system);
|
||||
payload["backend_pools"] = json!(snapshot.pools.iter().map(|p| p.view()).collect::<Vec<_>>());
|
||||
payload["vhost_metrics"] = json!(snapshot
|
||||
.vhost_metrics
|
||||
.iter()
|
||||
.map(|(id, metrics)| (id.clone(), metrics.view()))
|
||||
.collect::<std::collections::BTreeMap<_, _>>());
|
||||
let mut backend_pool_domains = std::collections::BTreeMap::new();
|
||||
for agent in &snapshot.envelope.config.docker_agents.agents {
|
||||
for assignment in agent
|
||||
.assignments
|
||||
.iter()
|
||||
.filter(|assignment| assignment.enabled)
|
||||
{
|
||||
if let Ok(hosts) = crate::docker::assignment_hosts(assignment, &agent.base_domain) {
|
||||
if let Some(host) = hosts.first() {
|
||||
backend_pool_domains.insert(
|
||||
crate::docker::generated_id(&agent.id, &assignment.service_key),
|
||||
host.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
payload["backend_pool_domains"] = json!(backend_pool_domains);
|
||||
payload["backends"] = json!(snapshot
|
||||
.envelope
|
||||
.config
|
||||
.vhosts
|
||||
.iter()
|
||||
.map(|h| h.backends.len())
|
||||
.sum::<usize>());
|
||||
payload["routes"] = json!(snapshot
|
||||
.envelope
|
||||
.config
|
||||
.vhosts
|
||||
.iter()
|
||||
.map(|h| h.routes.len())
|
||||
.sum::<usize>());
|
||||
payload["enabled_routes"] = json!(snapshot
|
||||
.envelope
|
||||
.config
|
||||
.vhosts
|
||||
.iter()
|
||||
.filter(|h| h.enabled)
|
||||
.map(|h| h.routes.iter().filter(|r| r.enabled).count())
|
||||
.sum::<usize>());
|
||||
payload["metrics"] = json!(state.metrics.view());
|
||||
payload["transform_metrics"] = json!(state.transform_metrics.total());
|
||||
payload["logs_dropped"] = json!(state.logs.dropped.load(Ordering::Relaxed));
|
||||
payload["software_update"] = json!(state.software_update.snapshot());
|
||||
payload["logging"] = json!({
|
||||
"settings": &snapshot.envelope.config.logging,
|
||||
"sinks": state.logs.stats(),
|
||||
"error_file": crate::logging::error_log_stats(),
|
||||
});
|
||||
payload["tls"] = json!({
|
||||
"policy": &snapshot.envelope.config.tls.policy,
|
||||
"cipher_suites": crate::tls::cipher_suite_names(),
|
||||
"effective_cipher_suites": snapshot.envelope.config.tls.policy.effective_cipher_suites()
|
||||
});
|
||||
payload
|
||||
}
|
||||
async fn status(State(state): State<Arc<App>>) -> Json<Value> {
|
||||
Json(status_payload(&state))
|
||||
}
|
||||
async fn backend_status(State(state): State<Arc<App>>) -> Json<Value> {
|
||||
let snapshot = state.config.load_full();
|
||||
Json(
|
||||
json!({"pools":snapshot.pools.iter().map(|p| p.view()).collect::<Vec<_>>(),
|
||||
"active_frontends":state.active_listeners, "desired_frontends":snapshot.listeners}),
|
||||
)
|
||||
}
|
||||
fn docker_runtime_views(state: &Arc<App>) -> (Vec<Value>, Vec<Value>) {
|
||||
let snapshot = state.config.load_full();
|
||||
let mut routes = Vec::new();
|
||||
let mut ids = std::collections::HashSet::new();
|
||||
for agent in &snapshot.envelope.config.docker_agents.agents {
|
||||
for assignment in &agent.assignments {
|
||||
let pool_id = crate::docker::generated_id(&agent.id, &assignment.service_key);
|
||||
ids.insert(pool_id.clone());
|
||||
routes.push(json!({"agent_id": &agent.id, "service_key": &assignment.service_key, "pool_id": pool_id}));
|
||||
}
|
||||
}
|
||||
let pools = snapshot
|
||||
.pools
|
||||
.iter()
|
||||
.filter(|pool| ids.contains(&pool.id))
|
||||
.map(|pool| pool.view())
|
||||
.collect();
|
||||
(pools, routes)
|
||||
}
|
||||
async fn docker_status(State(state): State<Arc<App>>) -> Json<Value> {
|
||||
let (pools, pool_routes) = docker_runtime_views(&state);
|
||||
Json(
|
||||
json!({"observations": state.docker.view().await, "pools": pools, "pool_routes": pool_routes}),
|
||||
)
|
||||
}
|
||||
async fn docker_refresh(State(state): State<Arc<App>>) -> Result<Json<Value>, ApiError> {
|
||||
let observations = state.refresh_docker().await?;
|
||||
let (pools, pool_routes) = docker_runtime_views(&state);
|
||||
Ok(Json(
|
||||
json!({"observations": observations, "pools": pools, "pool_routes": pool_routes}),
|
||||
))
|
||||
}
|
||||
async fn certificate_inventory(State(state): State<Arc<App>>) -> Json<Value> {
|
||||
let snapshot = state.config.load_full();
|
||||
let config = &snapshot.envelope.config;
|
||||
let configured_certificates = config.tls.certificates.clone();
|
||||
let certificate_directory = state.policy.cert_dir.clone();
|
||||
// Inspect only public certificate files on a blocking worker; never read keys.
|
||||
// Live PEM coverage wins so in-place renewals are reflected immediately; stored
|
||||
// metadata is only a fallback when the public certificate file cannot be read.
|
||||
let inspected_metadata = tokio::task::spawn_blocking(move || {
|
||||
configured_certificates
|
||||
.into_iter()
|
||||
.filter_map(|cert| {
|
||||
certificate_directory.as_deref().and_then(|directory| {
|
||||
crate::tls::certificate_metadata(directory, &cert.cert_file)
|
||||
.ok()
|
||||
.map(|metadata| (cert.id, metadata))
|
||||
})
|
||||
})
|
||||
.collect::<std::collections::HashMap<_, _>>()
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let renewals = state.acme.renewal_view();
|
||||
let entries: Vec<_> = config.tls.certificates.iter().map(|cert| {
|
||||
let assigned: Vec<_> = config.vhosts.iter().filter(|v|v.certificate_id.as_deref()==Some(cert.id.as_str()))
|
||||
.map(|v|json!({"id":v.id,"hosts":v.frontend.hosts})).collect();
|
||||
let metadata = inspected_metadata.get(&cert.id);
|
||||
let covered_hosts = metadata.map(|value| value.covered_hosts.clone()).unwrap_or_else(|| cert.covered_hosts.clone());
|
||||
let renewal = renewals.get(&cert.id);
|
||||
json!({"id":cert.id,"hosts":cert.hosts,"covered_hosts":covered_hosts,"cert_file":cert.cert_file,"key_file":cert.key_file,
|
||||
"issuer":cert.issuer,"certificate_issuer":metadata.and_then(|value| value.issuer.clone()),
|
||||
"not_before":metadata.and_then(|value| value.not_before.clone()),"not_after":metadata.and_then(|value| value.not_after.clone()),
|
||||
"not_before_unix":metadata.and_then(|value| value.not_before_unix),"not_after_unix":metadata.and_then(|value| value.not_after_unix),
|
||||
"days_remaining":metadata.and_then(|value| value.days_remaining),"expiry_status":metadata.map(|value| value.expiry_status).unwrap_or("unknown"),
|
||||
"renewal":renewal,"renewal_status":renewal.map(|value| value.status.as_str()),
|
||||
"last_renewal_attempt":renewal.map(|value| value.last_checked).filter(|value| *value > 0),
|
||||
"last_renewal_error":renewal.filter(|value| value.status == "failed").map(|value| value.message.as_str()),
|
||||
"default":config.tls.default_certificate.as_deref()==Some(cert.id.as_str()),"assigned_vhosts":assigned})
|
||||
}).collect();
|
||||
Json(
|
||||
json!({"revision":snapshot.envelope.revision,"certificates":entries,"acme":true, "providers":["http01", "cloudflare", "ovh", "rfc2136"]}),
|
||||
)
|
||||
}
|
||||
async fn configuration(State(state): State<Arc<App>>) -> Json<crate::config::Envelope> {
|
||||
Json(state.config.load().envelope.clone())
|
||||
}
|
||||
async fn update(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
Json(input): Json<Update>,
|
||||
) -> Result<Json<crate::config::Envelope>, ApiError> {
|
||||
state.update_as(input, session.username).await.map(Json)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct SectionUpdate {
|
||||
expected_revision: u64,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ResponseSection {
|
||||
server_header: crate::settings::ServerHeader,
|
||||
diagnostic_headers: crate::settings::DiagnosticHeaders,
|
||||
compression: crate::compression::Settings,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LoggingSection {
|
||||
logging: crate::logging::Settings,
|
||||
error_log: crate::logging::ErrorLogSettings,
|
||||
}
|
||||
|
||||
fn invalid_section(section: &str, error: serde_json::Error) -> ApiError {
|
||||
ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("invalid {section} configuration: {error}"),
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user