use axum::{ body::Body, extract::Request, http::{HeaderValue, StatusCode}, middleware, response::IntoResponse, Extension, }; use base64::Engine as _; use clap::Subcommand; use dav_server::DavHandler; use std::path::PathBuf; #[derive(Subcommand)] pub enum WebdavCommand { #[command(name = "webdav-start")] Start { #[arg(short, long, default_value = "8002")] port: u16, #[arg(short, long)] user: String, }, } pub async fn handle_webdav_command(cmd: WebdavCommand) -> anyhow::Result<()> { match cmd { WebdavCommand::Start { port, user } => { // Parse username and optional password (format: "name:password") let username = user.split(':').next().unwrap_or(&user).to_string(); let password = user.split(':').nth(1).map(|s| s.to_string()); let default_root = format!("/Users/accusys/momentry/var/sftpgo/data/{}", username); let home_dir = PathBuf::from( std::env::var("MB_WEBDAV_ROOT").unwrap_or(default_root), ); if !home_dir.exists() { return Err(anyhow::anyhow!( "User home directory not found: {}", home_dir.display() )); } println!("=== MarkBase WebDAV Server (VFS) ==="); println!("User: {}", username); if password.is_some() { println!("Auth: password protected"); } println!("Port: {}", port); println!("Home: {}", home_dir.display()); println!(); run_webdav_server(port, home_dir, username, password).await?; } } Ok(()) } async fn run_webdav_server( port: u16, home_dir: PathBuf, user: String, password: Option, ) -> anyhow::Result<()> { use axum::{routing::any, Router}; use tokio::net::TcpListener; let vfs = Box::new(crate::vfs::local_fs::LocalFs::new()); let upload_hook = None; let dav_handler = crate::webdav::create_webdav_handler(vfs, home_dir.clone(), upload_hook, user.clone()); async fn webdav_auth_middleware( Extension(expected): Extension, req: Request, next: middleware::Next, ) -> impl IntoResponse { let auth = req .headers() .get("Authorization") .and_then(|v| v.to_str().ok()) .filter(|v| v.starts_with("Basic ")) .and_then(|v| { let encoded = &v[6..]; let decoded = base64::engine::general_purpose::STANDARD .decode(encoded) .ok()?; let creds = String::from_utf8(decoded).ok()?; let colon = creds.find(':')?; Some((creds[..colon].to_string(), creds[colon + 1..].to_string())) }); let valid = auth.is_some_and(|(u, p)| { u == expected.username && expected.password.as_ref().is_none_or(|exp| p == *exp) }); if !valid { return ( StatusCode::UNAUTHORIZED, [( "WWW-Authenticate", HeaderValue::from_static("Basic realm=\"MarkBase WebDAV\""), )], Body::from("Unauthorized"), ).into_response(); } next.run(req).await } let app = Router::new() .route("/", any(handle_dav)) .route("/*path", any(handle_dav)) .layer(Extension(crate::webdav::WebdavCredentials { username: user.clone(), password, })) .layer(Extension(dav_handler)) .layer(middleware::from_fn(webdav_auth_middleware)); let addr = format!("0.0.0.0:{}", port); let listener = TcpListener::bind(&addr).await?; println!("WebDAV server listening on http://{}", addr); println!("Root: {}", home_dir.display()); println!("User: {}", user); println!(); println!("Press Ctrl+C to stop"); axum::serve(listener, app).await?; Ok(()) } async fn handle_dav( Extension(dav): Extension, Extension(_creds): Extension, req: Request, ) -> impl IntoResponse { dav.handle(req).await }