manta_server/server/
mod.rs

1//! HTTPS server setup: shared state, request-logging middleware, and the
2//! TLS server entry point.
3
4pub mod api_doc;
5pub mod auth_middleware;
6pub mod common;
7pub mod handlers;
8pub mod routes;
9
10use std::collections::HashMap;
11use std::net::SocketAddr;
12use std::sync::Arc;
13
14use axum_server::tls_rustls::RustlsConfig;
15use manta_backend_dispatcher::error::Error;
16use std::time::Duration;
17
18use crate::manta_backend_dispatcher::StaticBackendDispatcher;
19use crate::server::common::app_context::InfraContext;
20use manta_shared::common::kafka::Kafka;
21
22/// All per-site connection data the server needs to talk to backend APIs.
23///
24/// Owned by `ServerState` inside a `HashMap` keyed by site name.
25pub struct SiteBackend {
26  /// Dispatches API calls to the configured CSM or OpenCHAMI backend.
27  pub backend: StaticBackendDispatcher,
28  /// Base URL for the CSM/OpenCHAMI API (e.g. `https://api.cluster/apis`).
29  pub shasta_base_url: String,
30  /// PEM-encoded root CA certificate for the backend; empty vec skips verification.
31  pub shasta_root_cert: Vec<u8>,
32  /// SOCKS5 proxy URL; `None` means direct connections.
33  pub socks5_proxy: Option<String>,
34  /// HashiCorp Vault base URL; `None` means features requiring vault return 501.
35  pub vault_base_url: Option<String>,
36  /// Gitea VCS base URL derived from the site base URL.
37  pub gitea_base_url: String,
38  /// Kubernetes API URL; `None` means console and log-streaming endpoints return 501.
39  pub k8s_api_url: Option<String>,
40}
41
42/// Shared state for all HTTP handlers.
43///
44/// Holds one `SiteBackend` per configured site so that the server can serve
45/// multiple clusters.  Each request supplies the target site via the
46/// `X-Manta-Site` header; handlers call [`ServerState::infra_context`] to
47/// retrieve the per-site data.
48pub struct ServerState {
49  /// Per-site connection data, keyed by site name.
50  pub sites: HashMap<String, SiteBackend>,
51  /// How long a WebSocket console session may be idle before the server
52  /// closes it.  Protects against leaked Kubernetes pod attachments.
53  pub console_inactivity_timeout: Duration,
54  /// Kafka producer for security/audit events (currently used only by
55  /// `/api/v1/auth/*`). `None` disables audit emission.
56  pub auditor: Option<Kafka>,
57  /// Per-source-IP rate limit on `/api/v1/auth/*` (requests/minute).
58  /// `None` disables in-process rate limiting.
59  pub auth_rate_limit_per_minute: Option<u32>,
60}
61
62impl ServerState {
63  /// Build a borrowed `InfraContext` for the named site.
64  ///
65  /// Returns `Err(Error::NotFound)` when `site_name` is not in the map.
66  /// Called per-request so the service layer can work with its existing
67  /// `&InfraContext<'_>` API.
68  pub fn infra_context<'a>(
69    &'a self,
70    site_name: &'a str,
71  ) -> Result<InfraContext<'a>, Error> {
72    let site = self.sites.get(site_name).ok_or_else(|| {
73      Error::NotFound(format!("site '{site_name}' not found"))
74    })?;
75    Ok(InfraContext {
76      backend: &site.backend,
77      site_name,
78      shasta_base_url: &site.shasta_base_url,
79      shasta_root_cert: &site.shasta_root_cert,
80      socks5_proxy: site.socks5_proxy.as_deref(),
81      vault_base_url: site.vault_base_url.as_deref(),
82      gitea_base_url: &site.gitea_base_url,
83      k8s_api_url: site.k8s_api_url.as_deref(),
84    })
85  }
86}
87
88async fn log_requests(
89  request: axum::extract::Request,
90  next: axum::middleware::Next,
91) -> axum::response::Response {
92  let method = request.method().clone();
93  let uri = request.uri().clone();
94  let response = next.run(request).await;
95  tracing::info!("{} {} → {}", method, uri, response.status());
96  response
97}
98
99/// Start the HTTP or HTTPS server.
100///
101/// When `cert_path` and `key_path` are both `Some`, the server listens with
102/// TLS (`https://`).  When either is `None`, it listens as plain HTTP.
103pub async fn start_server(
104  state: Arc<ServerState>,
105  listen_addr: &str,
106  port: u16,
107  cert_path: Option<&str>,
108  key_path: Option<&str>,
109) -> Result<(), Error> {
110  let app = routes::build_router(state)
111    .layer(tower_http::timeout::TimeoutLayer::with_status_code(
112      axum::http::StatusCode::REQUEST_TIMEOUT,
113      Duration::from_secs(60),
114    ))
115    .layer(axum::middleware::from_fn(log_requests));
116
117  let addr: SocketAddr = format!("{listen_addr}:{port}")
118    .parse()
119    .map_err(|e| Error::BadRequest(format!("Invalid listen address: {e}")))?;
120
121  match (cert_path, key_path) {
122    (Some(cert), Some(key)) => {
123      let tls_config = RustlsConfig::from_pem_file(cert, key).await?;
124      let handle = axum_server::Handle::new();
125      let ready_handle = handle.clone();
126      tokio::spawn(async move {
127        ready_handle.listening().await;
128        tracing::info!(
129          "HTTPS server ready, accepting requests on https://{}",
130          addr
131        );
132        eprintln!("HTTPS server ready, accepting requests on https://{addr}");
133      });
134      axum_server::bind_rustls(addr, tls_config)
135        .handle(handle)
136        .serve(app.into_make_service_with_connect_info::<SocketAddr>())
137        .await?;
138    }
139    (None, None) => {
140      let handle = axum_server::Handle::new();
141      let ready_handle = handle.clone();
142      tokio::spawn(async move {
143        ready_handle.listening().await;
144        tracing::info!(
145          "HTTP server ready, accepting requests on http://{}",
146          addr
147        );
148        eprintln!("HTTP server ready, accepting requests on http://{addr}");
149      });
150      axum_server::bind(addr)
151        .handle(handle)
152        .serve(app.into_make_service_with_connect_info::<SocketAddr>())
153        .await?;
154    }
155    _ => {
156      return Err(Error::BadRequest(
157        "--cert and --key must be provided together".to_string(),
158      ));
159    }
160  }
161
162  Ok(())
163}