#[cfg(feature = "cli")] use std::path::PathBuf; #[cfg(feature = "cli")] use std::sync::Arc; #[cfg(feature = "cli")] use clap::Parser; #[cfg(feature = "cli")] use secretumvault::cli::{Cli, Command, OperatorCommand, SecretCommand}; #[cfg(feature = "cli")] use secretumvault::config::VaultConfig; #[cfg(feature = "cli")] use secretumvault::core::VaultCore; #[tokio::main] #[cfg(feature = "cli")] async fn main() -> Result<(), Box> { let cli = Cli::parse(); // Set up logging tracing_subscriber::fmt() .with_max_level( cli.log_level .parse::() .unwrap_or(tracing::Level::INFO), ) .init(); // Determine config path let config_path = cli.config.unwrap_or_else(|| PathBuf::from("svault.toml")); match cli.command { Command::Server { address, port } => server_command(&config_path, &address, port).await?, Command::Operator(cmd) => operator_command(&config_path, cmd).await?, Command::Secret(cmd) => secret_command(cmd).await?, } Ok(()) } #[cfg(feature = "cli")] async fn server_command( config_path: &PathBuf, cli_address: &str, cli_port: u16, ) -> Result<(), Box> { #[cfg(feature = "server")] { use secretumvault::api::server::build_router; tracing::info!("Loading configuration from {:?}", config_path); let config = VaultConfig::from_file(config_path)?; let vault = Arc::new(VaultCore::from_config(&config).await?); tracing::info!("Vault initialized successfully"); let bind_address = resolve_bind_address(&config.server.address, cli_address, cli_port); let router = build_router(vault); let tls_config = build_tls_config(&config.server); start_server(&bind_address, router, tls_config).await?; Ok(()) } #[cfg(not(feature = "server"))] { tracing::error!("Server feature not enabled. Compile with --features server"); Err("Server feature not enabled".into()) } } #[cfg(all(feature = "cli", feature = "server"))] fn resolve_bind_address(config_address: &str, cli_address: &str, cli_port: u16) -> String { if cli_address != "127.0.0.1" || cli_port != 8200 { format!("{}:{}", cli_address, cli_port) } else { config_address.to_string() } } #[cfg(all(feature = "cli", feature = "server"))] fn build_tls_config( server_config: &secretumvault::config::ServerSection, ) -> Option { match (&server_config.tls_cert, &server_config.tls_key) { (Some(cert), Some(key)) => Some(secretumvault::api::tls::TlsConfig::new( cert.clone(), key.clone(), server_config.tls_client_ca.clone(), )), _ => None, } } #[cfg(all(feature = "cli", feature = "server"))] async fn shutdown_signal() { use tokio::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 => {}, } tracing::info!("Shutdown signal received, stopping server gracefully..."); } #[cfg(all(feature = "cli", feature = "server"))] async fn start_server( bind_address: &str, app: axum::Router, tls_config: Option, ) -> Result<(), Box> { use std::net::SocketAddr; use tokio::net::TcpListener; let addr: SocketAddr = bind_address .parse() .map_err(|_| format!("Invalid bind address: {}", bind_address))?; let listener = TcpListener::bind(addr).await?; match tls_config { Some(tls) => { tls.validate()?; let rustls_config = if tls.client_ca_path.is_some() { secretumvault::api::tls::load_server_config_with_mtls(&tls)? } else { secretumvault::api::tls::load_server_config(&tls)? }; tracing::info!("Starting HTTPS server on https://{}", addr); if tls.client_ca_path.is_some() { tracing::info!("mTLS enabled - client certificate verification required"); } let tls_acceptor = tokio_rustls::TlsAcceptor::from(std::sync::Arc::new(rustls_config)); serve_with_tls(listener, app, tls_acceptor, shutdown_signal()).await?; Ok(()) } None => { tracing::warn!("Starting HTTP server on http://{}", addr); tracing::warn!("TLS not configured. For production, configure tls_cert and tls_key"); serve_plain(listener, app, shutdown_signal()).await?; Ok(()) } } } #[cfg(all(feature = "cli", feature = "server"))] async fn serve_plain( listener: tokio::net::TcpListener, app: axum::Router, shutdown: impl std::future::Future + Send + 'static, ) -> Result<(), Box> { axum::serve(listener, app) .with_graceful_shutdown(shutdown) .await?; Ok(()) } #[cfg(all(feature = "cli", feature = "server"))] async fn serve_with_tls( listener: tokio::net::TcpListener, app: axum::Router, tls_acceptor: tokio_rustls::TlsAcceptor, shutdown: impl std::future::Future + Send + 'static, ) -> Result<(), Box> { use hyper_util::rt::{TokioExecutor, TokioIo}; use hyper_util::server::conn::auto::Builder; use hyper_util::service::TowerToHyperService; let app = app.into_service(); tokio::pin!(shutdown); loop { tokio::select! { result = listener.accept() => { let (tcp_stream, _remote_addr) = result?; let tls_acceptor = tls_acceptor.clone(); let app = app.clone(); tokio::spawn(async move { // TLS handshake let tls_stream = match tls_acceptor.accept(tcp_stream).await { Ok(stream) => stream, Err(e) => { tracing::warn!("TLS handshake failed: {}", e); return; } }; let io = TokioIo::new(tls_stream); let hyper_service = TowerToHyperService::new(app); if let Err(err) = Builder::new(TokioExecutor::new()) .serve_connection(io, hyper_service) .await { tracing::warn!("Error serving connection: {}", err); } }); } _ = &mut shutdown => { tracing::info!("Shutdown signal received, stopping server..."); break; } } } Ok(()) } #[cfg(feature = "cli")] async fn operator_command( config_path: &PathBuf, cmd: OperatorCommand, ) -> Result<(), Box> { tracing::info!("Loading configuration from {:?}", config_path); let config = VaultConfig::from_file(config_path)?; let vault = Arc::new(VaultCore::from_config(&config).await?); tracing::info!("Vault loaded successfully"); match cmd { OperatorCommand::Init { shares, threshold } => { tracing::info!( "Initializing vault with {} shares, {} threshold", shares, threshold ); match secretumvault::cli::commands::init_vault(&vault, shares, threshold).await { Ok(share_list) => { secretumvault::cli::commands::print_init_result(&share_list, threshold as u64); tracing::info!("Vault initialized successfully"); } Err(e) => { tracing::error!("Failed to initialize vault: {}", e); return Err(format!("Init failed: {}", e).into()); } } } OperatorCommand::Unseal { shares } => { tracing::info!("Unsealing vault with {} shares", shares.len()); match secretumvault::cli::commands::unseal_vault(&vault, &shares).await { Ok(success) => { if success { println!("✓ Vault unsealed successfully!"); tracing::info!("Vault unsealed"); } else { println!("✗ Vault is still sealed (more shares needed?)"); tracing::warn!("Vault still sealed"); } } Err(e) => { tracing::error!("Failed to unseal vault: {}", e); return Err(format!("Unseal failed: {}", e).into()); } } } OperatorCommand::Seal => { tracing::info!("Sealing vault"); match secretumvault::cli::commands::seal_vault(&vault).await { Ok(()) => { println!("✓ Vault sealed successfully!"); tracing::info!("Vault sealed"); } Err(e) => { tracing::error!("Failed to seal vault: {}", e); return Err(format!("Seal failed: {}", e).into()); } } } OperatorCommand::Status => { tracing::info!("Checking vault status"); match secretumvault::cli::commands::vault_status(&vault).await { Ok((sealed, initialized)) => { secretumvault::cli::commands::print_status(sealed, initialized); } Err(e) => { tracing::error!("Failed to get vault status: {}", e); return Err(format!("Status check failed: {}", e).into()); } } } } Ok(()) } #[cfg(feature = "cli")] async fn secret_command(cmd: SecretCommand) -> Result<(), Box> { use secretumvault::cli::client::VaultClient; match cmd { SecretCommand::Read { path, address, port, token, } => { tracing::info!("Reading secret from {}:{}: {}", address, port, path); let client = VaultClient::new(&address, port, token); match client.read_secret(&path).await { Ok(data) => { println!("{}", serde_json::to_string_pretty(&data)?); tracing::info!("Secret read successfully"); } Err(e) => { tracing::error!("Failed to read secret: {}", e); return Err(format!("Read failed: {}", e).into()); } } } SecretCommand::Write { path, data, address, port, token, } => { tracing::info!("Writing secret to {}:{}: {}", address, port, path); let client = VaultClient::new(&address, port, token); let payload: serde_json::Value = serde_json::from_str(&data)?; match client.write_secret(&path, &payload).await { Ok(response) => { println!("✓ Secret written successfully!"); if let Some(data) = response.get("data") { println!("{}", serde_json::to_string_pretty(data)?); } tracing::info!("Secret written successfully"); } Err(e) => { tracing::error!("Failed to write secret: {}", e); return Err(format!("Write failed: {}", e).into()); } } } SecretCommand::Delete { path, address, port, token, } => { tracing::info!("Deleting secret from {}:{}: {}", address, port, path); let client = VaultClient::new(&address, port, token); match client.delete_secret(&path).await { Ok(()) => { println!("✓ Secret deleted successfully!"); tracing::info!("Secret deleted successfully"); } Err(e) => { tracing::error!("Failed to delete secret: {}", e); return Err(format!("Delete failed: {}", e).into()); } } } SecretCommand::List { path, address, port, token, } => { tracing::info!("Listing secrets at {}:{}: {}", address, port, path); let client = VaultClient::new(&address, port, token); match client.list_secrets(&path).await { Ok(keys) => { println!("\nSecrets at {}:", path); println!("━━━━━━━━━━━━━━━━━━━━━━"); for key in keys { println!(" {}", key); } println!(); tracing::info!("Secrets listed successfully"); } Err(e) => { tracing::error!("Failed to list secrets: {}", e); return Err(format!("List failed: {}", e).into()); } } } } Ok(()) } #[cfg(not(feature = "cli"))] fn main() { eprintln!("CLI feature not enabled. Compile with --features cli"); std::process::exit(1); }