diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index 0e7a529fae..9fee84792b 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -9,6 +9,7 @@ mod web_mcp; use std::collections::{HashMap, HashSet}; use std::net::{IpAddr, SocketAddr}; +use std::process::ExitCode; use std::sync::Arc; use argon2::password_hash::rand_core::OsRng; @@ -456,18 +457,28 @@ fn add_mq_routes(router: Router>) -> Router> { router } -fn main() { +fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); if help_requested(&args) { print!("{HELP_TEXT}"); - return; + return ExitCode::SUCCESS; } let runtime = dbx_core::scheduled_backup::worker_runtime().expect("Failed to build tokio runtime"); - runtime.block_on(serve()); + match runtime.block_on(serve()) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("{error}"); + ExitCode::FAILURE + } + } } -async fn serve() { +fn web_mcp_startup_error(error: String) -> String { + format!("Failed to start DBX Web: invalid Web MCP configuration: {error}") +} + +async fn serve() -> Result<(), String> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() @@ -542,7 +553,7 @@ async fn serve() { let web_mcp = Arc::new(if migration_ready { WebMcpRuntime::load(&app_state.storage, !password_disabled && password_hash.is_some()) .await - .expect("Invalid DBX Web MCP configuration") + .map_err(web_mcp_startup_error)? } else { WebMcpRuntime::disabled() }); @@ -1457,7 +1468,7 @@ async fn serve() { .layer(CompressionLayer::new().compress_when(web_compression_predicate())) .layer(tower_http::trace::TraceLayer::new_for_http()); - let mcp_router = web_mcp_router(&web_state).expect("Invalid DBX Web MCP configuration"); + let mcp_router = web_mcp_router(&web_state).map_err(web_mcp_startup_error)?; app = app.merge( mcp_router .layer(middleware::from_fn_with_state(web_state.clone(), web_mcp_demo_gate)) diff --git a/crates/dbx-web/src/web_mcp.rs b/crates/dbx-web/src/web_mcp.rs index 9ef9a8a675..2ba3755ba3 100644 --- a/crates/dbx-web/src/web_mcp.rs +++ b/crates/dbx-web/src/web_mcp.rs @@ -318,6 +318,63 @@ mod tests { assert!(validate_token("token\n".to_string()).is_err()); } + #[tokio::test] + async fn deployment_token_without_allowed_hosts_returns_error() { + let (_dir, storage) = test_storage().await; + let result = WebMcpRuntime::load_with_deployment( + &storage, + Some(DeploymentConfig { + token: "deployment-secret".to_string(), + source: TokenSource::Environment, + allowed_hosts: vec![], + allowed_origins: vec![], + }), + false, + ) + .await; + + assert_eq!(result.err().as_deref(), Some("DBX_WEB_MCP_ALLOWED_HOSTS or a Web MCP allowed Host is required")); + } + + #[tokio::test] + async fn page_managed_settings_without_allowed_hosts_returns_dual_path_error() { + let (_dir, storage) = test_storage().await; + let runtime = WebMcpRuntime::load_with_deployment(&storage, None, true).await.unwrap(); + + let result = runtime.update(&storage, request(true, &[], false)).await; + + assert_eq!(result.err().as_deref(), Some("DBX_WEB_MCP_ALLOWED_HOSTS or a Web MCP allowed Host is required")); + assert!(!runtime.auth.enabled()); + assert!(storage.get_secret(SECRET_NAMESPACE, SECRET_KEY).await.unwrap().is_none()); + } + + #[tokio::test] + async fn valid_deployment_configuration_enables_mcp() { + let (_dir, storage) = test_storage().await; + let runtime = WebMcpRuntime::load_with_deployment( + &storage, + Some(DeploymentConfig { + token: "deployment-secret".to_string(), + source: TokenSource::Environment, + allowed_hosts: vec!["dbx.example.test:4224".to_string()], + allowed_origins: vec![], + }), + false, + ) + .await + .unwrap(); + + assert!(runtime.status("/mcp".to_string(), false).enabled); + } + + #[tokio::test] + async fn absent_deployment_configuration_leaves_mcp_disabled() { + let (_dir, storage) = test_storage().await; + let runtime = WebMcpRuntime::load_with_deployment(&storage, None, false).await.unwrap(); + + assert!(!runtime.status("/mcp".to_string(), false).enabled); + } + #[tokio::test] async fn managed_token_survives_restart_and_is_encrypted() { let (_dir, storage) = test_storage().await;