1pub mod error;
10mod message_token;
11pub mod probes;
12pub mod request_source;
13pub mod ruma_requests;
14mod tests;
15use std::{future, marker::PhantomData};
16
17pub use arminius_lib_config::ArminiusConfig;
18use arminius_lib_db::DefaultDb;
19use arminius_lib_nats::{
20 NatsConfiguration, NatsConfigurationError, NatsCredentials, SharedNatsClient,
21};
22use axum::{
23 Router,
24 body::Body,
25 extract::{FromRef, State},
26 http::{HeaderName, Method, StatusCode, Uri, header},
27 middleware::Next,
28};
29use axum_tracing_opentelemetry::middleware::OtelAxumLayer;
30use color_eyre::eyre::Context as _;
31use error::{InternalServerError, MatrixApiError};
32pub use message_token::{MessageTokenParseError, MessagesToken};
33use ruma::api::error::ErrorKind;
34use tokio::signal::unix::SignalKind;
35use tokio_util::sync::CancellationToken;
36use tower_http::cors::{Any, CorsLayer};
37
38use crate::probes::probe_router;
39
40#[derive(Clone)]
42pub struct ServerState {
43 pub nats_connection: SharedNatsClient,
45 pub database: DefaultDb,
48 pub config: ArminiusConfig,
50}
51
52impl std::fmt::Debug for ServerState {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct("ServerState")
55 .field("nats_connection", &self.nats_connection)
56 .field("database", &"(redacted)")
57 .field("config", &self.config)
58 .finish()
59 }
60}
61
62impl ServerState {
63 #[must_use]
65 #[cfg(feature = "test_utils")]
66 #[expect(
67 clippy::expect_used,
68 reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
69 )]
70 pub async fn create_for_test(
71 pg_pool_options: sqlx::pool::PoolOptions<sqlx::Postgres>,
72 pg_connect_options: sqlx::postgres::PgConnectOptions,
73 ) -> ServerState {
74 use std::ops::Deref as _;
75
76 use sqlx::ConnectOptions as _;
77
78 let pool = pg_pool_options
79 .connect_with(pg_connect_options)
80 .await
81 .expect("Could not connect to database for tests");
82
83 let database = DefaultDb::new(pool.clone());
84 database.run_migrations().await.expect("Failed to run migrations");
85
86 let connect_options =
90 pool.connect_options().deref().clone().log_statements(tracing::log::LevelFilter::Info);
91 pool.set_connect_options(connect_options);
92
93 let nats_configuration = NatsConfiguration::for_test().await;
94 let nats_connection = nats_configuration
95 .connect(
96 "tests".to_owned(),
97 "0.0.0".to_owned(),
98 "NATS configuration just for tests".to_owned(),
99 )
100 .await
101 .expect("Could not connect to nats");
102 nats_connection.set_up_streams().await.expect("Could not set up streams");
103 nats_connection
104 .set_maintenance_mode(false)
105 .await
106 .expect("Could not disable maintenance mode");
107 let config = ArminiusConfig::for_test_using_ipv6();
108 ServerState { nats_connection, database, config }
109 }
110}
111
112#[macro_export]
115macro_rules! new_server_state {
116 () => {
117 arminius_lib_component::ServerState::new(
118 env!("CARGO_BIN_NAME").to_owned(),
119 env!("CARGO_PKG_VERSION").to_owned(),
120 env!("CARGO_PKG_DESCRIPTION").to_owned(),
121 )
122 };
123}
124
125impl ServerState {
126 #[must_use]
128 pub fn create(
129 nats_connection: SharedNatsClient,
130 database: DefaultDb,
131 config: ArminiusConfig,
132 ) -> ServerState {
133 Self { nats_connection, database, config }
134 }
135 pub async fn new(
138 service_name: String,
139 version: String,
140 description: String,
141 ) -> Result<Self, ComponentInitializationError> {
142 let nats_credentials =
143 NatsCredentials::from_env().map_err(NatsConfigurationError::Credential)?;
144 let database = DefaultDb::from_env().await?;
145 Ok(Self {
146 nats_connection: NatsConfiguration::with_credentials(nats_credentials)?
147 .connect(service_name, version, description)
148 .await?,
149 database,
150 config: ArminiusConfig::from_env()?,
151 })
152 }
153}
154
155#[derive(thiserror::Error, Debug)]
157pub enum ComponentInitializationError {
158 #[error(transparent)]
160 DatabaseError(#[from] arminius_lib_db::DatabaseError),
161 #[error(transparent)]
163 NatsConfiguration(#[from] NatsConfigurationError),
164 #[error(transparent)]
166 ArminiusConfigurationError(#[from] arminius_lib_config::ArminiusConfigurationError),
167}
168
169impl FromRef<ServerState> for SharedNatsClient {
170 fn from_ref(component_state: &ServerState) -> SharedNatsClient {
171 component_state.nats_connection.clone()
172 }
173}
174
175impl FromRef<ServerState> for DefaultDb {
176 fn from_ref(component_state: &ServerState) -> DefaultDb {
177 component_state.database.clone()
178 }
179}
180
181impl FromRef<ServerState> for ArminiusConfig {
182 fn from_ref(component_state: &ServerState) -> ArminiusConfig {
183 component_state.config.clone()
184 }
185}
186
187#[derive(thiserror::Error, Debug, MatrixApiError)]
189#[error("Endpoint not found")]
190#[matrix_api_error(status_code = StatusCode::NOT_FOUND, error_kind = ErrorKind::Unrecognized, component_crate_root = crate)]
191pub struct EndpointNotFound;
192
193async fn unknown_endpoint(method: Method, uri: Uri) -> EndpointNotFound {
195 tracing::info!(%uri, %method, "Got unknown endpoint");
196 EndpointNotFound
197}
198
199#[derive(thiserror::Error, Debug, MatrixApiError)]
201#[error("Method not allowed")]
202#[matrix_api_error(status_code = StatusCode::METHOD_NOT_ALLOWED, error_kind = ErrorKind::Unrecognized, component_crate_root = crate)]
203pub struct MethodNotAllowed;
204
205async fn method_not_allowed_endpoint(method: Method, uri: Uri) -> MethodNotAllowed {
207 tracing::info!(%uri, %method, "User used disallowed method");
208 MethodNotAllowed
209}
210
211#[derive(MatrixApiError, thiserror::Error, Debug)]
213#[matrix_api_error(component_crate_root = crate)]
214pub enum MaintenanceModeLayerError {
215 #[error("Service in maintenance mode")]
217 #[matrix_api_error(status_code = StatusCode::SERVICE_UNAVAILABLE, error_kind = ErrorKind::Unknown)]
218 MaintenanceMode,
219
220 #[error(transparent)]
222 InternalServerError(#[from] InternalServerError),
223}
224
225async fn maintenance_mode_layer(
228 state: State<ServerState>,
229 req: axum::http::Request<Body>,
230 next: Next,
231) -> Result<axum::http::Response<Body>, MaintenanceModeLayerError> {
232 let in_maintenance =
233 state.nats_connection.is_in_maintenance_mode().await.map_err(|_| InternalServerError)?;
234
235 if in_maintenance {
236 Err(MaintenanceModeLayerError::MaintenanceMode)
237 } else {
238 Ok(next.run(req).await)
239 }
240}
241
242#[derive(Debug)]
245#[must_use = "Initialized server but never used"]
246pub struct Server<COMPONENT: Component> {
247 pub state: ServerState,
249 pub router: Router,
252 pub listener: tokio::net::TcpListener,
254 components: PhantomData<COMPONENT>,
257}
258
259impl<COMPONENT: Component> Server<COMPONENT> {
260 pub async fn initialize(state: ServerState) -> color_eyre::eyre::Result<Self> {
262 COMPONENT::initialize(&state).await?;
264
265 let traced_router = COMPONENT::router()
266 .with_state(state.clone())
267 .fallback(unknown_endpoint)
268 .method_not_allowed_fallback(method_not_allowed_endpoint)
269 .layer(OtelAxumLayer::default());
271
272 let router = Router::new()
273 .merge(traced_router)
274 .layer(axum::middleware::from_fn_with_state(state.clone(), maintenance_mode_layer))
275 .layer(
276 CorsLayer::new()
278 .allow_headers([
279 header::AUTHORIZATION,
280 header::CONTENT_TYPE,
281 HeaderName::from_static("x-requested-with"),
282 ])
283 .allow_methods([
284 Method::GET,
285 Method::POST,
286 Method::PUT,
287 Method::DELETE,
288 Method::OPTIONS,
289 ])
290 .allow_origin(Any),
291 )
292 .merge(probe_router(state.clone()));
294
295 let listener = tokio::net::TcpListener::bind(state.config.addr)
296 .await
297 .context("Failed binding listener")?;
298
299 Ok(Self { state, router, listener, components: PhantomData })
300 }
301
302 pub async fn run_axum(
305 self,
306 cancellation_token: CancellationToken,
307 ) -> color_eyre::eyre::Result<()> {
308 tracing::info!(addr = %self.state.config.addr, "Starting listener");
309 axum::serve(self.listener, self.router)
310 .with_graceful_shutdown(cancellation_token.cancelled_owned())
311 .await
312 .context("Failed serving webserver")?;
313 Ok(())
314 }
315
316 pub async fn run(self) -> color_eyre::eyre::Result<()> {
319 let cancellation_token = CancellationToken::new();
320 Box::pin(self.run_with_cancellation_token(cancellation_token)).await
322 }
323
324 pub async fn run_with_cancellation_token(
327 self,
328 cancellation_token: CancellationToken,
329 ) -> color_eyre::eyre::Result<()> {
330 let tc = cancellation_token.clone();
331 tokio::spawn(async move {
332 await_shutdown_signal().await;
333 tc.cancel();
334 });
335
336 COMPONENT::initialize(&self.state).await?;
338
339 tokio::try_join!(
341 COMPONENT::run(self.state.clone(), cancellation_token.clone()),
342 self.run_axum(cancellation_token),
343 )?;
344
345 Ok(())
346 }
347}
348
349pub trait Component {
351 fn router() -> Router<ServerState> {
353 Router::new()
354 }
355 #[must_use]
357 fn initialize(_: &ServerState) -> impl Future<Output = color_eyre::eyre::Result<()>> + Send {
358 future::ready(Ok(()))
359 }
360
361 #[must_use]
364 fn run(
365 _state: ServerState,
366 _cancellation_token: CancellationToken,
367 ) -> impl Future<Output = color_eyre::eyre::Result<()>> + Send {
368 future::ready(Ok(()))
369 }
370}
371
372#[derive(Debug)]
376pub struct ComposedComponent<T: Component, U: Component> {
377 _1: PhantomData<T>,
379 _2: PhantomData<U>,
381}
382
383impl<T: Component, U: Component> Component for ComposedComponent<T, U> {
384 fn router() -> Router<ServerState> {
385 T::router().merge(U::router())
386 }
387
388 async fn initialize(database: &ServerState) -> color_eyre::eyre::Result<()> {
389 T::initialize(database).await?;
391 U::initialize(database).await?;
393 Ok(())
394 }
395
396 async fn run(
397 state: ServerState,
398 cancellation_token: CancellationToken,
399 ) -> color_eyre::eyre::Result<()> {
400 tokio::try_join!(
402 T::run(state.clone(), cancellation_token.clone()),
403 U::run(state.clone(), cancellation_token.clone())
404 )?;
405 Ok(())
406 }
407}
408
409#[derive(Debug)]
412pub struct EmptyComponent;
413
414impl Component for EmptyComponent {}
415
416#[macro_export]
424macro_rules! compose_components {
425 ($c1:ident,$c2:ident) => {
426 ::arminius_lib_component::ComposedComponent<$c1, $c2>
427 };
428 ($c1:ident,$c2:ident,$c3:ident,$c4:ident,$c5:ident,$c6:ident,$c7:ident,$c8:ident,$c9:ident,$($c10:ident),+) => {
429 ::arminius_lib_component::ComposedComponent<::arminius_lib_component::ComposedComponent<::arminius_lib_component::ComposedComponent<::arminius_lib_component::ComposedComponent<$c1, $c2>, ::arminius_lib_component::ComposedComponent<$c3, $c4>>, ::arminius_lib_component::ComposedComponent<::arminius_lib_component::ComposedComponent<$c5, $c6>, ::arminius_lib_component::ComposedComponent<$c7, $c8>>>, compose_components!($c9,$($c10),+)>
430 };
431 ($c1:ident,$c2:ident,$c3:ident,$c4:ident,$c5:ident,$($c6:ident),+) => {
432 ::arminius_lib_component::ComposedComponent<::arminius_lib_component::ComposedComponent<::arminius_lib_component::ComposedComponent<$c1, $c2>, ::arminius_lib_component::ComposedComponent<$c3, $c4>>, compose_components!($c5,$($c6),+)>
433 };
434 ($c1:ident,$c2:ident,$c3:ident,$($c4:ident),+) => {
435 ::arminius_lib_component::ComposedComponent<::arminius_lib_component::ComposedComponent<$c1, $c2>, compose_components!($c3,$($c4),+)>
436 };
437 ($c1:ident,$c2:ident,$($c3:ident),+) => {
438 ::arminius_lib_component::ComposedComponent<$c1, compose_components!($c2,$($c3),+)>
439 };
440}
441
442pub async fn await_shutdown_signal() {
445 let mut sigterm_signal = match tokio::signal::unix::signal(SignalKind::terminate()) {
446 Ok(signal) => signal,
447 Err(error) => {
448 tracing::error!(%error, "Could not create listener for sigterm");
449 return;
450 }
451 };
452 tokio::select! {
453 result = tokio::signal::ctrl_c() => {
454 match result {
455 Ok(_) => (),
456 Err(error) => {
457 tracing::error!(%error, "Error registering ctrl+c listener");
458 }
459 }
460 },
461 result = sigterm_signal.recv() => {
462 match result {
463 Some(()) => (),
464 None => {
465 tracing::error!("Error registering SIGTERM listener");
466 }
467 }
468 }
469 }
470}