1pub mod access;
24pub mod ops;
25mod ui;
26
27use std::sync::atomic::{AtomicUsize, Ordering};
28use std::sync::{Arc, OnceLock};
29use std::time::{Duration, Instant};
30
31use crate::application::Application;
32use crate::entry_point::Config;
33use crate::header::Header;
34use crate::mcp::json_escape;
35use crate::request::Request;
36use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
37use crate::server::{Address, ConnectionInfo};
38
39pub const DEFAULT_TOKEN_ENV: &str = "RWS_ADMIN_TOKEN";
41pub const MAX_CONNECTIONS: usize = 32;
43pub const API_PREFIX: &str = "/admin/api/v1";
45
46#[derive(Debug, Clone, Default, PartialEq)]
48pub struct AdminConfig {
49 pub listen: String,
51 pub token_env: String,
53 pub allowed_origins: Vec<String>,
55 pub users: String,
57 pub audit_log: String,
59 pub oidc: AdminOidcConfig,
61}
62
63#[derive(Debug, Clone, Default, PartialEq)]
65pub struct AdminOidcConfig {
66 pub issuer: String,
67 pub provider: String,
68 pub tenant: String,
69 pub client_id: String,
70 pub client_secret_env: String,
71 pub cookie_secret_env: String,
72 pub roles: String,
74 pub role_claim: String,
76}
77
78impl AdminOidcConfig {
79 pub fn enabled(&self) -> bool {
80 !self.issuer.is_empty() || !self.provider.is_empty()
81 }
82}
83
84pub const RWS_CONFIG_ADMIN_USERS: &str = "RWS_CONFIG_ADMIN_USERS";
86pub const RWS_CONFIG_ADMIN_AUDIT_LOG: &str = "RWS_CONFIG_ADMIN_AUDIT_LOG";
87pub const RWS_CONFIG_ADMIN_OIDC_ISSUER: &str = "RWS_CONFIG_ADMIN_OIDC_ISSUER";
88pub const RWS_CONFIG_ADMIN_OIDC_PROVIDER: &str = "RWS_CONFIG_ADMIN_OIDC_PROVIDER";
89pub const RWS_CONFIG_ADMIN_OIDC_TENANT: &str = "RWS_CONFIG_ADMIN_OIDC_TENANT";
90pub const RWS_CONFIG_ADMIN_OIDC_CLIENT_ID: &str = "RWS_CONFIG_ADMIN_OIDC_CLIENT_ID";
91pub const RWS_CONFIG_ADMIN_OIDC_CLIENT_SECRET_ENV: &str = "RWS_CONFIG_ADMIN_OIDC_CLIENT_SECRET_ENV";
92pub const RWS_CONFIG_ADMIN_OIDC_COOKIE_SECRET_ENV: &str = "RWS_CONFIG_ADMIN_OIDC_COOKIE_SECRET_ENV";
93pub const RWS_CONFIG_ADMIN_OIDC_ROLES: &str = "RWS_CONFIG_ADMIN_OIDC_ROLES";
94pub const RWS_CONFIG_ADMIN_OIDC_ROLE_CLAIM: &str = "RWS_CONFIG_ADMIN_OIDC_ROLE_CLAIM";
95
96impl AdminConfig {
97 pub fn from_env() -> AdminConfig {
98 let read = |name: &str| std::env::var(name).unwrap_or_default().trim().to_string();
99 let token_env = read(Config::RWS_CONFIG_ADMIN_TOKEN_ENV);
100 AdminConfig {
101 listen: read(Config::RWS_CONFIG_ADMIN_LISTEN),
102 token_env: if token_env.is_empty() { DEFAULT_TOKEN_ENV.to_string() } else { token_env },
103 allowed_origins: read(Config::RWS_CONFIG_ADMIN_ALLOWED_ORIGINS)
104 .split(',')
105 .map(|o| o.trim().trim_end_matches('/').to_string())
106 .filter(|o| !o.is_empty())
107 .collect(),
108 users: read(RWS_CONFIG_ADMIN_USERS),
109 audit_log: read(RWS_CONFIG_ADMIN_AUDIT_LOG),
110 oidc: AdminOidcConfig {
111 issuer: read(RWS_CONFIG_ADMIN_OIDC_ISSUER).trim_end_matches('/').to_string(),
112 provider: read(RWS_CONFIG_ADMIN_OIDC_PROVIDER).to_ascii_lowercase(),
113 tenant: read(RWS_CONFIG_ADMIN_OIDC_TENANT),
114 client_id: read(RWS_CONFIG_ADMIN_OIDC_CLIENT_ID),
115 client_secret_env: read(RWS_CONFIG_ADMIN_OIDC_CLIENT_SECRET_ENV),
116 cookie_secret_env: read(RWS_CONFIG_ADMIN_OIDC_COOKIE_SECRET_ENV),
117 roles: read(RWS_CONFIG_ADMIN_OIDC_ROLES),
118 role_claim: read(RWS_CONFIG_ADMIN_OIDC_ROLE_CLAIM),
119 },
120 }
121 }
122
123 pub fn enabled(&self) -> bool {
124 !self.listen.is_empty()
125 }
126
127 pub fn token(&self) -> Result<String, String> {
129 match std::env::var(&self.token_env) {
130 Ok(t) if !t.trim().is_empty() => Ok(t.trim().to_string()),
131 _ => Err(format!("the admin token variable {} is unset or empty", self.token_env)),
132 }
133 }
134}
135
136fn started() -> Instant {
138 static STARTED: OnceLock<Instant> = OnceLock::new();
139 *STARTED.get_or_init(Instant::now)
140}
141
142#[derive(Clone)]
144pub struct AdminApi {
145 inner: Arc<Inner>,
146}
147
148type Reloader = Arc<dyn Fn() + Send + Sync>;
149
150struct Inner {
151 users: Vec<access::TokenUser>,
152 allowed_origins: Vec<String>,
153 reloader: Reloader,
154 reload_wait: Duration,
155 #[cfg(feature = "sso")]
156 oidc: Option<Arc<AdminOidc>>,
157}
158
159#[cfg(feature = "sso")]
162struct AdminOidc {
163 layer: crate::proxy_config::oidc_route::LazyOidc,
164 roles: Vec<(String, access::Role)>,
165}
166
167impl AdminApi {
168 pub fn new(token: impl Into<String>) -> AdminApi {
170 started();
171 crate::log::tail::enable();
172 AdminApi {
173 inner: Arc::new(Inner {
174 users: vec![access::TokenUser { name: "admin".to_string(), token: token.into(), role: access::Role::Admin }],
175 allowed_origins: Vec::new(),
176 reloader: Arc::new(crate::config_reload::request_reload),
177 reload_wait: Duration::from_secs(10),
178 #[cfg(feature = "sso")]
179 oidc: None,
180 }),
181 }
182 }
183
184 fn edit(self, f: impl FnOnce(&mut Inner)) -> AdminApi {
185 let mut inner = Inner {
186 users: self.inner.users.clone(),
187 allowed_origins: self.inner.allowed_origins.clone(),
188 reloader: Arc::clone(&self.inner.reloader),
189 reload_wait: self.inner.reload_wait,
190 #[cfg(feature = "sso")]
191 oidc: self.inner.oidc.clone(),
192 };
193 f(&mut inner);
194 AdminApi { inner: Arc::new(inner) }
195 }
196
197 pub fn users(self, users: Vec<access::TokenUser>) -> AdminApi {
199 self.edit(|i| i.users.extend(users))
200 }
201
202 #[cfg(feature = "sso")]
205 pub fn oidc(self, config: &AdminOidcConfig) -> Result<AdminApi, String> {
206 let roles = access::parse_role_map(&config.roles)?;
207 if roles.is_empty() {
208 return Err("[admin] oidc_roles is empty: nobody signed in would get a role".to_string());
209 }
210 let layer = crate::proxy_config::oidc_route::LazyOidc::new(crate::proxy_config::OidcRouteAuth {
211 issuer: config.issuer.clone(),
212 provider: config.provider.clone(),
213 tenant: config.tenant.clone(),
214 client_id: config.client_id.clone(),
215 client_secret_env: config.client_secret_env.clone(),
216 cookie_secret_env: config.cookie_secret_env.clone(),
217 groups_claim: config.role_claim.clone(),
218 session_ttl_secs: 12 * 3600,
219 path_prefix: "/admin".to_string(),
220 ..Default::default()
221 })?;
222 Ok(self.edit(|i| i.oidc = Some(Arc::new(AdminOidc { layer, roles }))))
223 }
224
225 pub fn allowed_origins(self, origins: Vec<String>) -> AdminApi {
227 self.edit(|i| i.allowed_origins = origins)
228 }
229
230 pub fn reloader(self, reload: impl Fn() + Send + Sync + 'static, wait: Duration) -> AdminApi {
234 self.edit(|i| {
235 i.reloader = Arc::new(reload);
236 i.reload_wait = wait;
237 })
238 }
239
240 fn reload(&self) -> Response {
243 if !crate::metrics::SERVER_READY.load(Ordering::SeqCst) {
244 return error(STATUS_CODE_REASON_PHRASE.n503_service_unavailable, "the server is starting or stopping");
246 }
247 let (before, _) = crate::config_reload::last_reload();
248 (self.inner.reloader)();
249 let deadline = Instant::now() + self.inner.reload_wait;
250 loop {
251 let (count, errors) = crate::config_reload::last_reload();
252 if count > before {
253 let errors: Vec<String> = errors.iter().map(|e| format!("\"{}\"", json_escape(e))).collect();
254 let applied = errors.is_empty();
255 return json(STATUS_CODE_REASON_PHRASE.n200_ok, format!(r#"{{"reloaded":true,"applied":{applied},"reloads":{count},"errors":[{}]}}"#, errors.join(",")));
256 }
257 if Instant::now() >= deadline {
258 return error(STATUS_CODE_REASON_PHRASE.n504_gateway_timeout, "the reload didn't finish in time; check the server log");
259 }
260 std::thread::sleep(Duration::from_millis(20));
261 }
262 }
263
264 fn authenticate(&self, request: &Request) -> Authenticated {
267 if let Some(sent) = request.header("Authorization").and_then(|h| h.value.strip_prefix("Bearer ").map(str::trim)) {
268 let mut found = None;
270 for user in &self.inner.users {
271 if crate::proxy_config::constant_time_eq(sent.as_bytes(), user.token.as_bytes()) {
272 found = Some(access::Principal { user: user.name.clone(), role: user.role, via: "token" });
273 }
274 }
275 return found.map_or(Authenticated::No, Authenticated::Yes);
276 }
277 #[cfg(feature = "sso")]
278 if let Some(oidc) = &self.inner.oidc {
279 if let Some(user) = oidc.layer.auth().and_then(|auth| auth.signed_in(request)) {
280 let name = user.email.clone().unwrap_or_else(|| user.sub.clone());
281 return match access::role_for(&oidc.roles, user.email.as_deref(), &user.groups) {
282 Some(role) => Authenticated::Yes(access::Principal { user: name, role, via: "oidc" }),
283 None => Authenticated::NoRole(name),
284 };
285 }
286 }
287 Authenticated::No
288 }
289
290 fn oidc_enabled(&self) -> bool {
291 #[cfg(feature = "sso")]
292 return self.inner.oidc.is_some();
293 #[cfg(not(feature = "sso"))]
294 false
295 }
296
297 fn origin_allowed(&self, request: &Request) -> bool {
299 let Some(origin) = request.header("Origin").map(|h| h.value.trim().trim_end_matches('/').to_string()) else {
300 return true;
301 };
302 let host = request.header("Host").map(|h| h.value.trim().to_string()).unwrap_or_default();
303 let own = [format!("http://{host}"), format!("https://{host}")];
304 own.contains(&origin) || self.inner.allowed_origins.contains(&origin)
305 }
306}
307
308pub(crate) fn json(status: &crate::response::StatusCodeReasonPhrase, body: String) -> Response {
309 let mut response = Response::json(status, body.into_bytes());
310 response.headers.push(Header::new("Cache-Control", "no-store"));
311 response
312}
313
314pub(crate) fn error(status: &crate::response::StatusCodeReasonPhrase, message: &str) -> Response {
315 json(status, format!(r#"{{"error":"{}"}}"#, json_escape(message)))
316}
317
318impl Application for AdminApi {
319 fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
320 if !self.origin_allowed(request) {
321 return Ok(error(STATUS_CODE_REASON_PHRASE.n403_forbidden, "origin not allowed"));
322 }
323 let path = crate::url::URL::request_target_path(&request.request_uri).unwrap_or_default();
324 if request.method == "GET" || request.method == "HEAD" {
325 if path == "/admin" || path == "/" {
326 return Ok(ui::redirect());
327 }
328 if let Some(file) = ui::file(path) {
329 return Ok(file);
330 }
331 }
332 if path.starts_with("/admin/auth/") {
334 #[cfg(feature = "sso")]
335 if let Some(oidc) = &self.inner.oidc {
336 return match oidc.layer.auth() {
337 Some(auth) => crate::middleware::Middleware::handle(&*auth, request, connection, &NotFound),
338 None => Ok(error(STATUS_CODE_REASON_PHRASE.n503_service_unavailable, "the identity provider can't be reached; try again shortly")),
339 };
340 }
341 return Ok(error(STATUS_CODE_REASON_PHRASE.n404_not_found, "OIDC sign-in isn't configured"));
342 }
343 let route = path.strip_prefix(API_PREFIX).map(|r| r.trim_end_matches('/'));
344 if route == Some("/auth") && request.method == "GET" {
346 return Ok(ok(format!(r#"{{"token":true,"oidc":{}}}"#, self.oidc_enabled())));
347 }
348 let address = connection.client.ip.clone();
349 let principal = match self.authenticate(request) {
350 Authenticated::Yes(p) => p,
351 Authenticated::NoRole(name) => {
352 let response = error(STATUS_CODE_REASON_PHRASE.n403_forbidden, &format!("{name} is signed in but has no admin role (see [admin] oidc_roles)"));
353 if !is_read(&request.method) {
354 access::record_change(&access::Principal { user: name, role: access::Role::Viewer, via: "oidc" }, &address, &request.method, path, &request.body, 403);
355 }
356 return Ok(response);
357 }
358 Authenticated::No => {
359 if !is_read(&request.method) && request.header("Authorization").is_some() {
360 access::record_change(&access::Principal { user: "(bad token)".to_string(), role: access::Role::Viewer, via: "token" }, &address, &request.method, path, &request.body, 401);
361 }
362 let mut response = error(STATUS_CODE_REASON_PHRASE.n401_unauthorized, "missing or wrong bearer token");
363 response.headers.push(Header::new("WWW-Authenticate", r#"Bearer realm="rws admin""#));
364 return Ok(response);
365 }
366 };
367 let Some(route) = route else {
368 return Ok(error(STATUS_CODE_REASON_PHRASE.n404_not_found, "not found"));
369 };
370 if principal.via == "oidc" && !is_read(&request.method) && request.header("X-Rws-Admin").is_none() {
373 access::record_change(&principal, &address, &request.method, path, &request.body, 403);
374 return Ok(error(STATUS_CODE_REASON_PHRASE.n403_forbidden, "missing X-Rws-Admin header"));
375 }
376 let needed = access::required_role(&request.method, route);
377 if principal.role < needed {
378 access::record_change(&principal, &address, &request.method, path, &request.body, 403);
379 return Ok(error(STATUS_CODE_REASON_PHRASE.n403_forbidden, &format!("{} has role {}; this needs {}", principal.user, principal.role.as_str(), needed.as_str())));
380 }
381 let response = match (request.method.as_str(), route) {
382 ("GET", "/whoami") => ok(format!(r#"{{"user":"{}","role":"{}","via":"{}"}}"#, json_escape(&principal.user), principal.role.as_str(), principal.via)),
383 ("GET", "/audit") => ok(access::recent_json(query(request, "n").and_then(|n| n.parse().ok()).unwrap_or(100).min(access::AUDIT_CAPACITY))),
384 (_, "/whoami" | "/audit") => wrong_method("GET"),
385 _ => self.route(request, connection, route),
386 };
387 if !is_read(&request.method) && !(request.method == "POST" && route == "/authz/check") {
388 access::record_change(&principal, &address, &request.method, path, &request.body, response.status_code as u16);
389 }
390 Ok(response)
391 }
392}
393
394enum Authenticated {
396 Yes(access::Principal),
397 NoRole(String),
399 No,
400}
401
402fn is_read(method: &str) -> bool {
403 matches!(method, "GET" | "HEAD" | "OPTIONS")
404}
405
406#[cfg(feature = "sso")]
409struct NotFound;
410
411#[cfg(feature = "sso")]
412impl Application for NotFound {
413 fn execute(&self, _: &Request, _: &ConnectionInfo) -> Result<Response, String> {
414 Ok(error(STATUS_CODE_REASON_PHRASE.n404_not_found, "not found"))
415 }
416}
417
418fn ok(body: String) -> Response {
419 json(STATUS_CODE_REASON_PHRASE.n200_ok, body)
420}
421
422fn bad_request(message: &str) -> Response {
423 error(STATUS_CODE_REASON_PHRASE.n400_bad_request, message)
424}
425
426fn body_bool(request: &Request, key: &str) -> Option<bool> {
428 let body = std::str::from_utf8(&request.body).ok()?;
429 let value = crate::mcp::json_rpc::object_members(body)?.into_iter().find(|(k, _)| k == key)?.1;
430 match value.as_str() {
431 "true" => Some(true),
432 "false" => Some(false),
433 _ => None,
434 }
435}
436
437fn body_str(request: &Request, key: &str) -> Option<String> {
439 let body = std::str::from_utf8(&request.body).ok()?;
440 let value = crate::mcp::json_rpc::object_members(body)?.into_iter().find(|(k, _)| k == key)?.1;
441 value.starts_with('"').then(|| crate::mcp::json_rpc::extract_str(&format!(r#"{{"v":{value}}}"#), "v")).flatten()
442}
443
444fn wrong_method(allowed: &str) -> Response {
446 let mut response = error(STATUS_CODE_REASON_PHRASE.n405_method_not_allowed, &format!("use {allowed}"));
447 response.headers.push(Header::new("Allow", allowed.to_string()));
448 response
449}
450
451fn authz_route(method: &str, route: &str, request: &Request) -> Response {
453 let decoded = |key: &str| query(request, key).map(|v| crate::url::URL::percent_decode(&v)).filter(|v| !v.is_empty());
454 let answer = |result: Result<String, String>, missing_is_404: bool| match result {
457 Ok(body) => ok(body),
458 Err(e) if missing_is_404 && e.starts_with("no authorization configured") => error(STATUS_CODE_REASON_PHRASE.n404_not_found, &e),
459 Err(e) => error(STATUS_CODE_REASON_PHRASE.n400_bad_request, &e),
460 };
461 match (method, route) {
462 ("GET", "/authz") => answer(ops::authz_schema_json(), true),
463 (_, "/authz") => wrong_method("GET"),
464 ("GET", "/authz/tuples") => {
465 let limit = decoded("limit").and_then(|n| n.parse().ok()).unwrap_or(100);
466 answer(ops::authz_tuples_json(decoded("object").as_deref(), decoded("relation").as_deref(), decoded("subject").as_deref(), limit), true)
467 }
468 ("POST", "/authz/tuples") => match body_str(request, "tuple") {
469 Some(tuple) => answer(ops::authz_write(&tuple), true),
470 None => bad_request(r#"expected {"tuple":"doc:42#viewer@user:alice"}"#),
471 },
472 ("DELETE", "/authz/tuples") => match decoded("tuple").or_else(|| body_str(request, "tuple")) {
473 Some(tuple) => answer(ops::authz_delete(&tuple), true),
474 None => bad_request(r#"expected ?tuple=… or {"tuple":"doc:42#viewer@user:alice"}"#),
475 },
476 (_, "/authz/tuples") => wrong_method("GET, POST, DELETE"),
477 ("POST", "/authz/check") => match (body_str(request, "subject"), body_str(request, "relation"), body_str(request, "object")) {
478 (Some(subject), Some(relation), Some(object)) => answer(ops::authz_check(&subject, &relation, &object), true),
479 _ => bad_request(r#"expected {"subject":"user:alice","relation":"viewer","object":"doc:42"}"#),
480 },
481 (_, "/authz/check") => wrong_method("POST"),
482 _ => error(STATUS_CODE_REASON_PHRASE.n404_not_found, "unknown /authz endpoint"),
483 }
484}
485
486fn query(request: &Request, key: &str) -> Option<String> {
488 let (_, query) = request.request_uri.split_once('?')?;
489 query.split('&').find_map(|pair| {
490 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
491 (k == key).then(|| v.to_string())
492 })
493}
494
495fn parse_ip(ip: &str) -> Option<String> {
496 let ip = crate::url::URL::percent_decode(ip.trim());
497 ip.parse::<std::net::IpAddr>().ok().map(|ip| ip.to_string())
498}
499
500impl AdminApi {
501 fn route(&self, request: &Request, connection: &ConnectionInfo, route: &str) -> Response {
502 let method = request.method.as_str();
503 if let Some(ip) = route.strip_prefix("/blocklist/") {
504 return match (method, parse_ip(ip)) {
505 ("DELETE", Some(ip)) => ok(ops::unblock(&ip)),
506 ("DELETE", None) => bad_request("not an IP address"),
507 _ => wrong_method("DELETE"),
508 };
509 }
510 if let Some(name) = route.strip_prefix("/flags/") {
511 let name = crate::url::URL::percent_decode(name);
512 return match method {
513 "PUT" => match body_bool(request, "enabled") {
514 Some(enabled) if !name.is_empty() => ok(ops::set_flag(&name, enabled)),
515 _ => bad_request(r#"expected {"enabled":true|false}"#),
516 },
517 _ => wrong_method("PUT"),
518 };
519 }
520 if let Some(name) = route.strip_prefix("/upstreams/").and_then(|r| r.strip_suffix("/drain")) {
521 let name = crate::url::URL::percent_decode(name);
522 return match (method, body_str(request, "backend"), body_bool(request, "drained")) {
523 ("PUT", Some(backend), Some(drained)) => match ops::set_drained(&name, &backend, drained) {
524 Ok(body) => ok(body),
525 Err(e) => error(STATUS_CODE_REASON_PHRASE.n404_not_found, &e),
526 },
527 ("PUT", _, _) => bad_request(r#"expected {"backend":"<url>","drained":true|false}"#),
528 _ => wrong_method("PUT"),
529 };
530 }
531 if let Some(name) = route.strip_prefix("/caches/") {
532 let name = crate::url::URL::percent_decode(name);
533 let decoded = |key: &str| query(request, key).map(|v| crate::url::URL::percent_decode(&v));
534 return match method {
535 "DELETE" => match ops::purge_cache(&name, decoded("path").as_deref(), decoded("prefix").as_deref()) {
536 Some(body) => ok(body),
537 None => error(STATUS_CODE_REASON_PHRASE.n404_not_found, &format!("no cache named {name:?}")),
538 },
539 _ => wrong_method("DELETE"),
540 };
541 }
542 if route == "/traffic" {
543 if method != "GET" {
544 return wrong_method("GET");
545 }
546 let hours = query(request, "hours").and_then(|h| h.parse().ok()).unwrap_or(24);
547 let host = query(request, "host").map(|h| crate::url::URL::percent_decode(&h));
548 return match ops::traffic_json(hours, host.as_deref()) {
549 Ok(body) => ok(body),
550 Err(e) => error(STATUS_CODE_REASON_PHRASE.n404_not_found, &e),
551 };
552 }
553 if route == "/authz" || route.starts_with("/authz/") {
554 return authz_route(method, route, request);
555 }
556 match (method, route) {
557 ("GET", "/caches") => ok(ops::caches_json()),
558 (_, "/caches") => wrong_method("GET"),
559 ("GET", "/logs") => {
560 let n = query(request, "n").and_then(|n| n.parse().ok()).unwrap_or(100);
561 ok(ops::logs_json(n))
562 }
563 (_, "/logs") => wrong_method("GET"),
564 ("GET", "/logs/stream") => {
565 let (sender, mut response) = crate::sse::SseStream::new().buffer(256).start();
566 if !crate::log::tail::follow(sender) {
567 return error(STATUS_CODE_REASON_PHRASE.n503_service_unavailable, "too many log followers");
568 }
569 response.headers.push(Header::new("Cache-Control", "no-store"));
570 response
571 }
572 (_, "/logs/stream") => wrong_method("GET"),
573 ("GET", "/upstreams") => ok(ops::upstreams_json()),
574 (_, "/upstreams") => wrong_method("GET"),
575 ("GET", "/certificates") => ok(ops::certificates_json()),
576 (_, "/certificates") => wrong_method("GET"),
577 ("POST", "/certificates/renew") => match ops::request_renewal() {
578 true => json(STATUS_CODE_REASON_PHRASE.n202_accepted, r#"{"requested":true}"#.to_string()),
579 false => error(STATUS_CODE_REASON_PHRASE.n404_not_found, "ACME isn't configured: nothing to renew"),
580 },
581 (_, "/certificates/renew") => wrong_method("POST"),
582 ("GET", "/status") => ok(status_json(connection)),
583 (_, "/status") => wrong_method("GET"),
584 ("GET", "/config") => ok(ops::config_json()),
585 (_, "/config") => wrong_method("GET"),
586 ("POST", "/reload") => self.reload(),
587 (_, "/reload") => wrong_method("POST"),
588 ("GET", "/metrics") => ok(ops::metrics_json()),
589 (_, "/metrics") => wrong_method("GET"),
590 ("GET", "/build") => ok(ops::build_features_json()),
591 (_, "/build") => wrong_method("GET"),
592 ("GET", "/rate-limit") => ok(ops::rate_limit_json()),
593 (_, "/rate-limit") => wrong_method("GET"),
594 ("GET", "/rate-limit/check") => match query(request, "ip").as_deref().and_then(parse_ip) {
595 Some(ip) => ok(ops::check_rate_limit_json(&ip)),
596 None => bad_request("expected ?ip=<address>"),
597 },
598 (_, "/rate-limit/check") => wrong_method("GET"),
599 ("GET", "/cors") => ok(ops::cors_json()),
600 (_, "/cors") => wrong_method("GET"),
601 ("GET", "/maintenance") => ok(ops::maintenance_json()),
602 ("PUT", "/maintenance") => match body_bool(request, "active") {
603 Some(active) => ok(ops::set_maintenance(active)),
604 None => bad_request(r#"expected {"active":true|false}"#),
605 },
606 (_, "/maintenance") => wrong_method("GET, PUT"),
607 ("GET", "/blocklist") => ok(ops::blocklist_json()),
608 ("POST", "/blocklist") => match body_str(request, "ip").as_deref().and_then(parse_ip) {
609 Some(ip) => ok(ops::block(&ip)),
610 None => bad_request(r#"expected {"ip":"<address>"}"#),
611 },
612 (_, "/blocklist") => wrong_method("GET, POST"),
613 ("GET", "/flags") => ok(ops::flags_json()),
614 (_, "/flags") => wrong_method("GET"),
615 ("GET", "/cache") => match ops::registered_cache_json() {
616 Some(stats) => ok(stats),
617 None => error(STATUS_CODE_REASON_PHRASE.n404_not_found, "no response cache in this server"),
618 },
619 ("DELETE", "/cache") => match ops::clear_registered_cache() {
620 true => ok(r#"{"cleared":true}"#.to_string()),
621 false => error(STATUS_CODE_REASON_PHRASE.n404_not_found, "no response cache in this server"),
622 },
623 (_, "/cache") => wrong_method("GET, DELETE"),
624 ("GET", "/routes") => ok(ops::registered_routes_json()),
625 (_, "/routes") => wrong_method("GET"),
626 ("GET", "/requests" | "/errors") => {
627 let n = query(request, "n").and_then(|n| n.parse().ok()).unwrap_or(20);
628 ok(ops::recent_json(n, route == "/errors"))
629 }
630 (_, "/requests" | "/errors") => wrong_method("GET"),
631 _ => error(STATUS_CODE_REASON_PHRASE.n404_not_found, "not found"),
632 }
633 }
634}
635
636pub fn features() -> Vec<&'static str> {
638 let mut on = Vec::new();
639 macro_rules! feature {
640 ($($name:literal),*) => { $( if cfg!(feature = $name) { on.push($name); } )* };
641 }
642 feature!("http1", "http2", "http3", "acme", "auth", "sso", "sso-saml", "sso-server", "gateway-api", "tower", "openapi", "mimalloc", "brotli", "zstd", "wasm-plugins");
643 on
644}
645
646fn status_json(connection: &ConnectionInfo) -> String {
647 let env = |name: &str| std::env::var(name).unwrap_or_default();
648 let tls = !env(Config::RWS_CONFIG_TLS_CERT_FILE).is_empty() || !env(Config::RWS_CONFIG_ACME_DOMAINS).is_empty();
649 let redirect = env(Config::RWS_CONFIG_HTTP_REDIRECT_PORT);
650 let features: Vec<String> = features().iter().map(|f| format!("\"{f}\"")).collect();
651 let disk: Vec<String> = crate::disk_guard::status()
654 .into_iter()
655 .map(|(dir, free, total, low)| format!(r#"{{"dir":"{}","free_bytes":{free},"total_bytes":{total},"low":{low}}}"#, json_escape(&dir)))
656 .collect();
657 format!(
658 r#"{{"version":"{}","pid":{},"uptime_secs":{},"ready":{},"features":[{}],"listeners":{{"main":{{"address":"{}:{}","tls":{tls}}},"redirect":{},"admin":"{}:{}"}},"disk":[{}],"cluster":{}}}"#,
659 env!("CARGO_PKG_VERSION"),
660 std::process::id(),
661 started().elapsed().as_secs(),
662 crate::metrics::SERVER_READY.load(Ordering::SeqCst),
663 features.join(","),
664 json_escape(&env(Config::RWS_CONFIG_IP)),
665 json_escape(&env(Config::RWS_CONFIG_PORT)),
666 if redirect.trim().is_empty() { "null".to_string() } else { format!("\"{}\"", json_escape(redirect.trim())) },
667 json_escape(&connection.server.ip),
668 connection.server.port,
669 disk.join(","),
670 crate::cluster::status_json().unwrap_or_else(|| "null".to_string()),
671 )
672}
673
674pub(crate) fn validate(get: &dyn Fn(&str) -> Option<String>, report: &mut crate::config_check::ConfigReport) {
677 let listen = get(Config::RWS_CONFIG_ADMIN_LISTEN).unwrap_or_default();
678 let listen = listen.trim();
679 if listen.is_empty() {
680 for key in [Config::RWS_CONFIG_ADMIN_TOKEN_ENV, Config::RWS_CONFIG_ADMIN_ALLOWED_ORIGINS] {
681 if get(key).is_some_and(|v| !v.trim().is_empty()) {
682 report.warn("[admin] has settings but no listen: the admin API is off");
683 break;
684 }
685 }
686 return;
687 }
688 match listen.parse::<std::net::SocketAddr>() {
689 Err(_) => report.error(format!("[admin] listen = {listen:?}: expected host:port with an IP address, e.g. \"127.0.0.1:9901\"")),
690 Ok(addr) if !addr.ip().is_loopback() => report.warn(format!(
691 "[admin] listen = {listen:?} is reachable from other hosts: firewall it, or listen on 127.0.0.1 and use an SSH tunnel"
692 )),
693 Ok(_) => {}
694 }
695 let token_env = get(Config::RWS_CONFIG_ADMIN_TOKEN_ENV).map(|v| v.trim().to_string()).filter(|v| !v.is_empty());
696 let token_env = token_env.unwrap_or_else(|| DEFAULT_TOKEN_ENV.to_string());
697 if !crate::config_check::offline() && std::env::var(&token_env).unwrap_or_default().trim().is_empty() {
698 report.error(format!("[admin] reads its token from {token_env}, which is unset or empty: the admin API won't start"));
699 }
700 for origin in get(Config::RWS_CONFIG_ADMIN_ALLOWED_ORIGINS).unwrap_or_default().split(',').map(str::trim).filter(|o| !o.is_empty()) {
701 if !(origin.starts_with("http://") || origin.starts_with("https://")) || origin.trim_end_matches('/').matches('/').count() > 2 {
702 report.error(format!("[admin] allowed_origins: {origin:?} is not an origin (scheme://host[:port])"));
703 }
704 }
705 let users = get(RWS_CONFIG_ADMIN_USERS).unwrap_or_default();
707 let env = |name: &str| if crate::config_check::offline() { Some("offline-check-placeholder".to_string()) } else { std::env::var(name).ok() };
708 if let Err(e) = access::parse_users(&users, &env) {
709 report.error(e);
710 }
711 let audit_log = get(RWS_CONFIG_ADMIN_AUDIT_LOG).unwrap_or_default();
712 if let Some(dir) = std::path::Path::new(audit_log.trim()).parent().filter(|d| !audit_log.trim().is_empty() && !d.as_os_str().is_empty()) {
713 if !crate::config_check::offline() && !dir.is_dir() {
714 report.error(format!("[admin] audit_log: directory {} doesn't exist", dir.display()));
715 }
716 }
717 let issuer = get(RWS_CONFIG_ADMIN_OIDC_ISSUER).unwrap_or_default();
718 let provider = get(RWS_CONFIG_ADMIN_OIDC_PROVIDER).unwrap_or_default();
719 if !issuer.trim().is_empty() || !provider.trim().is_empty() {
720 if get(RWS_CONFIG_ADMIN_OIDC_CLIENT_ID).unwrap_or_default().trim().is_empty() {
721 report.error("[admin] oidc sign-in has no oidc_client_id");
722 }
723 match access::parse_role_map(&get(RWS_CONFIG_ADMIN_OIDC_ROLES).unwrap_or_default()) {
724 Ok(map) if map.is_empty() => report.error("[admin] oidc sign-in needs oidc_roles (e.g. [\"rws-admins=admin\", \"*=viewer\"]): nobody would get a role"),
725 Ok(_) => {}
726 Err(e) => report.error(e),
727 }
728 if !provider.trim().is_empty() && !matches!(provider.trim().to_ascii_lowercase().as_str(), "google" | "microsoft" | "github") {
729 report.error(format!("[admin] oidc_provider = {provider:?}: expected google, microsoft or github (others: set oidc_issuer)"));
730 }
731 let cookie = get(RWS_CONFIG_ADMIN_OIDC_COOKIE_SECRET_ENV).unwrap_or_default();
732 if cookie.trim().is_empty() {
733 report.warn("[admin] oidc sign-in without oidc_cookie_secret_env: sign-ins end when rws restarts");
734 }
735 #[cfg(not(feature = "sso"))]
736 report.error("[admin] oidc sign-in needs the sso feature, which this build doesn't include; the admin API won't start");
737 }
738}
739
740pub fn start(config: &AdminConfig) -> Result<Option<std::net::SocketAddr>, String> {
743 if !config.enabled() {
744 return Ok(None);
745 }
746 let token = config.token()?;
747 #[cfg(unix)]
748 let inherited = config.listen.parse().ok().and_then(|addr| crate::systemd::tcp(addr, Some("admin")));
749 #[cfg(not(unix))]
750 let inherited: Option<std::net::TcpListener> = None;
751 let listener = match inherited {
752 Some(listener) => listener,
753 None => std::net::TcpListener::bind(&config.listen).map_err(|e| format!("can't listen on {}: {e}", config.listen))?,
754 };
755 let local = listener.local_addr().map_err(|e| e.to_string())?;
756 let users = access::parse_users(&config.users, &|name| std::env::var(name).ok())?;
757 access::set_audit_file(&config.audit_log);
758 #[allow(unused_mut)]
759 let mut api = AdminApi::new(token).allowed_origins(config.allowed_origins.clone()).users(users);
760 if config.oidc.enabled() {
761 #[cfg(feature = "sso")]
762 {
763 api = api.oidc(&config.oidc)?;
764 }
765 #[cfg(not(feature = "sso"))]
766 return Err("[admin] oidc_* needs the sso feature, which this build doesn't include".to_string());
767 }
768 std::thread::Builder::new()
769 .name("rws-admin".to_string())
770 .spawn(move || serve(listener, api))
771 .map_err(|e| e.to_string())?;
772 Ok(Some(local))
773}
774
775pub fn start_from_env() {
778 match start(&AdminConfig::from_env()) {
779 Ok(Some(addr)) => {
780 println!("admin API at http://{addr}{API_PREFIX}/");
781 if crate::traffic::config::enabled_setting() != Some(false) {
784 crate::traffic::start();
785 }
786 }
787 Ok(None) => {}
788 Err(e) => eprintln!("admin API not started: {e}"),
789 }
790}
791
792fn serve(listener: std::net::TcpListener, api: AdminApi) {
793 let open = Arc::new(AtomicUsize::new(0));
794 for stream in listener.incoming() {
795 let Ok(stream) = stream else { continue };
796 if open.fetch_add(1, Ordering::SeqCst) >= MAX_CONNECTIONS {
797 open.fetch_sub(1, Ordering::SeqCst);
798 continue;
799 }
800 let (api, held) = (api.clone(), Arc::clone(&open));
801 let spawned = std::thread::Builder::new().name("rws-admin-conn".to_string()).spawn(move || {
802 let address = |a: Option<std::net::SocketAddr>| match a {
803 Some(a) => Address { ip: a.ip().to_string(), port: a.port() as i32 },
804 None => Address { ip: String::new(), port: 0 },
805 };
806 let mut connection = ConnectionInfo::new(address(stream.peer_addr().ok()), address(stream.local_addr().ok()));
807 connection.request_size = 16 * 1024;
808 let _ = stream.set_read_timeout(Some(Duration::from_secs(30)));
809 let _ = stream.set_nodelay(true);
810 let _ = crate::server::Server::process(stream, connection, api);
811 held.fetch_sub(1, Ordering::SeqCst);
812 });
813 if spawned.is_err() {
814 open.fetch_sub(1, Ordering::SeqCst);
815 }
816 }
817}
818
819#[cfg(test)]
820mod tests;