vault-link/sync-server/src/server.rs

181 lines
5.7 KiB
Rust

use crate::consts::{HISTORY_HEADER, HISTORY_MISMATCH_HEADER};
pub mod auth;
mod device_id_header;
mod endpoints;
pub(crate) mod history;
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 history::history_middleware;
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("/", create_authenticated_routes(app_state.clone()))
.route("/", get(endpoints::index::index))
.route(
"/vaults/:vault_id/config",
get(endpoints::get_server_config::get_server_config),
)
.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::<HeaderValue>().expect("Failed to parse origin"))
.allow_headers([
http::header::CONTENT_TYPE,
http::header::AUTHORIZATION,
DEVICE_ID_HEADER_NAME.clone(),
HISTORY_HEADER,
])
.expose_headers([HISTORY_HEADER, HISTORY_MISMATCH_HEADER])
.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 create_authenticated_routes(app_state: AppState) -> Router<AppState> {
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_metadata::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.clone(),
history_middleware,
))
.layer(middleware::from_fn_with_state(app_state, auth_middleware))
}
async fn start_server(app: IntoMakeService<axum::Router>, 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 => {},
}
}