use std::convert::Infallible; use std::net::SocketAddr; use std::path::Path; #[cfg(unix)] use std::process::Command; use std::sync::atomic::Ordering; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{bail, Context, Result}; use dav_server::{fakels::FakeLs, DavHandler}; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; use tokio::sync::watch; use tracing::{debug, warn}; use zeroize::Zeroizing; use crate::crypto::mnemonic_to_dek; use crate::storage::Database; use crate::ui; use crate::vfs::SanctumFs; /// Authentifizierungsmethode für das Einbinden eines Containers: Entweder Master-Passwort oder 24-Wort Notfallschlüssel. #[derive(Debug, Clone)] pub enum ContainerAuth { Password(Zeroizing), RecoveryKey(Zeroizing), } /// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:") pub fn format_drive(drive_letter: char) -> String { format!("{}:", drive_letter.to_ascii_uppercase()) } /// Trennt ein Windows-Netzlaufwerk via WNetCancelConnection2W bzw. Unix-Mountpoint via gio. pub fn unmount_drive(drive_letter: char) -> Result<()> { #[cfg(windows)] { crate::windows::unmount_drive_wnet(drive_letter) } #[cfg(unix)] { let _ = drive_letter; Ok(()) } #[cfg(not(any(windows, unix)))] { let _ = drive_letter; Ok(()) } } /// Bindet ein Windows-Netzlaufwerk via in-process WNetAddConnection2W ein (R-05, kein Token in argv) /// bzw. unter Unix via `gio mount dav://127.0.0.1:/`. fn run_mount_command(drive_letter: char, port: u16, session_token: &str) -> Result<()> { #[cfg(windows)] { crate::windows::mount_drive_wnet(drive_letter, port, session_token) } #[cfg(unix)] { let dav_url = format!("dav://127.0.0.1:{}/", port); let _ = Command::new("gio").args(["mount", &dav_url]).output(); let _ = (drive_letter, session_token); Ok(()) } #[cfg(not(any(windows, unix)))] { let _ = (drive_letter, port, session_token); Ok(()) } } /// Startet den WebDAV-Server für den Sanctum-Container und bindet ihn als Netzlaufwerk (Windows) bzw. VFS (Linux) ein. pub async fn mount_container( container_path: &Path, drive_letter: char, mount_point: Option<&Path>, requested_port: Option, auth: ContainerAuth, open_explorer: bool, enable_tray: bool, idle_timeout: Option, lock_on_screen_lock: bool, anti_leak: bool, stealth: bool, ) -> Result<()> { let drive_str = format_drive(drive_letter); let _ = &drive_str; let _ = mount_point; let _ = open_explorer; let _ = enable_tray; let _ = lock_on_screen_lock; if !container_path.exists() { bail!( "Containerdatei '{}' existiert nicht.", container_path.display() ); } if !stealth { println!(); ui::step(1, 4, "📦", "Öffne Container & verifiziere Header..."); } let db = Database::open(container_path).context("Konnte Container-Datenbank nicht öffnen")?; let meta = db .read_meta() .context("Konnte Container-Header nicht lesen")?; let (dek, carrier_dek, carrier_node_id, version, vault_id) = match auth { ContainerAuth::Password(ref password) => { if !stealth { ui::step( 2, 4, "🔑", "Leite KEK via Argon2id ab (konstante Zeit über alle Slots)...", ); } match meta.authenticate(password) { Some(keys) => { if !stealth { ui::step( 3, 4, "🔓", "Master-Passwort erfolgreich verifiziert & DEK entschlüsselt!", ); } let vault_id = keys.slot_id(); let ver = keys.version(); let c_dek = keys.carrier_dek(); let c_nid = keys.carrier_node_id(); (keys.0, c_dek, c_nid, ver, vault_id) } None => { bail!("Ungültiges Master-Passwort oder Container beschädigt"); } } } ContainerAuth::RecoveryKey(ref phrase) => { if !stealth { ui::step(2, 4, "🔑", "Dekodiere DEK aus 24-Wort Notfallschlüssel..."); } let dek = mnemonic_to_dek(phrase).context("Ungültiger 24-Wort Notfallschlüssel")?; if !stealth { ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!"); } let carrier_node_id = db.find_carrier_node_id()?.unwrap_or(0); let (carrier_dek, final_carrier_node_id, vault_id) = if carrier_node_id > 0 { let slot_id = crate::recovery::detect_recovery_key_slot(&db, &dek, meta.version)?; if slot_id == 1 { if !stealth { println!(" [i] Der angegebene Notfallschlüssel gehört zum Hidden-Vault (Slot 1)."); println!(" Für den Zugriff auf die Trägerdatei wird das Passwort des Standard-Vaults benötigt."); } let decoy_pass = rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ") .context("Fehler beim Einlesen des Standard-Vault Passworts")?; let decoy_keys = meta.authenticate(&decoy_pass).ok_or_else(|| { anyhow::anyhow!("Ungültiges Passwort für Standard-Vault.") })?; (Some(decoy_keys.dek().clone()), Some(carrier_node_id), 1) } else { (None, Some(carrier_node_id), 0) } } else { (None, None, 0) }; ( dek, carrier_dek, final_carrier_node_id, meta.version, vault_id, ) } }; // S-06: Advisory Lock setzen, um parallele Mounts und schreibende Sync-Läufe abzuwehren let _advisory_lock = db.acquire_advisory_lock_guard(false)?; // 128-Bit Session-Token für Loopback-Schutz (CWE-306) & Anti-CSRF generieren let mut token_bytes = [0u8; 16]; rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token_bytes); let session_token = hex::encode(token_bytes); // WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield & Carrier-Routing) let fs = SanctumFs::with_carrier( db.clone(), dek, carrier_dek, carrier_node_id, version, anti_leak, vault_id, ); let last_activity = fs.last_activity(); let dav_server = DavHandler::builder() .filesystem(Box::new(fs)) .locksystem(FakeLs::new()) .build_handler(); // TCP-Port ermitteln und binden let port_to_try = requested_port.unwrap_or(8443); let listener = match TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port_to_try))).await { Ok(l) => l, Err(_) if requested_port.is_none() => { TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))) .await .context("Konnte keinen lokalen TCP-Port binden")? } Err(e) => { bail!("Konnte Port 127.0.0.1:{} nicht binden: {}", port_to_try, e); } }; let bound_addr = listener.local_addr()?; let bound_port = bound_addr.port(); if !stealth { #[cfg(windows)] ui::step( 4, 4, "🌐", &format!( "Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...", bound_port, drive_str ), ); #[cfg(not(windows))] ui::step( 4, 4, "🌐", &format!( "Starte WebDAV-Server auf Port {} & initialisiere VFS...", bound_port ), ); } let (shutdown_tx, shutdown_rx) = watch::channel(false); // Hyper HTTP Server Loop im Hintergrund starten let server_handle = tokio::spawn(serve_webdav_loop( listener, dav_server, session_token.clone(), shutdown_rx, )); // Netzlaufwerk bzw. Verzeichnis einbinden if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) { let _ = shutdown_tx.send(true); let _ = server_handle.await; return Err(e); } // Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault) #[cfg(windows)] if open_explorer { let _ = crate::windows::open_in_explorer(drive_letter); } // System-Tray Initialisierung (nur Windows) #[cfg(windows)] let (_tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1); #[cfg(windows)] let _tray = if enable_tray { let icon_source = crate::windows::get_default_system_icon() .unwrap_or(tray_item::IconSource::Resource("")); let title = format!("Sanctum ({drive_str})"); match tray_item::TrayItem::new(&title, icon_source) { Ok(mut tray) => { let container_name = container_path .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); let _ = tray.add_label(&format!("Sanctum: {drive_str} ({container_name})")); let dl = drive_letter; let _ = tray.add_menu_item("Im Explorer öffnen", move || { let _ = crate::windows::open_in_explorer(dl); }); let s_tx = _tray_shutdown_tx.clone(); let _ = tray.add_menu_item("Trennen & Beenden", move || { let _ = s_tx.blocking_send(()); }); Some(tray) } Err(e) => { debug!("System-Tray Icon konnte nicht erstellt werden: {e}"); None } } } else { None }; // Inaktivitäts-Timer (Auto-Lock) let (idle_shutdown_tx, mut idle_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1); if let Some(timeout_secs) = idle_timeout { if timeout_secs > 0 { let last_act = last_activity.clone(); let idle_tx = idle_shutdown_tx.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(std::time::Duration::from_secs(1)); loop { interval.tick().await; let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); let last = last_act.load(Ordering::Relaxed); if now.saturating_sub(last) >= timeout_secs { let _ = idle_tx.send(()).await; break; } } }); } } // Windows-Sitzungssperre (Win + L Auto-Lock, nur Windows) #[cfg(windows)] let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1); #[cfg(windows)] let _session_monitor = if lock_on_screen_lock { match crate::windows::start_session_lock_monitor(session_lock_tx) { Ok(guard) => Some(guard), Err(e) => { warn!("Konnte Windows Session-Lock Monitor nicht aktivieren: {e}"); None } } } else { None }; if stealth { #[cfg(windows)] println!("Sanctum: Netzlaufwerk {} bereit.", drive_str); #[cfg(not(windows))] println!("Sanctum: WebDAV-Server auf Port {} bereit.", bound_port); } else { println!(); println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ ✔ Sanctum Container erfolgreich gemountet │"); println!("└─────────────────────────────────────────────────────────────┘"); println!(); println!(" • Container: {}", container_path.display()); #[cfg(windows)] println!( " • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str) ); #[cfg(not(windows))] { if let Some(mp) = mount_point { println!(" • Mountpoint: {}", ui::cyan(&mp.display().to_string())); } else { println!(" • Modus: WebDAV Userland-VFS"); } } println!( " • WebDAV-URL: http://127.0.0.1:{}/ (lokal geschützt)", bound_port ); #[cfg(not(windows))] { for line in format_linux_mount_instructions(bound_port, &session_token, mount_point) { println!("{}", line); } } if let Some(secs) = idle_timeout { println!(" • Auto-Lock: Inaktivität nach {}s", secs); } #[cfg(windows)] if lock_on_screen_lock { println!(" • Sitzung: Automatisches Sperren bei Win + L aktiv"); } if anti_leak { #[cfg(windows)] println!(" • Anti-Leak: Explorer-Metadatenfilter aktiv (Thumbs.db, desktop.ini blockiert)"); #[cfg(not(windows))] println!(" • Anti-Leak: VFS-Metadatenfilter aktiv (.directory, .Trash, desktop.ini blockiert)"); } #[cfg(windows)] if enable_tray { println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)"); } println!(); #[cfg(windows)] println!( " [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp") ); #[cfg(not(windows))] println!( " [{}] Drücke [Ctrl+C] zum sicheren Beenden.", ui::yellow("Tipp") ); println!(); } // Windows Console Close Monitor (CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT, nur Windows) #[cfg(windows)] let (console_close_tx, mut console_close_rx) = tokio::sync::mpsc::channel::<()>(1); #[cfg(windows)] let _console_guard = crate::windows::start_console_ctrl_monitor(console_close_tx, drive_letter).ok(); // Unix Signale (SIGTERM, SIGHUP) #[cfg(unix)] let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).ok(); #[cfg(unix)] let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok(); // Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen) #[cfg(windows)] tokio::select! { res = tokio::signal::ctrl_c() => { let _ = res; if !stealth { println!(); println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]")); } } Some(()) = console_close_rx.recv() => { if !stealth { println!(); println!(" {} Konsolenfenster wird geschlossen — sichere Trennung ausgeführt!", ui::yellow("[!]")); } } Some(()) = tray_shutdown_rx.recv() => { if !stealth { println!(); println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]")); } } Some(()) = session_lock_rx.recv() => { if !stealth { println!(); println!(" {} Windows-Sitzung gesperrt (Win + L) — Auto-Lock ausgelöst!", ui::yellow("[!]")); } } Some(()) = idle_shutdown_rx.recv() => { if !stealth { println!(); println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]")); } } } #[cfg(unix)] tokio::select! { res = tokio::signal::ctrl_c() => { let _ = res; if !stealth { println!(); println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]")); } } _ = async { match sigterm.as_mut() { Some(s) => { s.recv().await; } None => { std::future::pending::<()>().await; } } } => { if !stealth { println!(); println!(" {} Beendigungssignal (SIGTERM) empfangen.", ui::yellow("[!]")); } } _ = async { match sighup.as_mut() { Some(s) => { s.recv().await; } None => { std::future::pending::<()>().await; } } } => { if !stealth { println!(); println!(" {} Beendigungssignal (SIGHUP) empfangen.", ui::yellow("[!]")); } } Some(()) = idle_shutdown_rx.recv() => { if !stealth { println!(); println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]")); } } } #[cfg(not(any(windows, unix)))] tokio::select! { res = tokio::signal::ctrl_c() => { let _ = res; if !stealth { println!(); println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]")); } } Some(()) = idle_shutdown_rx.recv() => { if !stealth { println!(); println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]")); } } } if stealth { let _ = unmount_drive(drive_letter); let _ = shutdown_tx.send(true); let _ = server_handle.await; if let Ok(freelist) = db.freelist_count() { if freelist > 0 { let _ = db.incremental_vacuum(None); } } let _ = db.checkpoint(); #[cfg(windows)] println!("Sanctum: Laufwerk {} getrennt und geschlossen.", drive_str); #[cfg(not(windows))] println!("Sanctum: Container geschlossen und WebDAV-Server beendet."); } else { #[cfg(windows)] { print!( " {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str ); let _ = std::io::Write::flush(&mut std::io::stdout()); // Automatisches Unmount if let Err(e) = unmount_drive(drive_letter) { println!("{}", ui::yellow(&format!("Warnung ({e})"))); } else { println!("{}", ui::green("OK")); } } #[cfg(not(windows))] { print!(" {} Beende WebDAV-Server ... ", ui::dim("[-]")); let _ = std::io::Write::flush(&mut std::io::stdout()); println!("{}", ui::green("OK")); } // HTTP Server beenden let _ = shutdown_tx.send(true); let _ = server_handle.await; // Storage-Kompaktierung (Incremental Vacuum), falls freie Seiten existieren if let Ok(freelist) = db.freelist_count() { if freelist > 0 { print!( " {} Führe Storage-Kompaktierung aus ({} freie Seiten) ... ", ui::dim("[-]"), freelist ); let _ = std::io::Write::flush(&mut std::io::stdout()); match db.incremental_vacuum(None) { Ok(freed) => println!("{} ({} Seiten freigegeben)", ui::green("OK"), freed), Err(e) => println!("{}", ui::yellow(&format!("Warnung ({e})"))), } } } // SQLite WAL Checkpoint erzwingen print!(" {} Führe SQLite WAL-Checkpoint aus ... ", ui::dim("[-]")); let _ = std::io::Write::flush(&mut std::io::stdout()); if let Err(e) = db.checkpoint() { println!("{}", ui::red(&format!("Fehler ({e})"))); } else { println!("{}", ui::green("OK")); } println!(); println!( "{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green("✔") ); println!(); } drop(db); // S-10: Verifikation und Bereinigung von temporären SQLite WAL- und SHM-Dateien let base_os = container_path.as_os_str().to_os_string(); let mut wal = base_os.clone(); wal.push("-wal"); let _ = std::fs::remove_file(wal); let mut shm = base_os; shm.push("-shm"); let _ = std::fs::remove_file(shm); Ok(()) } /// Überprüft, ob ein Host-Header exakt auf Loopback (127.0.0.1, localhost, [::1], ::1) zeigt (Anti-DNS-Rebinding & S-06). /// Striktes Parsing von Host und Port ohne anfällige starts_with-Präfixprüfungen. pub fn is_loopback_host(host_str: &str) -> bool { let host_trimmed = host_str.trim().to_ascii_lowercase(); if host_trimmed.is_empty() { return false; } if host_trimmed == "::1" { return true; } let host_part = if host_trimmed.starts_with('[') { // IPv6 bracketed: [::1] oder [::1]:port if let Some(bracket_end) = host_trimmed.find(']') { let inside = &host_trimmed[1..bracket_end]; let after = &host_trimmed[bracket_end + 1..]; if !after.is_empty() { if !after.starts_with(':') { return false; } if after[1..].parse::().is_err() { return false; } } inside } else { return false; } } else { // IPv4 oder Hostname: localhost, localhost:8443, 127.0.0.1, 127.0.0.1:8443 if let Some((h, p)) = host_trimmed.rsplit_once(':') { if p.parse::().is_ok() { h } else { return false; } } else { &host_trimmed } }; matches!(host_part, "127.0.0.1" | "localhost" | "::1") } /// Erzeugt die plattformspezifischen Linux-Mount-Anweisungen und Authentifizierungsdaten (M-03). pub fn format_linux_mount_instructions( port: u16, token: &str, mount_point: Option<&std::path::Path>, ) -> Vec { let mut lines = Vec::new(); lines.push(format!( " • WebDAV-Auth: Benutzer: sanctum | Token: {}", token )); lines.push(format!( " • gio Befehl: echo {} | gio mount dav://sanctum@127.0.0.1:{}/", token, port )); if let Some(mp) = mount_point { lines.push(format!( " • davfs2: mount -t davfs -o username=sanctum http://127.0.0.1:{}/ {}", port, mp.display() )); } else { lines.push(format!( " • davfs2: mount -t davfs -o username=sanctum http://127.0.0.1:{}/ /mnt/sanctum", port )); } lines } /// Maximale Anzahl gleichzeitiger Verbindungen zum lokalen WebDAV-Endpunkt (Schutz gegen Socket-Exhaustion). const MAX_CONCURRENT_DAV_CONNECTIONS: usize = 64; /// Timeout für das Lesen von HTTP-Headern (Schutz gegen Slowloris-Angriffe auf Loopback). const HTTP_HEADER_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15); /// Hilfsfunktion zur Validierung von HTTP Basic Auth. /// Unterstützt Format `username:password` (wobei Passwort dem Session-Token entspricht). /// SA-06: Verwendet strikten Constant-Time-Vergleich gegen Timing-Side-Channel-Angriffe. pub fn check_basic_auth(auth_header: &str, expected_token: &str) -> bool { let auth_str = auth_header.trim(); let encoded = if let Some(rest) = auth_str.strip_prefix("Basic ") { rest } else if let Some(rest) = auth_str.strip_prefix("basic ") { rest } else { return false; }; use base64::Engine; let decoded_bytes = match base64::engine::general_purpose::STANDARD.decode(encoded.trim()) { Ok(bytes) => bytes, Err(_) => return false, }; let decoded_str = match std::str::from_utf8(&decoded_bytes) { Ok(s) => s, Err(_) => return false, }; use subtle::ConstantTimeEq; if let Some((_user, pass)) = decoded_str.split_once(':') { pass.as_bytes().ct_eq(expected_token.as_bytes()).into() } else { false } } /// Hilfsfunktion zur Validierung des X-Sanctum-Token Headers. /// SA-06: Verwendet strikten Constant-Time-Vergleich gegen Timing-Side-Channel-Angriffe. pub fn check_token_header(headers: &hyper::HeaderMap, expected_token: &str) -> bool { use subtle::ConstantTimeEq; if let Some(val) = headers.get("X-Sanctum-Token") { if let Ok(val_str) = val.to_str() { return val_str .trim() .as_bytes() .ct_eq(expected_token.as_bytes()) .into(); } } false } /// Prüft, ob eine URI oder deren Query-Parameter das sensible Session-Token enthält (SA-05 / CWE-598). pub fn uri_contains_token(uri: &hyper::Uri, session_token: &str) -> bool { if session_token.is_empty() { return false; } uri.path().contains(session_token) || uri.query().unwrap_or("").contains(session_token) } /// Erzeugt eine standardkonforme HTTP 401 Unauthorized Antwort mit WWW-Authenticate Header. fn unauthorized_response() -> hyper::Response { hyper::Response::builder() .status(hyper::StatusCode::UNAUTHORIZED) .header(hyper::header::WWW_AUTHENTICATE, "Basic realm=\"Sanctum\"") .header(hyper::header::CONTENT_LENGTH, "0") .body(dav_server::body::Body::empty()) .unwrap() } /// Führt die asynchrone WebDAV HTTP-Server-Schleife mit Header-basierter Authentifizierung & Host-Header Sicherheits-Middleware aus. pub async fn serve_webdav_loop( listener: TcpListener, dav_server: DavHandler, session_token: String, mut shutdown_rx: watch::Receiver, ) { let conn_semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_DAV_CONNECTIONS)); loop { tokio::select! { res = listener.accept() => { let (stream, _) = match res { Ok(val) => val, Err(e) => { warn!("Verbindungsfehler im TCP-Listener: {e}"); continue; } }; // RT-01: Schutz gegen Connection-Starvation / Socket-Flooding (CWE-400) let permit = match conn_semaphore.clone().try_acquire_owned() { Ok(p) => p, Err(_) => { warn!( "WebDAV-Verbindungslimit ({} aktive Verbindungen) erreicht: Wehre potenziellen Connection-Starvation-Angriff ab.", MAX_CONCURRENT_DAV_CONNECTIONS ); drop(stream); continue; } }; let io = TokioIo::new(stream); let handler = dav_server.clone(); let expected_token = session_token.clone(); tokio::spawn(async move { let _permit = permit; // Permit wird bei Verbindungsende automatisch freigegeben let service = service_fn(move |req| { let h = handler.clone(); let token = expected_token.clone(); async move { // 1. RT-02: Strikte Fail-Closed Host-Header Validierung (Anti-DNS-Rebinding & Anti-Spoofing) let host_valid = match req.headers().get(hyper::header::HOST) { Some(host_val) => match host_val.to_str() { Ok(host_str) => is_loopback_host(host_str), Err(_) => false, }, None => false, }; if !host_valid { warn!( "Abgewiesener Zugriff: Fehlender, ungültiger oder externer Host-Header ({:?})", req.headers().get(hyper::header::HOST) ); let res = hyper::Response::builder() .status(hyper::StatusCode::FORBIDDEN) .header(hyper::header::CONTENT_LENGTH, "0") .body(dav_server::body::Body::empty()) .unwrap(); return Ok::<_, Infallible>(res); } // 2. SA-05: Verhindere Session-Token in Request-URI oder Query (CWE-598). // Session-Tokens dürfen ausschließlich in HTTP-Headern übertragen werden. if uri_contains_token(req.uri(), &token) { warn!("Abgewiesener Zugriff: Session-Token in URI/Query übermittelt (SA-05 / CWE-598)"); let res = hyper::Response::builder() .status(hyper::StatusCode::FORBIDDEN) .header(hyper::header::CONTENT_LENGTH, "0") .body(dav_server::body::Body::empty()) .unwrap(); return Ok::<_, Infallible>(res); } // 3. SA-05 & SA-06: Header-basierte Authentifizierung mit Constant-Time Token-Vergleich // a) HTTP Basic Auth (Authorization: Basic ...) // b) Header X-Sanctum-Token let mut authenticated = false; if let Some(auth_val) = req.headers().get(hyper::header::AUTHORIZATION) { if let Ok(auth_str) = auth_val.to_str() { if check_basic_auth(auth_str, &token) { authenticated = true; } } } if !authenticated && check_token_header(req.headers(), &token) { authenticated = true; } if !authenticated { debug!("Abgewiesener unauthentifizierter Zugriff auf: {}", req.uri().path()); return Ok::<_, Infallible>(unauthorized_response()); } Ok::<_, Infallible>(h.handle(req).await) } }); // RT-01: Header-Read-Timeout & TokioTimer zur aktiven Abwehr von Slowloris-Hanging-Sockets let mut builder = http1::Builder::new(); builder.timer(hyper_util::rt::TokioTimer::new()); builder.header_read_timeout(HTTP_HEADER_READ_TIMEOUT); if let Err(err) = builder.serve_connection(io, service).await { debug!("HTTP-Verbindungsende: {:?}", err); } }); } _ = shutdown_rx.changed() => { debug!("WebDAV-Server-Task empfängt Shutdown-Signal."); break; } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_loopback_host_validation() { assert!(is_loopback_host("127.0.0.1")); assert!(is_loopback_host("127.0.0.1:8443")); assert!(is_loopback_host("localhost")); assert!(is_loopback_host("localhost:8443")); assert!(is_loopback_host("[::1]")); assert!(is_loopback_host("[::1]:8443")); // Abweisung externer Hosts oder DNS-Rebinding-Attacken assert!(!is_loopback_host("evil.com")); assert!(!is_loopback_host("attacker.local")); assert!(!is_loopback_host("192.168.1.50")); assert!(!is_loopback_host("10.0.0.1")); assert!(!is_loopback_host("")); } #[test] fn test_format_drive() { assert_eq!(format_drive('s'), "S:"); assert_eq!(format_drive('Z'), "Z:"); } #[test] fn test_check_basic_auth() { use base64::Engine; let token = "deadbeefcafebabe0123456789abcdef"; let creds = format!("sanctum:{}", token); let header_val = format!( "Basic {}", base64::engine::general_purpose::STANDARD.encode(creds) ); assert!(check_basic_auth(&header_val, token)); // Kleingeschriebenes basic Präfix let lower_header = format!( "basic {}", base64::engine::general_purpose::STANDARD.encode(format!("user:{}", token)) ); assert!(check_basic_auth(&lower_header, token)); // Falsches Token let wrong_token_header = format!( "Basic {}", base64::engine::general_purpose::STANDARD.encode("sanctum:wrongtoken") ); assert!(!check_basic_auth(&wrong_token_header, token)); // Ungültiges Base64 oder Format assert!(!check_basic_auth("Basic !!!notbase64!!!", token)); assert!(!check_basic_auth("Bearer 12345", token)); assert!(!check_basic_auth("", token)); } #[test] fn test_check_token_header() { let token = "deadbeefcafebabe0123456789abcdef"; let mut headers = hyper::HeaderMap::new(); assert!(!check_token_header(&headers, token)); headers.insert("X-Sanctum-Token", token.parse().unwrap()); assert!(check_token_header(&headers, token)); let mut wrong_headers = hyper::HeaderMap::new(); wrong_headers.insert("X-Sanctum-Token", "wrong".parse().unwrap()); assert!(!check_token_header(&wrong_headers, token)); } #[test] fn test_uri_contains_token_rejection() { let token = "deadbeefcafebabe0123456789abcdef"; // Token im Pfad let uri_path: hyper::Uri = format!("http://127.0.0.1:8443/{}/Photos/vacation.jpg", token) .parse() .unwrap(); assert!(uri_contains_token(&uri_path, token)); // Token im Query-String let uri_query: hyper::Uri = format!("http://127.0.0.1:8443/Photos/vacation.jpg?token={}", token) .parse() .unwrap(); assert!(uri_contains_token(&uri_query, token)); // Harmloser Pfad ohne Token let uri_clean: hyper::Uri = "http://127.0.0.1:8443/Photos/vacation.jpg".parse().unwrap(); assert!(!uri_contains_token(&uri_clean, token)); // Leeres Token darf niemals matchen assert!(!uri_contains_token(&uri_clean, "")); } #[test] fn test_m03_format_linux_mount_instructions() { let token = "fedcba9876543210fedcba9876543210"; let instructions = format_linux_mount_instructions( 8443, token, Some(std::path::Path::new("/mnt/secure")), ); let all_text = instructions.join("\n"); assert!(all_text.contains("Benutzer: sanctum")); assert!(all_text.contains(token)); assert!(all_text.contains("gio mount dav://sanctum@127.0.0.1:8443/")); assert!(all_text.contains("mount -t davfs -o username=sanctum http://127.0.0.1:8443/ /mnt/secure")); } }