1use std::{path::Path, time::Duration};
2
3use axum::{
4 BoxError, Extension, Router,
5 error_handling::HandleErrorLayer,
6 http::{StatusCode, header::AUTHORIZATION},
7 response::IntoResponse,
8 routing::{get, post},
9};
10use tokio::net::TcpListener;
11#[cfg(unix)]
12use tokio::net::UnixListener;
13use tokio_metrics::TaskMonitor;
14use tower::{ServiceBuilder, limit::ConcurrencyLimitLayer, timeout::TimeoutLayer};
15use tower_http::{
16 cors::CorsLayer, limit::RequestBodyLimitLayer, sensitive_headers::SetSensitiveHeadersLayer,
17};
18use tracing::{debug, info};
19
20use crate::{
21 Result, config,
22 dependency::Dependency,
23 gql::{self, SyndSchema},
24 serve::layer::{authenticate, request_metrics::RequestMetricsLayer, trace},
25 session::{DaemonSessionConfig, DaemonSessionSweeper, SessionIdleShutdown},
26 shutdown::Shutdown,
27};
28
29pub mod auth;
30mod probe;
31mod session;
32
33pub mod layer;
34
35pub async fn rustls_config_from_pem_files(
36 certificate: impl AsRef<Path>,
37 private_key: impl AsRef<Path>,
38) -> Result<axum_server::tls_rustls::RustlsConfig> {
39 axum_server::tls_rustls::RustlsConfig::from_pem_file(certificate, private_key)
40 .await
41 .map_err(|source| crate::Error::TlsConfig { source })
42}
43
44pub struct ServeOptions {
45 pub timeout: Duration,
46 pub body_limit_bytes: usize,
47 pub concurrency_limit: usize,
48 pub daemon_sessions: DaemonSessionConfig,
49}
50
51impl ServeOptions {
52 #[must_use]
53 pub fn with_daemon_sessions(mut self, daemon_sessions: DaemonSessionConfig) -> Self {
54 self.daemon_sessions = daemon_sessions;
55 self
56 }
57}
58
59impl Default for ServeOptions {
60 fn default() -> Self {
61 Self {
62 timeout: Duration::from_secs(30),
63 body_limit_bytes: config::serve::DEFAULT_REQUEST_BODY_LIMIT_BYTES,
64 concurrency_limit: config::serve::DEFAULT_REQUEST_CONCURRENCY_LIMIT,
65 daemon_sessions: DaemonSessionConfig::default(),
66 }
67 }
68}
69
70#[derive(Clone)]
71pub(crate) struct Context {
72 pub gql_monitor: TaskMonitor,
73 pub schema: SyndSchema,
74}
75
76pub async fn serve(listener: TcpListener, dep: Dependency, shutdown: Shutdown) -> Result<()> {
78 let ApiService { router, tls_config } = build_service(dep, &shutdown);
79
80 info!("Serving...");
81
82 let listener = listener.into_std()?;
83 let handle = shutdown.into_handle();
84
85 if let Some(tls_config) = tls_config {
86 axum_server::from_tcp_rustls(listener, tls_config)?
87 .handle(handle)
88 .serve(router.into_make_service())
89 .await?;
90 } else {
91 axum_server::from_tcp(listener)?
92 .handle(handle)
93 .serve(router.into_make_service())
94 .await?;
95 }
96
97 info!("Shutdown complete");
98
99 Ok(())
100}
101
102#[cfg(unix)]
103pub async fn serve_unix(listener: UnixListener, dep: Dependency, shutdown: Shutdown) -> Result<()> {
104 let daemon_sessions = dep.serve_options.daemon_sessions;
105 let sessions = dep
106 .sessions
107 .with_lease_policy(daemon_sessions.lease_policy())
108 .with_idle_shutdown(SessionIdleShutdown::new(
109 daemon_sessions.idle_shutdown_grace(),
110 shutdown.clone(),
111 ));
112 let ApiService { router, .. } = build_service(dep, &shutdown);
113 let shutdown_requested = shutdown.cancellation_token();
114 let _session_sweeper =
115 DaemonSessionSweeper::new(sessions.clone(), shutdown_requested.clone()).spawn();
116 let router = router
117 .route("/session/open", post(session::open))
118 .route("/session/renew", post(session::renew))
119 .route("/session/close", post(session::close))
120 .route(synd_protocol::daemon::STATUS_PATH, get(control::status))
121 .route("/daemon/shutdown", post(control::shutdown))
122 .layer(Extension(sessions))
123 .layer(Extension(shutdown));
124
125 debug!("Serving on Unix socket");
126
127 axum::serve(listener, router)
128 .with_graceful_shutdown(async move {
129 shutdown_requested.cancelled().await;
130 })
131 .await?;
132
133 debug!("Unix socket server shutdown complete");
134
135 Ok(())
136}
137
138struct ApiService {
139 router: Router,
140 tls_config: Option<axum_server::tls_rustls::RustlsConfig>,
141}
142
143fn build_service(dep: Dependency, shutdown: &Shutdown) -> ApiService {
144 let Dependency {
145 authenticator,
146 registry,
147 tls_config,
148 serve_options:
149 ServeOptions {
150 timeout: request_timeout,
151 body_limit_bytes: request_body_limit_bytes,
152 concurrency_limit,
153 daemon_sessions: _,
154 },
155 monitors,
156 sessions: _,
157 } = dep;
158
159 let cx = Context {
160 gql_monitor: monitors.graphql_task_monitor(),
161 schema: gql::schema_builder().data(registry).finish(),
162 };
163
164 tokio::spawn(monitors.emit_metrics(
165 config::metrics::MONITOR_INTERVAL,
166 shutdown.cancellation_token(),
167 ));
168
169 let router = Router::new()
170 .route("/graphql", post(gql::handler::graphql))
171 .route("/graphql/ws", get(gql::handler::graphql_ws))
172 .layer(Extension(cx))
173 .layer(authenticate::AuthenticateLayer::new(authenticator))
174 .route("/graphql", get(gql::handler::graphiql))
175 .layer(
176 ServiceBuilder::new()
177 .layer(SetSensitiveHeadersLayer::new(std::iter::once(
178 AUTHORIZATION,
179 )))
180 .layer(trace::layer())
181 .layer(HandleErrorLayer::new(handle_middleware_error))
182 .layer(TimeoutLayer::new(request_timeout))
183 .layer(ConcurrencyLimitLayer::new(concurrency_limit))
184 .layer(RequestBodyLimitLayer::new(request_body_limit_bytes))
185 .layer(CorsLayer::new()),
186 )
187 .route(config::serve::HEALTH_CHECK_PATH, get(probe::healthcheck))
188 .layer(RequestMetricsLayer::new())
189 .fallback(not_found);
190
191 ApiService { router, tls_config }
192}
193
194mod control {
195 use axum::{Extension, Json, http::StatusCode};
196 use synd_protocol::daemon::DaemonStatusResponse;
197
198 use crate::session::DaemonSessions;
199 use crate::shutdown::{Shutdown, ShutdownReason};
200
201 pub(super) async fn status(
202 Extension(sessions): Extension<DaemonSessions>,
203 ) -> Json<DaemonStatusResponse> {
204 Json(DaemonStatusResponse::new(sessions.status()))
205 }
206
207 pub(super) async fn shutdown(Extension(shutdown): Extension<Shutdown>) -> StatusCode {
208 shutdown.shutdown_with_reason(ShutdownReason::ApiRequest);
209 StatusCode::ACCEPTED
210 }
211}
212
213async fn handle_middleware_error(err: BoxError) -> (StatusCode, String) {
214 if err.is::<tower::timeout::error::Elapsed>() {
215 (
216 StatusCode::REQUEST_TIMEOUT,
217 "Request took too long".to_string(),
218 )
219 } else {
220 (
221 StatusCode::INTERNAL_SERVER_ERROR,
222 format!("Unhandled internal error: {err}"),
223 )
224 }
225}
226
227async fn not_found() -> impl IntoResponse {
228 StatusCode::NOT_FOUND
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[tokio::test]
236 async fn error_mapping() {
237 assert_eq!(
238 handle_middleware_error(Box::new(tower::timeout::error::Elapsed::new()))
239 .await
240 .0,
241 StatusCode::REQUEST_TIMEOUT
242 );
243 assert_eq!(
244 handle_middleware_error(Box::new(std::io::Error::from(
245 std::io::ErrorKind::OutOfMemory
246 )))
247 .await
248 .0,
249 StatusCode::INTERNAL_SERVER_ERROR,
250 );
251
252 assert_eq!(
253 not_found().await.into_response().status(),
254 StatusCode::NOT_FOUND
255 );
256 }
257}