pub mod auth; mod device_id_header; mod endpoints; pub(crate) mod requests; pub(crate) mod responses; #[cfg(test)] mod tests; use anyhow::{Context as _, Result}; use auth::auth_middleware; use axum::{ Router, extract::{DefaultBodyLimit, Request}, http::{self, HeaderValue, Method}, middleware, routing::{IntoMakeService, get}, }; use device_id_header::DEVICE_ID_HEADER_NAME; use log::info; use tokio::signal; use tower_http::{ LatencyUnit, cors::CorsLayer, limit::RequestBodyLimitLayer, timeout::TimeoutLayer, trace::{ DefaultOnBodyChunk, DefaultOnEos, DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer, }, }; use tracing::{Level, info_span}; use crate::{ app_state::AppState, config::{Config, server_config::ServerConfig}, }; pub async fn create_server(config: Config) -> Result<()> { let app_state = AppState::try_new(config) .await .context("Failed to initialise app state")?; let server_config = app_state.config.server.clone(); let app = Router::new() .nest("/", get_authed_routes(app_state.clone())) .route("/", get(endpoints::index::index)) .route("/vaults/:vault_id/ping", get(endpoints::ping::ping)) .route( "/vaults/:vault_id/ws", get(endpoints::websocket::websocket_handler), ) .layer(DefaultBodyLimit::disable()) .layer(RequestBodyLimitLayer::new( app_state.config.server.max_body_size_mb * 1024 * 1024, )) .layer(TimeoutLayer::new(server_config.response_timeout)) .layer( CorsLayer::new() .allow_origin("*".parse::().expect("Failed to parse origin")) .allow_headers([ http::header::CONTENT_TYPE, http::header::AUTHORIZATION, DEVICE_ID_HEADER_NAME.clone(), http::header::HeaderName::from_static("x-vault-link-history"), ]) .expose_headers([ http::header::HeaderName::from_static("x-vault-link-history"), http::header::HeaderName::from_static("x-vault-link-history-mismatch"), ]) .allow_methods([Method::GET, Method::PUT]), ) .layer( TraceLayer::new_for_http() .make_span_with(|request: &Request<_>| { info_span!( "http", method = ?request.method(), uri = ?request.uri(), ) }) .on_request(DefaultOnRequest::new().level(Level::INFO)) .on_response( DefaultOnResponse::new() .level(Level::INFO) .latency_unit(LatencyUnit::Millis), ) .on_body_chunk(DefaultOnBodyChunk::new()) .on_eos(DefaultOnEos::new()) .on_failure(DefaultOnFailure::new().level(Level::ERROR)), ) .with_state(app_state) .fallback(endpoints::not_found::not_found) .method_not_allowed_fallback(endpoints::method_not_allowed::method_not_allowed) .into_make_service(); start_server(app, &server_config).await } fn get_authed_routes(app_state: AppState) -> Router { Router::new() .route( "/vaults/:vault_id/file-manifest", get(endpoints::get_file_manifest::get_file_manifest) .put(endpoints::put_file_manifest::put_file_manifest), ) .route( "/vaults/:vault_id/vault-snapshot", get(endpoints::vault_snapshot::vault_snapshot), ) .route( "/vaults/:vault_id/events-since", get(endpoints::events::events), ) .route( "/vaults/:vault_id/documents/:document_id", get(endpoints::fetch_latest_document_version::fetch_latest_document_version) .put(endpoints::put_file_content::put_file_content), ) .route( "/vaults/:vault_id/documents/:document_id/metadata", get(endpoints::fetch_latest_document_version::fetch_latest_document_metadata), ) .route( "/vaults/:vault_id/documents/:document_id/versions/:vault_update_id/content", get(endpoints::fetch_document_version_content::fetch_document_version_content), ) .layer(middleware::from_fn_with_state(app_state, auth_middleware)) } async fn start_server(app: IntoMakeService, config: &ServerConfig) -> Result<()> { let address = format!("{}:{}", config.host, config.port); let listener = tokio::net::TcpListener::bind(address.clone()) .await .with_context(|| format!("Failed to bind to address: {address}"))?; info!( "Listening on http://{}", listener .local_addr() .context("Failed to get local address")? ); axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .tcp_nodelay(true) .await .context("Failed to start server") } async fn shutdown_signal() { let ctrl_c = async { signal::ctrl_c() .await .expect("failed to install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { () = ctrl_c => {}, () = terminate => {}, } }