use std::convert::Infallible; use std::net::SocketAddr; use std::path::Path; 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 `net use : /delete /y` bzw. Unix-Mountpoint via `gio mount -u`. pub fn unmount_drive(drive_letter: char) -> Result<()> { #[cfg(windows)] { let drive_str = format_drive(drive_letter); let output = Command::new("net") .args(["use", &drive_str, "/delete", "/y"]) .output() .context("Fehler beim Ausführen des Befehls 'net use'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); bail!( "Netzlaufwerk {} konnte nicht getrennt werden:\n{}{}", drive_str, stdout, stderr ); } Ok(()) } #[cfg(unix)] { let _ = drive_letter; Ok(()) } #[cfg(not(any(windows, unix)))] { let _ = drive_letter; Ok(()) } } /// Bindet ein Windows-Netzlaufwerk via `net use : http://127.0.0.1:// /persistent:no` ein /// bzw. unter Unix via `gio mount dav://127.0.0.1://`. fn run_mount_command(drive_str: &str, port: u16, session_token: &str) -> Result<()> { #[cfg(windows)] { let url = format!("http://127.0.0.1:{}/{}/", port, session_token); let mut output = Command::new("net") .args(["use", drive_str, &url, "/persistent:no"]) .output() .context("Fehler beim Ausführen des Befehls 'net use'")?; // PRR-01: Selbstreparatur bei verwaister Zuordnung nach unsauberem Vorläufer (Systemfehler 85) if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); let is_already_in_use = stderr.contains("85") || stderr.contains("bereits verwendet") || stderr.contains("already in use") || stdout.contains("85") || stdout.contains("bereits verwendet") || stdout.contains("already in use"); if is_already_in_use { debug!("Verwaiste Zuordnung für {} entdeckt — führe automatische Bereinigung durch...", drive_str); let _ = Command::new("net") .args(["use", drive_str, "/delete", "/y"]) .output(); // Zweiter Versuch nach automatischer Bereinigung output = Command::new("net") .args(["use", drive_str, &url, "/persistent:no"]) .output() .context("Fehler beim erneuten Ausführen des Befehls 'net use'")?; } } if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); let webclient_hint = if stderr.contains("67") || stderr.contains("Netzwerkname") || stderr.contains("Systemfehler") { "\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'." } else { "" }; bail!( "Laufwerk {} konnte nicht eingebunden werden:\n{}{}{}", drive_str, stdout, stderr, webclient_hint ); } Ok(()) } #[cfg(unix)] { let dav_url = format!("dav://127.0.0.1:{}/{}/", port, session_token); let _ = Command::new("gio") .args(["mount", &dav_url]) .output(); let _ = drive_str; Ok(()) } #[cfg(not(any(windows, unix)))] { let _ = (drive_str, 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 _ = 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) } }; // 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); let token_path_prefix = format!("/{}", session_token); // 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() .strip_prefix(token_path_prefix.clone()) .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, token_path_prefix.clone(), shutdown_rx, )); // Netzlaufwerk bzw. Verzeichnis einbinden if let Err(e) = run_mount_command(&drive_str, 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))] { println!(" • gio Befehl: gio mount dav://127.0.0.1:{}//", bound_port); if let Some(mp) = mount_point { println!(" • davfs2: mount -t davfs http://127.0.0.1:{}// {}", bound_port, mp.display()); } } 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") } /// 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); /// Führt die asynchrone WebDAV HTTP-Server-Schleife mit Session-Token & Host-Header Sicherheits-Middleware aus. pub async fn serve_webdav_loop( listener: TcpListener, dav_server: DavHandler, token_path_prefix: 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_prefix = token_path_prefix.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 prefix = expected_prefix.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) .body(dav_server::body::Body::empty()) .unwrap(); return Ok::<_, Infallible>(res); } // 2. Session-Token Pfadprüfung (Loopback-Schutz gegen unbefugte lokale Prozesse & Browser CSRF) let path = req.uri().path(); if !path.starts_with(&prefix) { debug!("Abgewiesener Zugriff ohne gültiges Session-Token: {}", path); let res = hyper::Response::builder() .status(hyper::StatusCode::FORBIDDEN) .body(dav_server::body::Body::empty()) .unwrap(); return Ok::<_, Infallible>(res); } 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 { // Client-Disconnects im Explorer oder Timeout-Drops sind normal 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:"); } }