use axum::{extract::Request, response::IntoResponse, routing::any, Extension, Router}; use clap::{Parser, Subcommand}; use std::path::Path; #[derive(Parser)] #[command(name = "markbase", about = "Momentry Display Engine")] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Start display server Display { #[arg(short, long, default_value = "11438")] port: u16, /// Optional initial markdown file file: Option, }, /// Render markdown to HTML (stdout) Render { file: String, #[arg(short, long)] output: Option, }, /// Configuration management Config { #[command(subcommand)] action: ConfigCommands, }, /// Scan and import files from directory Scan { /// User ID #[arg(short, long)] user: String, /// Directory to scan #[arg(short, long)] dir: String, /// Batch size for database insertion #[arg(short, long, default_value = "100")] batch: usize, /// Skip SHA256 hash calculation (faster import) #[arg(short, long, default_value = "true")] skip_hash: bool, /// Number of threads for hash calculation (if skip_hash=false) #[arg(short, long, default_value = "4")] threads: usize, }, /// Compute SHA256 hashes for imported files Hash { /// User ID #[arg(short, long)] user: String, /// Number of threads for parallel hash calculation #[arg(short, long, default_value = "4")] threads: usize, }, /// Start WebDAV server WebDAV { #[command(subcommand)] action: WebDAVCommands, }, /// Manage iSCSI target (gotgt) Iscsi { #[command(subcommand)] action: IscsiCommands, }, /// Start SFTP server Sftp { /// Port to listen on #[arg(short, long, default_value = "2023")] port: u16, /// User ID for database #[arg(short, long)] user: String, }, /// Test bcrypt password hash BcryptTest { /// Password to hash #[arg(short, long, default_value = "demo123")] password: String, /// Hash to verify (optional) #[arg(long)] verify_hash: Option, }, /// Start SSH server (hand-written implementation) SshServer { /// Port to listen on (default 2024) #[arg(short, long, default_value = "2024")] port: u16, }, } #[derive(Subcommand)] pub enum IscsiCommands { /// Start iSCSI target daemon Start { #[arg(short, long)] user: String, #[arg(short, long, default_value = "3260")] port: u16, #[arg(short, long, default_value = "5GB")] lun_size: String, #[arg(short, long)] force: bool, /// Block device path (e.g., /dev/disk5). Overrides file-backed LUN. #[arg(long)] device: Option, }, /// Stop iSCSI target daemon Stop, /// Show iSCSI target status Status, } #[derive(Subcommand)] enum WebDAVCommands { /// Start WebDAV server for user Start { /// Port to listen on #[arg(short, long, default_value = "8002")] port: u16, /// User ID for database #[arg(short, long)] user: String, }, } #[derive(Subcommand)] enum ConfigCommands { /// Initialize default configuration file Init { #[arg(short, long)] force: bool, }, /// Show current configuration Show { #[arg(short, long)] section: Option, }, /// Edit configuration Edit { #[arg(short, long)] key: String, #[arg(short, long)] value: String, }, /// Validate configuration Validate, } #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match cli.command { Commands::Display { port, file } => { markbase_core::server::run(port, file).await?; } Commands::Render { file, output } => { let md = std::fs::read_to_string(&file)?; let html = markbase_core::render::md_to_html(&md); if let Some(path) = &output { std::fs::write(path, html)?; } else { println!("{html}"); } } Commands::Config { action } => { handle_config_command(action)?; } Commands::Scan { user, dir, batch, skip_hash, threads, } => { use markbase_core::scan::ScanOptions; let options = ScanOptions { skip_hash, threads }; markbase_core::scan::scan_directory(&user, &dir, batch, options)?; } Commands::Hash { user, threads } => { markbase_core::scan::compute_hashes(&user, threads)?; } Commands::WebDAV { action } => { handle_webdav_command(action).await?; } Commands::Iscsi { action } => { handle_iscsi_command(action)?; } Commands::Sftp { port, user } => { println!("SFTP server command is currently disabled (old implementation)"); println!("Use 'ssh-server' command for the new SSH+SFTP implementation"); // handle_sftp_command(port, user).await?; } Commands::BcryptTest { password, verify_hash } => { handle_bcrypt_test(password, verify_hash)?; } Commands::SshServer { port } => { println!("=== MarkBase SSH Server (Hand-written Implementation) ==="); println!("Port: {}", port); // port已经是u16,不是Option println!("Implementation: SSH-2.0-MarkBaseSSH_1.0"); println!("Features: SSH + SFTP + SCP + rsync"); println!("Security: ⭐⭐⭐⭐⭐ (RustCrypto authoritative libraries)"); println!(); markbase_core::ssh_server::server::run_ssh_server(Some(port))?; } Commands::BcryptTest { password, verify_hash, } => { handle_bcrypt_test(password, verify_hash)?; } } Ok(()) } fn handle_config_command(action: ConfigCommands) -> anyhow::Result<()> { match action { ConfigCommands::Init { force } => { let config_path = Path::new("config/markbase.toml"); if config_path.exists() && !force { println!("Configuration file already exists at config/markbase.toml"); println!("Use --force to overwrite"); return Ok(()); } let config = markbase_core::config::MarkBaseConfig::default_config(); config.save(config_path)?; println!("✓ Configuration file created: config/markbase.toml"); println!("Default values:"); println!(" Server port: {}", config.server.port); println!(" PostgreSQL host: {}", config.postgresql.host); println!(" Test users: {}", config.test.users.join(", ")); } ConfigCommands::Show { section } => { let config_path = Path::new("config/markbase.toml"); if !config_path.exists() { println!("Configuration file not found. Run 'markbase config init' first."); return Ok(()); } let config = markbase_core::config::MarkBaseConfig::load(config_path)?; if let Some(s) = section { show_section(&config, &s); } else { println!("{}", toml::to_string_pretty(&config)?); } } ConfigCommands::Edit { key, value } => { let config_path = Path::new("config/markbase.toml"); if !config_path.exists() { println!("Configuration file not found. Run 'markbase config init' first."); return Ok(()); } let mut config = markbase_core::config::MarkBaseConfig::load(config_path)?; match config.get(&key) { Some(old_value) => { config.set(&key, &value)?; config.validate()?; config.save(config_path)?; println!("✓ Updated {}: {} → {}", key, old_value, value); } None => { println!("Invalid config key: {}", key); println!( "Valid keys: server.*, postgresql.*, authentication.*, test.*, logging.*" ); } } } ConfigCommands::Validate => { let config_path = Path::new("config/markbase.toml"); if !config_path.exists() { println!("Configuration file not found. Run 'markbase config init' first."); return Ok(()); } let config = markbase_core::config::MarkBaseConfig::load(config_path)?; match config.validate() { Ok(_) => { println!("✓ Configuration is valid"); } Err(e) => { println!("✗ Configuration validation failed: {}", e); } } } } Ok(()) } fn show_section(config: &markbase_core::config::MarkBaseConfig, section: &str) { match section { "server" => println!("{}", toml::to_string_pretty(&config.server).unwrap()), "postgresql" => println!("{}", toml::to_string_pretty(&config.postgresql).unwrap()), "authentication" => println!("{}", toml::to_string_pretty(&config.authentication).unwrap()), "test" => println!("{}", toml::to_string_pretty(&config.test).unwrap()), "logging" => println!("{}", toml::to_string_pretty(&config.logging).unwrap()), _ => println!("Invalid section: {}. Valid sections: server, postgresql, authentication, test, logging", section), } } fn handle_iscsi_command(action: IscsiCommands) -> anyhow::Result<()> { let binary = find_binary("markbase-iscsi"); let mut cmd = std::process::Command::new(&binary); cmd.arg("iscsi"); match action { IscsiCommands::Start { user, port, lun_size, force, device, } => { cmd.arg("start") .args(["--user", &user]) .args(["--port", &port.to_string()]) .args(["--lun-size", &lun_size]); if force { cmd.arg("--force"); } if let Some(d) = device { cmd.args(["--device", &d]); } } IscsiCommands::Stop => { cmd.arg("stop"); } IscsiCommands::Status => { cmd.arg("status"); } } let status = cmd.status()?; std::process::exit(status.code().unwrap_or(1)); } async fn handle_webdav_command(action: WebDAVCommands) -> anyhow::Result<()> { match action { WebDAVCommands::Start { port, user } => { let db_path = std::path::PathBuf::from(filetree::FileTree::user_db_path(&user)); if !db_path.exists() { return Err(anyhow::anyhow!( "User database not found: {}", db_path.display() )); } println!("=== MarkBase WebDAV Server ==="); println!("User: {}", user); println!("Port: {}", port); println!("Database: {}", db_path.display()); println!(""); run_webdav_server(port, user, db_path).await?; } } Ok(()) } async fn run_webdav_server( port: u16, user: String, db_path: std::path::PathBuf, ) -> anyhow::Result<()> { use tokio::net::TcpListener; let webdav = markbase_webdav::webdav::MarkBaseWebDAV::new(user, db_path); let dav_handler = webdav.create_handler(); let app = Router::new() .route("/webdav", any(handle_dav)) .route("/webdav/", any(handle_dav)) .route("/webdav/*path", any(handle_dav)) .layer(Extension(dav_handler)); let addr = format!("127.0.0.1:{}", port); let listener = TcpListener::bind(&addr).await?; println!("WebDAV server listening on http://{}", addr); println!("Mount point: /webdav"); println!(""); println!("Press Ctrl+C to stop"); axum::serve(listener, app).await?; Ok(()) } async fn handle_dav( Extension(dav): Extension, req: Request, ) -> impl IntoResponse { dav.handle(req).await } fn find_binary(name: &str) -> std::path::PathBuf { let exe = std::env::current_exe().unwrap(); let dir = exe.parent().unwrap(); dir.join(name) } // async fn handle_sftp_command(port: u16, user: String) -> anyhow::Result<()> { // println!("=== MarkBase SFTP Server ==="); // println!("User: {}", user); // println!("Port: {}", port); // println!("Auth DB: data/auth.sqlite"); // println!("FileTree DB: data/users/{}.sqlite", user); // println!(""); // // let config = markbase_core::sftp::SftpConfig::load_default()?; // // if port != config.sftp.port { // println!( // "Warning: CLI port {} overrides config port {}", // port, config.sftp.port // ); // } // // markbase_core::sftp::run_server(config, &user).await?; // // Ok(()) // } fn handle_bcrypt_test(password: String, verify_hash: Option) -> anyhow::Result<()> { use bcrypt::{hash, verify, DEFAULT_COST}; println!("=== bcrypt Hash Test ==="); println!("Password: {}", password); println!(""); // Generate new hash let new_hash = hash(&password, DEFAULT_COST)?; println!("Generated hash:"); println!("{}", new_hash); println!(""); // Verify current hash if provided if let Some(hash_to_verify) = verify_hash { println!("Verifying hash: {}", hash_to_verify); let valid = verify(&password, &hash_to_verify)?; println!("Valid: {}", valid); println!(""); } // Verify database hash let db_hash = "$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6"; println!("Database hash: {}", db_hash); let valid = verify(&password, db_hash)?; println!("Database hash valid for '{}': {}", password, valid); println!(""); if !valid { println!("❌ Database hash is incorrect!"); println!("Update SQL:"); println!("UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');", new_hash); println!(""); println!("Execute:"); println!("sqlite3 data/auth.sqlite \"UPDATE sftpgo_users SET password_hash = '{}' WHERE username IN ('testuser', 'demo', 'warren', 'momentry');\"", new_hash); } else { println!("✅ Database hash is correct!"); } Ok(()) }