use std::net::SocketAddr;
use std::sync::Arc;
use crate::storage::repository::TrustRecordAdminRepository;
use axum::{Json, Router, routing::get};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tower_http::cors::CorsLayer;
use tracing::{debug, error, info, warn};
use crate::{
configs::{DidcommConfig, TrustRegistryConfig},
didcomm::listener::start_didcomm_listener,
health::RegistryHealth,
http::application_routes,
};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
#[cfg(feature = "standalone")]
fn setup_logging() {
use tracing_subscriber::EnvFilter;
let _ = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env()) .with_target(false)
.with_level(true)
.with_thread_ids(true)
.try_init();
}
pub struct ServerHandle {
http_addr: SocketAddr,
shutdown: CancellationToken,
http_task: JoinHandle<Result<(), BoxError>>,
didcomm_task: Option<JoinHandle<Result<(), BoxError>>>,
health: Arc<RegistryHealth>,
}
impl ServerHandle {
pub fn http_addr(&self) -> SocketAddr {
self.http_addr
}
pub fn base_url(&self) -> String {
format!("http://{}", self.http_addr)
}
pub fn shutdown_token(&self) -> CancellationToken {
self.shutdown.clone()
}
pub async fn shutdown(self) {
self.shutdown.cancel();
self.join().await;
}
pub fn health(&self) -> &Arc<RegistryHealth> {
&self.health
}
pub async fn join(self) {
let Self {
mut http_task,
didcomm_task,
health,
..
} = self;
let Some(didcomm_task) = didcomm_task else {
if let Err(e) = http_task.await {
error!("http task panicked: {e:?}");
}
return;
};
tokio::select! {
result = didcomm_task => {
log_task_exit("didcomm", &result);
health.mark_writes_unavailable(describe_task_exit(&result));
error!(
"DIDComm listener stopped; continuing to serve reads. \
Record mutations cannot be received until it recovers \
(/health reports status=degraded)."
);
if let Err(e) = http_task.await {
error!("http task panicked: {e:?}");
}
}
result = &mut http_task => log_task_exit("http", &result),
}
}
}
fn describe_task_exit(result: &Result<Result<(), BoxError>, tokio::task::JoinError>) -> String {
match result {
Ok(Ok(())) => "didcomm listener exited cleanly".to_string(),
Ok(Err(e)) => format!("didcomm listener failed: {e}"),
Err(e) => format!("didcomm listener panicked: {e}"),
}
}
fn log_task_exit(name: &str, result: &Result<Result<(), BoxError>, tokio::task::JoinError>) {
match result {
Ok(Ok(())) => info!("{name} task exited"),
Ok(Err(e)) => error!("{name} task failed: {e}"),
Err(e) => error!("{name} task panicked: {e:?}"),
}
}
fn build_cors_layer(allowed_origins: &[String]) -> CorsLayer {
if allowed_origins.is_empty() {
info!("CORS: No allowed origins configured, allowing all origins");
return CorsLayer::permissive();
}
if allowed_origins.len() == 1 && allowed_origins[0] == "*" {
info!("CORS: Wildcard configured, allowing all origins");
return CorsLayer::permissive();
}
info!("CORS: Configured allowed origins: {:?}", allowed_origins);
let origins: Vec<_> = allowed_origins
.iter()
.filter_map(|origin| origin.parse().ok())
.collect();
CorsLayer::new()
.allow_origin(origins)
.allow_methods(tower_http::cors::Any)
.allow_headers(tower_http::cors::Any)
}
fn build_router(parts: &crate::embed::RegistryParts) -> Router {
let cors = build_cors_layer(&parts.config.server_config.cors_allowed_origins);
let health = parts.health.clone();
let health_route = Router::new().route(
"/health",
get(move || {
let health = health.clone();
async move { Json(health.to_json()) }
}),
);
health_route
.merge(application_routes("", parts.shared_data()))
.layer(cors)
}
#[allow(clippy::too_many_arguments)]
async fn start_didcomm_server(
config: DidcommConfig,
repository: Arc<dyn TrustRecordAdminRepository>,
dispatcher: crate::capabilities::DispatcherHandle,
dedup: Arc<dyn crate::dedup::MessageIdStore>,
verifier: Arc<dyn trust_tasks_rs::DynProofVerifier>,
source: crate::didcomm::listener::DidCommSource,
shutdown: CancellationToken,
) -> Result<(), BoxError> {
let _ = start_didcomm_listener(
config, repository, dispatcher, dedup, verifier, source, shutdown,
)
.await?;
Ok(())
}
pub async fn serve(
config: Arc<TrustRegistryConfig>,
repository: Arc<dyn TrustRecordAdminRepository>,
shutdown: CancellationToken,
) -> Result<ServerHandle, BoxError> {
let registry = crate::TrustRegistry::builder(config)
.repository(repository)
.shutdown(shutdown)
.build()
.await?;
serve_registry(registry).await
}
pub(crate) async fn serve_registry(
registry: crate::TrustRegistry,
) -> Result<ServerHandle, BoxError> {
let parts = registry.into_parts();
let listener =
tokio::net::TcpListener::bind(&parts.config.server_config.listen_address).await?;
let http_addr = listener.local_addr()?;
let router = build_router(&parts);
info!("HTTP server is starting on {http_addr}...");
debug!("CONFIGS: {:?}", &parts.config);
let http_shutdown = parts.shutdown.clone();
let http_task = tokio::spawn(async move {
axum::serve(listener, router)
.with_graceful_shutdown(async move { http_shutdown.cancelled().await })
.await
.map_err(BoxError::from)
});
use crate::didcomm::listener::DidCommSource;
let didcomm_task = match (
parts.config.didcomm_config.is_enabled,
&parts.didcomm_source,
) {
(false, _) => {
warn!("DIDComm server is disabled.");
None
}
(true, DidCommSource::HostDriven) => {
info!(
"DIDComm is host-driven: the host owns the mediator socket and routes \
inbound documents through the registry itself."
);
None
}
(true, source) => Some(tokio::spawn(start_didcomm_server(
parts.config.didcomm_config.clone(),
parts.repository.clone(),
parts.capabilities.dispatcher(),
parts.dedup.clone(),
parts.verifier.clone(),
source.clone(),
parts.shutdown.clone(),
))),
};
Ok(ServerHandle {
http_addr,
shutdown: parts.shutdown,
http_task,
didcomm_task,
health: parts.health,
})
}
#[cfg(feature = "standalone")]
pub async fn start() {
use crate::configs::Configs;
use crate::storage::factory::TrustStorageRepoFactory;
dotenvy::dotenv().ok();
setup_logging();
let config = match TrustRegistryConfig::load().await {
Ok(c) => Arc::new(c),
Err(e) => {
error!(
"Failed to load configs. End of work. Original error is: {}",
e
);
panic!("Failed to load configs");
}
};
let repository_factory = TrustStorageRepoFactory::new(Arc::clone(&config));
let repository = match repository_factory.create().await {
Ok(r) => r,
Err(e) => {
error!("Failed to initialize trust record repository: {e}");
panic!("Failed to initialize trust record repository: {e}");
}
};
let shutdown = CancellationToken::new();
let handle = match serve(config, repository, shutdown).await {
Ok(handle) => handle,
Err(e) => {
error!("Failed to start Trust Registry server: {e}");
panic!("Failed to start Trust Registry server: {e}");
}
};
handle.join().await;
std::process::exit(1);
}