use std::net::SocketAddr;
use axum::Router;
use tokio::net::TcpListener;
use tracing::{debug, error, info};
use crate::config::ServerConfig;
pub async fn run(
config: &ServerConfig,
app: Router,
shutdown: impl std::future::Future<Output = ()> + Send + 'static,
) -> Result<(), std::io::Error> {
let ip: std::net::IpAddr = config.bind.parse().map_err(|e| {
error!(bind = %config.bind, port = %config.port, error = %e, "invalid bind address");
std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("invalid bind address: {e}"))
})?;
let addr = SocketAddr::new(ip, config.port);
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Status("Starting Issuerd...")]);
debug!(bind = %config.bind, port = %config.port, tls = config.tls.is_some(), "server configuration");
if let Some(ref tls) = config.tls {
info!(addr = %addr, "starting TLS server");
let rustls_config =
crate::tls::load_tls_config(&tls.cert_path, &tls.key_path).map_err(|e| {
error!(cert_path = %tls.cert_path, error = %e, "failed to load TLS configuration");
std::io::Error::new(std::io::ErrorKind::InvalidInput, e.to_string())
})?;
let axum_rustls =
axum_server::tls_rustls::RustlsConfig::from_config(std::sync::Arc::new(rustls_config));
let handle = axum_server::Handle::new();
let server = axum_server::bind_rustls(addr, axum_rustls)
.handle(handle.clone())
.serve(app.into_make_service_with_connect_info::<SocketAddr>());
let mut server_task = tokio::spawn(server);
tokio::select! {
result = &mut server_task => {
return match result {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(e),
Err(e) => Err(std::io::Error::other(format!("TLS server task failed: {e}"))),
};
}
_ = shutdown => {}
}
handle.graceful_shutdown(Some(std::time::Duration::from_secs(30)));
match server_task.await {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(e),
Err(e) => {
return Err(std::io::Error::other(format!("TLS server task failed: {e}")));
}
}
} else {
info!(addr = %addr, "starting server");
let listener = TcpListener::bind(&addr).await?;
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
.with_graceful_shutdown(shutdown)
.await?;
}
#[cfg(target_os = "linux")]
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Stopping]);
info!("Server shutdown complete");
Ok(())
}
pub async fn shutdown_signal() {
#[cfg(coverage)]
{
return;
}
#[cfg(not(coverage))]
{
let ctrl_c = async {
tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {
info!("Received Ctrl+C, initiating graceful shutdown");
}
_ = terminate => {
info!("Received SIGTERM, initiating graceful shutdown");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::ServerConfig;
#[tokio::test]
async fn run_with_invalid_bind_address_fails() {
let config = ServerConfig {
bind: "not-an-address".to_string(),
..Default::default()
};
let result = run(&config, axum::Router::new(), std::future::pending()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn run_with_valid_bind_address_binds() {
let config = ServerConfig {
bind: "127.0.0.1".to_string(),
port: 0,
..Default::default()
};
let result = tokio::time::timeout(
std::time::Duration::from_millis(200),
run(&config, axum::Router::new(), std::future::pending()),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn run_with_valid_tls_binds() {
let _ = rustls::crypto::ring::default_provider().install_default();
let tmp_dir = std::env::temp_dir()
.join(format!("issuerd_server_tls_bootstrap_{}", issuerd_core::utils::generate_id()));
let _ = std::fs::create_dir_all(&tmp_dir);
let cert_path = tmp_dir.join("cert.pem");
let key_path = tmp_dir.join("key.pem");
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()])
.expect("generate self-signed cert");
std::fs::write(&cert_path, cert.cert.pem()).expect("write cert pem");
std::fs::write(&key_path, cert.key_pair.serialize_pem()).expect("write key pem");
let config = ServerConfig {
bind: "127.0.0.1".to_string(),
port: 0,
tls: Some(crate::config::TlsConfig {
cert_path: cert_path.to_str().unwrap().to_string(),
key_path: key_path.to_str().unwrap().to_string(),
}),
..Default::default()
};
let result = tokio::time::timeout(
std::time::Duration::from_millis(200),
run(&config, axum::Router::new(), std::future::pending()),
)
.await;
assert!(result.is_err());
let _ = std::fs::remove_dir_all(&tmp_dir);
}
#[tokio::test]
async fn run_with_invalid_tls_config_fails() {
let _ = rustls::crypto::ring::default_provider().install_default();
let config = ServerConfig {
bind: "127.0.0.1".to_string(),
port: 0,
tls: Some(crate::config::TlsConfig {
cert_path: "/nonexistent/cert.pem".to_string(),
key_path: "/nonexistent/key.pem".to_string(),
}),
..Default::default()
};
let result = run(&config, axum::Router::new(), std::future::pending()).await;
assert!(result.is_err());
}
#[tokio::test]
async fn run_shutdown_completes_gracefully() {
let config = ServerConfig {
bind: "127.0.0.1".to_string(),
port: 0,
..Default::default()
};
let result = run(&config, axum::Router::new(), std::future::ready(())).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn run_tls_shutdown_completes_gracefully() {
let _ = rustls::crypto::ring::default_provider().install_default();
let tmp_dir = std::env::temp_dir()
.join(format!("issuerd_server_tls_shutdown_{}", issuerd_core::utils::generate_id()));
let _ = std::fs::create_dir_all(&tmp_dir);
let cert_path = tmp_dir.join("cert.pem");
let key_path = tmp_dir.join("key.pem");
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()])
.expect("generate self-signed cert");
std::fs::write(&cert_path, cert.cert.pem()).expect("write cert pem");
std::fs::write(&key_path, cert.key_pair.serialize_pem()).expect("write key pem");
let config = ServerConfig {
bind: "127.0.0.1".to_string(),
port: 0,
tls: Some(crate::config::TlsConfig {
cert_path: cert_path.to_str().unwrap().to_string(),
key_path: key_path.to_str().unwrap().to_string(),
}),
..Default::default()
};
let result = run(&config, axum::Router::new(), std::future::ready(())).await;
assert!(result.is_ok());
let _ = std::fs::remove_dir_all(&tmp_dir);
}
#[tokio::test]
async fn shutdown_signal_returns_immediately() {
let result =
tokio::time::timeout(std::time::Duration::from_millis(100), shutdown_signal()).await;
#[cfg(coverage)]
assert!(result.is_ok(), "under coverage shutdown_signal returns immediately");
#[cfg(not(coverage))]
assert!(result.is_err(), "without coverage shutdown_signal waits for a signal");
}
#[tokio::test]
async fn run_with_port_already_in_use_fails() {
let _guard = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = _guard.local_addr().unwrap();
let config = ServerConfig {
bind: "127.0.0.1".to_string(),
port: addr.port(),
..Default::default()
};
let result = run(&config, axum::Router::new(), std::future::ready(())).await;
assert!(result.is_err());
}
#[tokio::test]
async fn run_tls_with_port_already_in_use_fails() {
let _ = rustls::crypto::ring::default_provider().install_default();
let tmp_dir = std::env::temp_dir().join(format!(
"issuerd_server_tls_bind_conflict_{}",
issuerd_core::utils::generate_id()
));
let _ = std::fs::create_dir_all(&tmp_dir);
let cert_path = tmp_dir.join("cert.pem");
let key_path = tmp_dir.join("key.pem");
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()])
.expect("generate self-signed cert");
std::fs::write(&cert_path, cert.cert.pem()).expect("write cert pem");
std::fs::write(&key_path, cert.key_pair.serialize_pem()).expect("write key pem");
let _guard = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = _guard.local_addr().unwrap();
let config = ServerConfig {
bind: "127.0.0.1".to_string(),
port: addr.port(),
tls: Some(crate::config::TlsConfig {
cert_path: cert_path.to_str().unwrap().to_string(),
key_path: key_path.to_str().unwrap().to_string(),
}),
..Default::default()
};
let result = run(&config, axum::Router::new(), std::future::ready(())).await;
assert!(result.is_err());
let _ = std::fs::remove_dir_all(&tmp_dir);
}
}