1use std::sync::atomic::Ordering;
9use std::sync::{OnceLock, RwLock};
10
11use crate::cache::CacheLayer;
12use crate::core::json::escape as json_escape;
13
14fn quoted(s: &str) -> String {
15 format!("\"{}\"", json_escape(s))
16}
17
18pub fn config_json() -> String {
21 let mut pairs: Vec<(String, String)> = std::env::vars().filter(|(k, _)| k.starts_with("RWS_CONFIG_")).collect();
22 pairs.sort();
23 let members: Vec<String> =
24 pairs.iter().map(|(k, v)| format!("{}:{}", quoted(k), quoted(&crate::entry_point::shown(k, v)))).collect();
25 format!("{{{}}}", members.join(","))
26}
27
28pub fn build_features_json() -> String {
30 macro_rules! features {
31 ($($name:literal),*) => { vec![$( ($name, cfg!(feature = $name)) ),*] };
32 }
33 let all: Vec<(&str, bool)> = features!(
34 "http1", "http2", "http3", "acme", "auth", "serde", "macros", "sso", "sso-saml", "sso-server", "gateway-api",
35 "tower", "openapi", "mimalloc", "brotli", "zstd", "wasm-plugins"
36 );
37 let members: Vec<String> = all.iter().map(|(name, on)| format!("\"{name}\":{on}")).collect();
38 format!("{{{}}}", members.join(","))
39}
40
41pub fn metrics_json() -> String {
43 use crate::metrics;
44 format!(
45 r#"{{"requests_total":{},"errors_total":{},"active_connections":{},"thread_pool_queued":{},"ready":{}}}"#,
46 metrics::REQUESTS_TOTAL.load(Ordering::Relaxed),
47 metrics::ERRORS_TOTAL.load(Ordering::Relaxed),
48 metrics::ACTIVE_CONNECTIONS.load(Ordering::Relaxed),
49 metrics::THREAD_POOL_QUEUED.load(Ordering::Relaxed),
50 metrics::SERVER_READY.load(Ordering::Relaxed),
51 )
52}
53
54pub fn rate_limit_json() -> String {
55 let cfg = crate::config_reload::current();
56 format!(
57 r#"{{"configured":{},"max_requests":{},"window_secs":{}}}"#,
58 crate::rate_limit::is_configured(),
59 cfg.rate_limit_max_requests,
60 cfg.rate_limit_window_secs
61 )
62}
63
64pub fn check_rate_limit_json(ip: &str) -> String {
66 let limiter = crate::rate_limit::global();
67 let remaining = limiter.remaining(ip);
68 let allowed = limiter.check(ip);
69 format!(r#"{{"ip":{},"remaining":{remaining},"allowed":{allowed}}}"#, quoted(ip))
70}
71
72pub fn cors_json() -> String {
73 let cfg = crate::config_reload::current();
74 format!(
75 r#"{{"allow_all":{},"origins":{},"methods":{},"headers":{},"credentials":{},"expose_headers":{},"max_age":{}}}"#,
76 cfg.cors_allow_all,
77 quoted(&cfg.cors_allow_origins),
78 quoted(&cfg.cors_allow_methods),
79 quoted(&cfg.cors_allow_headers),
80 quoted(&cfg.cors_allow_credentials.to_string()),
81 quoted(&cfg.cors_expose_headers),
82 quoted(&cfg.cors_max_age.to_string()),
83 )
84}
85
86pub fn maintenance_json() -> String {
87 format!(r#"{{"active":{}}}"#, crate::maintenance::MAINTENANCE_MODE.load(Ordering::Relaxed))
88}
89
90pub fn set_maintenance(active: bool) -> String {
91 crate::maintenance::MAINTENANCE_MODE.store(active, Ordering::SeqCst);
92 crate::cluster::publish(crate::cluster::Op::Maintenance(active));
93 maintenance_json()
94}
95
96pub fn blocklist_json() -> String {
97 let list: Vec<String> = crate::blocklist::global().list().iter().map(|ip| quoted(ip)).collect();
98 format!("[{}]", list.join(","))
99}
100
101pub fn block(ip: &str) -> String {
102 crate::blocklist::global().block(ip);
103 crate::cluster::publish(crate::cluster::Op::Block(ip.to_string()));
104 format!(r#"{{"blocked":{}}}"#, quoted(ip))
105}
106
107pub fn unblock(ip: &str) -> String {
108 crate::blocklist::global().unblock(ip);
109 crate::cluster::publish(crate::cluster::Op::Unblock(ip.to_string()));
110 format!(r#"{{"unblocked":{}}}"#, quoted(ip))
111}
112
113pub fn flags_json() -> String {
115 let flags: Vec<String> =
116 crate::feature::global().list().iter().map(|(name, on)| format!(r#"{{"name":{},"enabled":{on}}}"#, quoted(name))).collect();
117 format!("[{}]", flags.join(","))
118}
119
120pub fn set_flag(name: &str, enabled: bool) -> String {
121 crate::feature::global().set(name, enabled);
122 crate::cluster::publish(crate::cluster::Op::Flag { name: name.to_string(), enabled });
123 format!(r#"{{"name":{},"enabled":{enabled}}}"#, quoted(name))
124}
125
126pub fn recent_json(n: usize, errors_only: bool) -> String {
129 let n = n.min(100);
130 let log = crate::request_log::global();
131 let entries = if errors_only { log.recent_errors(n) } else { log.recent(n) };
132 let entries: Vec<String> = entries
133 .iter()
134 .map(|e| {
135 format!(
136 r#"{{"ts":{},"method":{},"path":{},"status":{},"ip":{},"latency_ms":{}}}"#,
137 e.timestamp,
138 quoted(&e.method),
139 quoted(&e.path),
140 e.status,
141 quoted(&e.client_ip),
142 e.latency_ms
143 )
144 })
145 .collect();
146 format!("[{}]", entries.join(","))
147}
148
149fn cache_slot() -> &'static RwLock<Option<CacheLayer>> {
150 static SLOT: OnceLock<RwLock<Option<CacheLayer>>> = OnceLock::new();
151 SLOT.get_or_init(RwLock::default)
152}
153
154fn routes_slot() -> &'static RwLock<Vec<(String, String)>> {
155 static SLOT: OnceLock<RwLock<Vec<(String, String)>>> = OnceLock::new();
156 SLOT.get_or_init(RwLock::default)
157}
158
159pub fn register_cache(cache: CacheLayer) {
162 crate::cache::registry::register("app", cache.clone());
163 *cache_slot().write().unwrap_or_else(|e| e.into_inner()) = Some(cache);
164}
165
166pub fn register_routes(routes: Vec<(String, String)>) {
168 *routes_slot().write().unwrap_or_else(|e| e.into_inner()) = routes;
169}
170
171pub fn cache_stats_json(cache: &CacheLayer) -> String {
172 format!(r#"{{"hits":{},"misses":{},"size":{}}}"#, cache.hits(), cache.misses(), cache.size())
173}
174
175pub fn registered_cache_json() -> Option<String> {
177 cache_slot().read().unwrap_or_else(|e| e.into_inner()).as_ref().map(cache_stats_json)
178}
179
180pub fn clear_registered_cache() -> bool {
182 match cache_slot().read().unwrap_or_else(|e| e.into_inner()).as_ref() {
183 Some(cache) => {
184 cache.clear();
185 true
186 }
187 None => false,
188 }
189}
190
191pub fn routes_json(routes: &[(String, String)]) -> String {
192 let entries: Vec<String> =
193 routes.iter().map(|(m, p)| format!(r#"{{"method":{},"pattern":{}}}"#, quoted(m), quoted(p))).collect();
194 format!("[{}]", entries.join(","))
195}
196
197pub fn registered_routes_json() -> String {
198 routes_json(&routes_slot().read().unwrap_or_else(|e| e.into_inner()))
199}
200
201pub fn upstreams_json() -> String {
204 let upstreams: Vec<String> = crate::proxy_config::upstream::snapshot()
205 .iter()
206 .map(|(name, backends)| {
207 let backends: Vec<String> = backends
208 .iter()
209 .map(|b| {
210 format!(
211 r#"{{"url":{},"weight":{},"healthy":{},"breaker":{},"drained":{}}}"#,
212 quoted(&b.url),
213 b.weight,
214 b.healthy,
215 b.breaker.map_or("null".to_string(), quoted),
216 b.drained
217 )
218 })
219 .collect();
220 format!(r#"{{"name":{},"backends":[{}]}}"#, quoted(name), backends.join(","))
221 })
222 .collect();
223 format!("[{}]", upstreams.join(","))
224}
225
226pub fn set_drained(upstream: &str, backend: &str, drained: bool) -> Result<String, String> {
228 let snapshot = crate::proxy_config::upstream::snapshot();
229 let Some((_, backends)) = snapshot.iter().find(|(name, _)| name == upstream) else {
230 return Err(format!("no upstream {upstream:?}"));
231 };
232 if !backends.iter().any(|b| b.url == backend) {
233 return Err(format!("upstream {upstream:?} has no backend {backend:?}"));
234 }
235 crate::proxy_config::upstream::set_drained(upstream, backend, drained);
236 crate::cluster::publish(crate::cluster::Op::Drain { upstream: upstream.to_string(), backend: backend.to_string(), drained });
237 Ok(format!(r#"{{"upstream":{},"backend":{},"drained":{drained}}}"#, quoted(upstream), quoted(backend)))
238}
239
240pub fn certificates_json() -> String {
243 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0);
244 let mut certificates = crate::metrics::tls::certificates();
245 certificates.sort_by_key(|(_, not_after, _)| *not_after);
246 let list: Vec<String> = certificates
247 .iter()
248 .map(|(name, not_after, source)| {
249 format!(
250 r#"{{"name":{},"not_after":{},"expires":{},"days_left":{},"source":"{source}"}}"#,
251 quoted(name),
252 not_after,
253 quoted(&crate::http::date::format((*not_after).max(0) as u64)),
254 (not_after - now).div_euclid(86_400)
255 )
256 })
257 .collect();
258 format!(r#"{{"certificates":[{}],"acme":{}}}"#, list.join(","), acme_json())
259}
260
261#[cfg(feature = "acme")]
262fn acme_json() -> String {
263 let (running, last) = crate::acme::renewal_status();
264 let last = last.map_or("null".to_string(), |(at, outcome)| format!(r#"{{"at":{at},"outcome":{}}}"#, quoted(&outcome)));
265 format!(r#"{{"running":{running},"last_check":{last}}}"#)
266}
267
268#[cfg(not(feature = "acme"))]
269fn acme_json() -> String {
270 r#"{"running":false,"last_check":null}"#.to_string()
271}
272
273pub fn request_renewal() -> bool {
275 #[cfg(feature = "acme")]
276 return crate::acme::request_renewal();
277 #[cfg(not(feature = "acme"))]
278 false
279}
280
281pub fn logs_json(n: usize) -> String {
284 let lines: Vec<String> = crate::log::tail::recent(n).iter().map(|l| quoted(l)).collect();
285 format!("[{}]", lines.join(","))
286}
287
288pub fn caches_json() -> String {
291 let caches: Vec<String> = crate::cache::registry::list()
292 .iter()
293 .map(|(name, c)| {
294 format!(
295 r#"{{"name":{},"kind":"{}","entries":{},"bytes":{},"hits":{},"misses":{}}}"#,
296 quoted(name),
297 c.kind(),
298 c.size(),
299 c.stored_bytes().map_or("null".to_string(), |b| b.to_string()),
300 c.hits(),
301 c.misses()
302 )
303 })
304 .collect();
305 format!("[{}]", caches.join(","))
306}
307
308pub fn purge_cache(name: &str, path: Option<&str>, prefix: Option<&str>) -> Option<String> {
311 let cache = crate::cache::registry::get(name)?;
312 let target = match (path, prefix) {
313 (Some(p), _) if !p.is_empty() => crate::cache::Purge::Exact(p.to_string()),
314 (_, Some(p)) if !p.is_empty() => crate::cache::Purge::Prefix(p.to_string()),
315 _ => crate::cache::Purge::All,
316 };
317 let n = cache.purge(&target);
318 crate::cluster::publish(crate::cluster::Op::Purge { cache: name.to_string(), path: path.filter(|p| !p.is_empty()).map(str::to_string), prefix: prefix.filter(|p| !p.is_empty()).map(str::to_string) });
319 Some(format!(r#"{{"name":{},"purged":{n}}}"#, quoted(name)))
320}
321
322pub fn traffic_json(hours: u64, host: Option<&str>) -> Result<String, String> {
327 if !crate::traffic::enabled() {
328 return Err("traffic counting is off (on with the admin API unless [traffic] enabled = false)".to_string());
329 }
330 Ok(crate::traffic::summary::json(hours, host.filter(|h| !h.is_empty()), crate::traffic::record::now()))
331}
332
333fn authorizer_slot() -> &'static RwLock<Option<crate::authz::Authorizer>> {
336 static SLOT: OnceLock<RwLock<Option<crate::authz::Authorizer>>> = OnceLock::new();
337 SLOT.get_or_init(RwLock::default)
338}
339
340pub fn register_authorizer(authorizer: crate::authz::Authorizer) {
343 *authorizer_slot().write().unwrap_or_else(|e| e.into_inner()) = Some(authorizer);
344}
345
346pub fn authorizer() -> Result<crate::authz::Authorizer, String> {
349 if let Some(authorizer) = authorizer_slot().read().unwrap_or_else(|e| e.into_inner()).clone() {
350 return Ok(authorizer);
351 }
352 match crate::authz::config::current() {
353 Some(result) => result,
354 None => Err("no authorization configured ([authz] in the config file, or admin::ops::register_authorizer)".to_string()),
355 }
356}
357
358pub fn authz_schema_json() -> Result<String, String> {
360 let authorizer = authorizer()?;
361 let schema = authorizer.schema();
362 let types: Vec<String> = schema
363 .types()
364 .iter()
365 .map(|kind| format!("{}:[{}]", quoted(kind), schema.relations(kind).iter().map(|r| quoted(r)).collect::<Vec<_>>().join(",")))
366 .collect();
367 Ok(format!(r#"{{"types":{{{}}}}}"#, types.join(",")))
368}
369
370pub fn authz_tuples_json(object: Option<&str>, relation: Option<&str>, subject: Option<&str>, limit: usize) -> Result<String, String> {
372 let authorizer = authorizer()?;
373 let filter = crate::authz::TupleFilter {
374 object: object.filter(|o| o.contains(':')).map(crate::authz::ObjectRef::parse).transpose().map_err(|e| e.to_string())?,
375 object_kind: object.filter(|o| !o.contains(':')).map(str::to_string),
376 relation: relation.map(str::to_string),
377 subject: subject.map(crate::authz::Subject::parse).transpose().map_err(|e| e.to_string())?,
378 };
379 let tuples = authorizer.store().list(&filter, limit.clamp(1, 1000)).map_err(|e| e.to_string())?;
380 Ok(format!(r#"{{"tuples":[{}]}}"#, tuples.iter().map(|t| quoted(&t.to_string())).collect::<Vec<_>>().join(",")))
381}
382
383pub fn authz_write(tuple: &str) -> Result<String, String> {
385 let tuple = crate::authz::Tuple::parse(tuple).map_err(|e| e.to_string())?;
386 let written = authorizer()?.write(&tuple).map_err(|e| e.to_string())?;
387 Ok(format!(r#"{{"tuple":{},"written":{written}}}"#, quoted(&tuple.to_string())))
388}
389
390pub fn authz_delete(tuple: &str) -> Result<String, String> {
392 let tuple = crate::authz::Tuple::parse(tuple).map_err(|e| e.to_string())?;
393 let deleted = authorizer()?.delete(&tuple).map_err(|e| e.to_string())?;
394 Ok(format!(r#"{{"tuple":{},"deleted":{deleted}}}"#, quoted(&tuple.to_string())))
395}
396
397pub fn authz_check(subject: &str, relation: &str, object: &str) -> Result<String, String> {
400 let path = authorizer()?.explain(subject, relation, object).map_err(|e| e.to_string())?;
401 Ok(match path {
402 Some(path) => format!(r#"{{"allowed":true,"path":[{}]}}"#, path.iter().map(|p| quoted(p)).collect::<Vec<_>>().join(",")),
403 None => r#"{"allowed":false,"path":[]}"#.to_string(),
404 })
405}