feat(mount): add native Windows Cloud Files Engine (cldapi.dll + DefineDosDeviceW) fallback without WebClient
This commit is contained in:
1 parent
76c23dbfa3
commit
d93b03322e
13 files changed
+2371
-50
No files matched your search
+1767
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
||||
pub mod carrier;
|
||||
pub mod cloudfiles;
|
||||
pub mod crypto;
|
||||
pub mod mount;
|
||||
pub mod pathutil;
|
||||
|
||||
+11
-2
@@ -10,7 +10,9 @@ use sanctum::crypto::{
|
||||
mnemonic_to_dek, set_allow_legacy_names, validate_password, wrap_slot0_payload,
|
||||
wrap_slot1_payload, KdfParams, FORMAT_VERSION, FORMAT_VERSION_V3,
|
||||
};
|
||||
use sanctum::mount::{format_drive, mount_container, unmount_drive, ContainerAuth};
|
||||
use sanctum::mount::{
|
||||
format_drive, mount_container_with_backend, unmount_drive, ContainerAuth, MountBackend,
|
||||
};
|
||||
use sanctum::recovery::{
|
||||
detect_recovery_key_slot, export_header_backup, restore_header_backup,
|
||||
restore_slot_from_recovery_key,
|
||||
@@ -121,6 +123,10 @@ enum Commands {
|
||||
/// Autorisiert den Neuaufbau des Metadaten-MAC nach einem Header-Restore (F-02)
|
||||
#[arg(long, default_value_t = false)]
|
||||
rebuild_mac: bool,
|
||||
|
||||
/// Mount-Backend unter Windows: 'auto' (WebClient mit nahtlosem Fallback auf Cloud-Files), 'cloudfiles' (cldapi.dll ohne WebClient) oder 'webdav'
|
||||
#[arg(long, default_value = "auto", value_name = "MODE")]
|
||||
backend: String,
|
||||
},
|
||||
|
||||
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
||||
@@ -1648,7 +1654,9 @@ async fn run() -> Result<()> {
|
||||
anti_leak_list,
|
||||
stealth,
|
||||
rebuild_mac,
|
||||
backend,
|
||||
} => {
|
||||
let mount_backend = MountBackend::from_str_opt(&backend)?;
|
||||
let drive_char = match drive {
|
||||
Some(ref d) => parse_drive_letter(d)?,
|
||||
None => {
|
||||
@@ -1688,7 +1696,7 @@ async fn run() -> Result<()> {
|
||||
let enable_tray = !no_tray && !stealth;
|
||||
let lock_on_screen_lock = !no_screen_lock;
|
||||
let anti_leak = !no_anti_leak;
|
||||
mount_container(
|
||||
mount_container_with_backend(
|
||||
&path,
|
||||
drive_char,
|
||||
mount_point.as_deref(),
|
||||
@@ -1702,6 +1710,7 @@ async fn run() -> Result<()> {
|
||||
anti_leak_list.as_deref(),
|
||||
stealth,
|
||||
rebuild_mac,
|
||||
mount_backend,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
+218
-33
@@ -29,6 +29,32 @@ pub enum ContainerAuth {
|
||||
RecoveryKey(Zeroizing<String>),
|
||||
}
|
||||
|
||||
/// Auswahl des Mount-Backends unter Windows.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MountBackend {
|
||||
/// Versucht zuerst WebClient (inkl. Userland-ETW-Wakeup); falls deaktiviert,
|
||||
/// übernimmt nahtlos die Windows Cloud-Files-Engine (`cldapi.dll` + `DefineDosDeviceW`).
|
||||
Auto,
|
||||
/// Nutzt direkt die Windows Cloud-Files-Engine (`cldapi.dll` + `DefineDosDeviceW`) ohne WebClient.
|
||||
CloudFiles,
|
||||
/// Nutzt ausschließlich das klassische WebDAV-Netzlaufwerk (`WNetAddConnection2W`).
|
||||
WebDav,
|
||||
}
|
||||
|
||||
impl MountBackend {
|
||||
pub fn from_str_opt(s: &str) -> Result<Self> {
|
||||
match s.trim().to_ascii_lowercase().as_str() {
|
||||
"auto" => Ok(Self::Auto),
|
||||
"cloudfiles" | "cfapi" | "cldapi" | "cloud" => Ok(Self::CloudFiles),
|
||||
"webdav" | "wnet" | "dav" => Ok(Self::WebDav),
|
||||
other => bail!(
|
||||
"Unbekanntes Mount-Backend '{}'. Erlaubte Werte: auto, cloudfiles, webdav",
|
||||
other
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:")
|
||||
pub fn format_drive(drive_letter: char) -> String {
|
||||
format!("{}:", drive_letter.to_ascii_uppercase())
|
||||
@@ -88,6 +114,42 @@ pub async fn mount_container(
|
||||
anti_leak_list: Option<&Path>,
|
||||
stealth: bool,
|
||||
rebuild_mac: bool,
|
||||
) -> Result<()> {
|
||||
mount_container_with_backend(
|
||||
container_path,
|
||||
drive_letter,
|
||||
mount_point,
|
||||
requested_port,
|
||||
auth,
|
||||
open_explorer,
|
||||
enable_tray,
|
||||
idle_timeout,
|
||||
lock_on_screen_lock,
|
||||
anti_leak,
|
||||
anti_leak_list,
|
||||
stealth,
|
||||
rebuild_mac,
|
||||
MountBackend::Auto,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Startet das Sanctum-VFS mit expliziter Auswahl des Mount-Backends (`Auto`, `CloudFiles`, `WebDav`).
|
||||
pub async fn mount_container_with_backend(
|
||||
container_path: &Path,
|
||||
drive_letter: char,
|
||||
mount_point: Option<&Path>,
|
||||
requested_port: Option<u16>,
|
||||
auth: ContainerAuth,
|
||||
open_explorer: bool,
|
||||
enable_tray: bool,
|
||||
idle_timeout: Option<u64>,
|
||||
lock_on_screen_lock: bool,
|
||||
anti_leak: bool,
|
||||
anti_leak_list: Option<&Path>,
|
||||
stealth: bool,
|
||||
rebuild_mac: bool,
|
||||
backend: MountBackend,
|
||||
) -> Result<()> {
|
||||
let drive_str = format_drive(drive_letter);
|
||||
let _ = &drive_str;
|
||||
@@ -95,6 +157,7 @@ pub async fn mount_container(
|
||||
let _ = open_explorer;
|
||||
let _ = enable_tray;
|
||||
let _ = lock_on_screen_lock;
|
||||
let _ = backend;
|
||||
|
||||
if !container_path.exists() {
|
||||
bail!(
|
||||
@@ -346,15 +409,19 @@ pub async fn mount_container(
|
||||
|
||||
if !stealth {
|
||||
#[cfg(windows)]
|
||||
ui::step(
|
||||
4,
|
||||
4,
|
||||
"🌐",
|
||||
&format!(
|
||||
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
|
||||
bound_port, drive_str
|
||||
),
|
||||
);
|
||||
{
|
||||
let step_msg = match backend {
|
||||
MountBackend::CloudFiles => format!(
|
||||
"Initialisiere Cloud-Files Userland-Engine (cldapi.dll) & binde Laufwerk {} ein...",
|
||||
drive_str
|
||||
),
|
||||
_ => format!(
|
||||
"Starte WebDAV (Port {}) & binde Laufwerk {} ein...",
|
||||
bound_port, drive_str
|
||||
),
|
||||
};
|
||||
ui::step(4, 4, "🌐", &step_msg);
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
ui::step(
|
||||
4,
|
||||
@@ -377,13 +444,80 @@ pub async fn mount_container(
|
||||
shutdown_rx,
|
||||
));
|
||||
|
||||
// Netzlaufwerk bzw. Verzeichnis einbinden
|
||||
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
|
||||
let _ = fs_clone.sync_carrier_manifest();
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
return Err(e);
|
||||
// Netzlaufwerk bzw. Cloud-Files-Laufwerk einbinden
|
||||
let mut cf_session: Option<crate::cloudfiles::CloudFilesSession> = None;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
match backend {
|
||||
MountBackend::CloudFiles => {
|
||||
match crate::cloudfiles::CloudFilesSession::mount(drive_letter, fs_clone.clone())
|
||||
.await
|
||||
{
|
||||
Ok(s) => cf_session = Some(s),
|
||||
Err(e) => {
|
||||
let _ = fs_clone.sync_carrier_manifest();
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
MountBackend::WebDav => {
|
||||
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
|
||||
let _ = fs_clone.sync_carrier_manifest();
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
MountBackend::Auto => {
|
||||
if let Err(wnet_err) = run_mount_command(drive_letter, bound_port, &session_token) {
|
||||
if !stealth {
|
||||
println!(
|
||||
" {} Windows WebClient nicht verfügbar ({}) — schalte nahtlos auf Userland Cloud-Files-Engine (cldapi.dll) um...",
|
||||
ui::yellow("[i]"),
|
||||
wnet_err
|
||||
.to_string()
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("WebClient deaktiviert")
|
||||
);
|
||||
}
|
||||
match crate::cloudfiles::CloudFilesSession::mount(
|
||||
drive_letter,
|
||||
fs_clone.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(s) => cf_session = Some(s),
|
||||
Err(cf_err) => {
|
||||
let _ = fs_clone.sync_carrier_manifest();
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
bail!(
|
||||
"Weder WebClient ({}) noch Cloud-Files-Engine ({}) konnten Laufwerk {} einbinden.",
|
||||
wnet_err,
|
||||
cf_err,
|
||||
drive_str
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
|
||||
let _ = fs_clone.sync_carrier_manifest();
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
let using_cloudfiles = cf_session.is_some();
|
||||
let _ = using_cloudfiles;
|
||||
|
||||
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
|
||||
#[cfg(windows)]
|
||||
@@ -450,6 +584,24 @@ pub async fn mount_container(
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor für externes `sanctum unmount --drive <LETTER>` bei Cloud-Files DOS-Device
|
||||
#[cfg(windows)]
|
||||
let (ext_unmount_tx, mut ext_unmount_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||
#[cfg(windows)]
|
||||
if using_cloudfiles {
|
||||
let dl = drive_letter;
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(1));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if !crate::windows::is_dos_device_mounted(dl) {
|
||||
let _ = ext_unmount_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);
|
||||
@@ -479,10 +631,19 @@ pub async fn mount_container(
|
||||
println!();
|
||||
println!(" • Container: {}", container_path.display());
|
||||
#[cfg(windows)]
|
||||
println!(
|
||||
" • Netzlaufwerk: {} (im Windows Explorer bereit)",
|
||||
ui::cyan(&drive_str)
|
||||
);
|
||||
{
|
||||
if using_cloudfiles {
|
||||
println!(
|
||||
" • Laufwerk: {} (Cloud-Files Userland-Engine cldapi.dll, ohne WebClient)",
|
||||
ui::cyan(&drive_str)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" • Netzlaufwerk: {} (im Windows Explorer bereit)",
|
||||
ui::cyan(&drive_str)
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
if let Some(mp) = mount_point {
|
||||
@@ -569,7 +730,7 @@ pub async fn mount_container(
|
||||
#[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)
|
||||
// Warten auf Beendigungssignal (Ctrl+C, Tray-Klick, Inaktivität, Win+L, Konsolenfenster-Schließen, externes Unmount)
|
||||
#[cfg(windows)]
|
||||
tokio::select! {
|
||||
res = tokio::signal::ctrl_c() => {
|
||||
@@ -603,6 +764,12 @@ pub async fn mount_container(
|
||||
println!(" {} Inaktivitäts-Timeout erreicht — Auto-Lock ausgelöst!", ui::yellow("[!]"));
|
||||
}
|
||||
}
|
||||
Some(()) = ext_unmount_rx.recv() => {
|
||||
if !stealth {
|
||||
println!();
|
||||
println!(" {} Externer Unmount-Befehl für {} erkannt — schließe Container sicher ab!", ui::yellow("[!]"), drive_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -662,8 +829,12 @@ pub async fn mount_container(
|
||||
}
|
||||
|
||||
if stealth {
|
||||
if let Some(mut cf) = cf_session.take() {
|
||||
let _ = cf.shutdown().await;
|
||||
} else {
|
||||
let _ = unmount_drive(drive_letter);
|
||||
}
|
||||
let _ = fs_clone.sync_carrier_manifest();
|
||||
let _ = unmount_drive(drive_letter);
|
||||
let _ = shutdown_tx.send(true);
|
||||
let _ = server_handle.await;
|
||||
if let Ok(freelist) = db.freelist_count() {
|
||||
@@ -677,23 +848,37 @@ pub async fn mount_container(
|
||||
#[cfg(not(windows))]
|
||||
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
|
||||
} else {
|
||||
let _ = fs_clone.sync_carrier_manifest();
|
||||
#[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})")));
|
||||
if let Some(mut cf) = cf_session.take() {
|
||||
print!(
|
||||
" {} Synchronisiere & schreddere Cloud-Files-Staging für {} ... ",
|
||||
ui::dim("[-]"),
|
||||
drive_str
|
||||
);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
if let Err(e) = cf.shutdown().await {
|
||||
println!("{}", ui::yellow(&format!("Warnung ({e})")));
|
||||
} else {
|
||||
println!("{}", ui::green("OK"));
|
||||
}
|
||||
} else {
|
||||
println!("{}", ui::green("OK"));
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = fs_clone.sync_carrier_manifest();
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
print!(" {} Beende WebDAV-Server ... ", ui::dim("[-]"));
|
||||
|
||||
+343
-4
@@ -581,6 +581,11 @@ unsafe extern "system" fn console_ctrl_routine(ctrl_type: u32) -> i32 {
|
||||
drive_str.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
unsafe {
|
||||
WNetCancelConnection2W(wide_drive.as_ptr(), 0, 1);
|
||||
DefineDosDeviceW(
|
||||
0x0000_0002, /* DDD_REMOVE_DEFINITION */
|
||||
wide_drive.as_ptr(),
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -722,9 +727,338 @@ pub fn unlock_memory(ptr: *const u8, len: usize) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
extern "system" {
|
||||
fn DefineDosDeviceW(
|
||||
dw_flags: u32,
|
||||
lp_device_name: *const u16,
|
||||
lp_target_path: *const u16,
|
||||
) -> i32;
|
||||
fn QueryDosDeviceW(lp_device_name: *const u16, lp_target_path: *mut u16, ucch_max: u32) -> u32;
|
||||
}
|
||||
|
||||
/// Prüft ohne Administratorrechte über den Windows Service Control Manager,
|
||||
/// ob der Dienst `WebClient` deaktiviert (`SERVICE_DISABLED = 4`) oder bereits aktiv ist.
|
||||
/// Rückgabe: `(is_running, is_disabled)`
|
||||
#[cfg(windows)]
|
||||
pub fn query_webclient_service_state() -> (bool, bool) {
|
||||
#[repr(C)]
|
||||
struct SERVICE_STATUS {
|
||||
dw_service_type: u32,
|
||||
dw_current_state: u32,
|
||||
dw_controls_accepted: u32,
|
||||
dw_win32_exit_code: u32,
|
||||
dw_service_specific_exit_code: u32,
|
||||
dw_check_point: u32,
|
||||
dw_wait_hint: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct QUERY_SERVICE_CONFIGW {
|
||||
dw_service_type: u32,
|
||||
dw_start_type: u32,
|
||||
dw_error_control: u32,
|
||||
lp_binary_path_name: *mut u16,
|
||||
lp_load_order_group: *mut u16,
|
||||
dw_tag_id: u32,
|
||||
lp_dependencies: *mut u16,
|
||||
lp_service_start_name: *mut u16,
|
||||
lp_display_name: *mut u16,
|
||||
}
|
||||
|
||||
#[link(name = "advapi32")]
|
||||
extern "system" {
|
||||
fn OpenSCManagerW(
|
||||
lp_machine_name: *const u16,
|
||||
lp_database_name: *const u16,
|
||||
dw_desired_access: u32,
|
||||
) -> isize;
|
||||
fn OpenServiceW(
|
||||
h_sc_manager: isize,
|
||||
lp_service_name: *const u16,
|
||||
dw_desired_access: u32,
|
||||
) -> isize;
|
||||
fn QueryServiceStatus(h_service: isize, lp_service_status: *mut SERVICE_STATUS) -> i32;
|
||||
fn QueryServiceConfigW(
|
||||
h_service: isize,
|
||||
lp_service_config: *mut QUERY_SERVICE_CONFIGW,
|
||||
cb_buf_size: u32,
|
||||
pcb_bytes_needed: *mut u32,
|
||||
) -> i32;
|
||||
fn CloseServiceHandle(h_sc_object: isize) -> i32;
|
||||
}
|
||||
|
||||
const SC_MANAGER_CONNECT: u32 = 0x0001;
|
||||
const SERVICE_QUERY_CONFIG: u32 = 0x0001;
|
||||
const SERVICE_QUERY_STATUS: u32 = 0x0004;
|
||||
const SERVICE_RUNNING: u32 = 4;
|
||||
const SERVICE_DISABLED: u32 = 4;
|
||||
|
||||
unsafe {
|
||||
let scm = OpenSCManagerW(std::ptr::null(), std::ptr::null(), SC_MANAGER_CONNECT);
|
||||
if scm == 0 {
|
||||
return (false, false);
|
||||
}
|
||||
let svc_name: Vec<u16> = "WebClient\0".encode_utf16().collect();
|
||||
let svc = OpenServiceW(
|
||||
scm,
|
||||
svc_name.as_ptr(),
|
||||
SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG,
|
||||
);
|
||||
if svc == 0 {
|
||||
CloseServiceHandle(scm);
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
let mut status: SERVICE_STATUS = std::mem::zeroed();
|
||||
let is_running =
|
||||
QueryServiceStatus(svc, &mut status) != 0 && status.dw_current_state == SERVICE_RUNNING;
|
||||
|
||||
let mut bytes_needed: u32 = 0;
|
||||
let _ = QueryServiceConfigW(svc, std::ptr::null_mut(), 0, &mut bytes_needed);
|
||||
let mut is_disabled = false;
|
||||
if bytes_needed > 0 {
|
||||
let mut buf = vec![0u8; bytes_needed as usize];
|
||||
let cfg_ptr = buf.as_mut_ptr() as *mut QUERY_SERVICE_CONFIGW;
|
||||
if QueryServiceConfigW(svc, cfg_ptr, bytes_needed, &mut bytes_needed) != 0 {
|
||||
if (*cfg_ptr).dw_start_type == SERVICE_DISABLED {
|
||||
is_disabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloseServiceHandle(svc);
|
||||
CloseServiceHandle(scm);
|
||||
(is_running, is_disabled)
|
||||
}
|
||||
}
|
||||
|
||||
/// Weckt den Windows-Dienst `WebClient` aus dem reinen Userland (ohne Administratorrechte)
|
||||
/// über seinen offiziellen Microsoft ETW-Service-Trigger (`22b6d684-fa63-4578-87c9-effcbe6643c7`) auf.
|
||||
#[cfg(windows)]
|
||||
pub fn trigger_webclient_service_etw() -> bool {
|
||||
#[repr(C)]
|
||||
struct GUID {
|
||||
data1: u32,
|
||||
data2: u16,
|
||||
data3: u16,
|
||||
data4: [u8; 8],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct EVENT_DESCRIPTOR {
|
||||
id: u16,
|
||||
version: u8,
|
||||
channel: u8,
|
||||
level: u8,
|
||||
opcode: u8,
|
||||
task: u16,
|
||||
keyword: u64,
|
||||
}
|
||||
|
||||
#[link(name = "advapi32")]
|
||||
extern "system" {
|
||||
fn EventRegister(
|
||||
provider_id: *const GUID,
|
||||
enable_callback: *const std::ffi::c_void,
|
||||
callback_context: *const std::ffi::c_void,
|
||||
reg_handle: *mut u64,
|
||||
) -> u32;
|
||||
fn EventWrite(
|
||||
reg_handle: u64,
|
||||
event_descriptor: *const EVENT_DESCRIPTOR,
|
||||
user_data_count: u32,
|
||||
user_data: *const std::ffi::c_void,
|
||||
) -> u32;
|
||||
fn EventUnregister(reg_handle: u64) -> u32;
|
||||
}
|
||||
|
||||
let (running, disabled) = query_webclient_service_state();
|
||||
if running {
|
||||
return true;
|
||||
}
|
||||
if disabled {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Offizielle Microsoft ETW-Trigger-GUID für WebClient (MRxDAV): 22b6d684-fa63-4578-87c9-effcbe6643c7
|
||||
let webclient_trigger_guid = GUID {
|
||||
data1: 0x22b6_d684,
|
||||
data2: 0xfa63,
|
||||
data3: 0x4578,
|
||||
data4: [0x87, 0xc9, 0xef, 0xfc, 0xbe, 0x66, 0x43, 0xc7],
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let mut reg_handle: u64 = 0;
|
||||
if EventRegister(
|
||||
&webclient_trigger_guid,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
&mut reg_handle,
|
||||
) == 0
|
||||
{
|
||||
let desc = EVENT_DESCRIPTOR {
|
||||
id: 1,
|
||||
version: 0,
|
||||
channel: 0,
|
||||
level: 0,
|
||||
opcode: 0,
|
||||
task: 0,
|
||||
keyword: 0,
|
||||
};
|
||||
let _ = EventWrite(reg_handle, &desc, 0, std::ptr::null());
|
||||
let _ = EventUnregister(reg_handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Warte kurz (max. 1.5s), bis der SCM den Dienst auf SERVICE_RUNNING geschaltet hat
|
||||
for _ in 0..15 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
let (now_running, _) = query_webclient_service_state();
|
||||
if now_running {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Bindet ein lokales Verzeichnis als Laufwerksbuchstaben im aktuellen Benutzersitzungs-Namensraum
|
||||
/// via `DefineDosDeviceW` ein (erfordert keine Administratorrechte und keinen WebClient-Dienst).
|
||||
#[cfg(windows)]
|
||||
pub fn mount_dos_device(drive_letter: char, target_dir: &std::path::Path) -> Result<()> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
|
||||
let wide_drive: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let wide_target: Vec<u16> = target_dir
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
// Bereinige ggf. verwaiste Zuordnung auf demselben Buchstaben
|
||||
if is_dos_device_mounted(drive_letter) {
|
||||
unsafe {
|
||||
DefineDosDeviceW(0x0000_0002, wide_drive.as_ptr(), std::ptr::null());
|
||||
}
|
||||
}
|
||||
|
||||
let ok = unsafe { DefineDosDeviceW(0, wide_drive.as_ptr(), wide_target.as_ptr()) };
|
||||
if ok == 0 {
|
||||
bail!(
|
||||
"DefineDosDeviceW konnte Laufwerk {} nicht auf '{}' zuweisen",
|
||||
drive_str,
|
||||
target_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
// Benachrichtige die Windows Shell (Explorer) über das neue Laufwerk (SHCNE_DRIVEADD)
|
||||
notify_shell_drive_change(drive_letter, true);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entfernt eine über `DefineDosDeviceW` erstellte Laufwerkszuordnung.
|
||||
#[cfg(windows)]
|
||||
pub fn unmount_dos_device(drive_letter: char, target_dir: Option<&std::path::Path>) -> Result<()> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
const DDD_REMOVE_DEFINITION: u32 = 0x0000_0002;
|
||||
const DDD_EXACT_MATCH_ON_REMOVE: u32 = 0x0000_0004;
|
||||
|
||||
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
|
||||
let wide_drive: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
|
||||
let ok = if let Some(dir) = target_dir {
|
||||
let wide_target: Vec<u16> = dir
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
unsafe {
|
||||
DefineDosDeviceW(
|
||||
DDD_REMOVE_DEFINITION | DDD_EXACT_MATCH_ON_REMOVE,
|
||||
wide_drive.as_ptr(),
|
||||
wide_target.as_ptr(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
unsafe { DefineDosDeviceW(DDD_REMOVE_DEFINITION, wide_drive.as_ptr(), std::ptr::null()) }
|
||||
};
|
||||
|
||||
if ok == 0 {
|
||||
// Fallback ohne exakten Pfad
|
||||
let _ = unsafe {
|
||||
DefineDosDeviceW(DDD_REMOVE_DEFINITION, wide_drive.as_ptr(), std::ptr::null())
|
||||
};
|
||||
}
|
||||
|
||||
notify_shell_drive_change(drive_letter, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prüft, ob ein Laufwerksbuchstabe aktuell als DOS-Device existiert.
|
||||
#[cfg(windows)]
|
||||
pub fn is_dos_device_mounted(drive_letter: char) -> bool {
|
||||
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
|
||||
let wide_drive: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let mut buf = [0u16; 512];
|
||||
let len = unsafe { QueryDosDeviceW(wide_drive.as_ptr(), buf.as_mut_ptr(), buf.len() as u32) };
|
||||
len > 0
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn is_dos_device_mounted(_drive_letter: char) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn notify_shell_drive_change(drive_letter: char, added: bool) {
|
||||
extern "system" {
|
||||
fn SHChangeNotify(
|
||||
w_event_id: i32,
|
||||
u_flags: u32,
|
||||
dw_item1: *const std::ffi::c_void,
|
||||
dw_item2: *const std::ffi::c_void,
|
||||
);
|
||||
}
|
||||
const SHCNE_DRIVEADD: i32 = 0x0000_0100;
|
||||
const SHCNE_DRIVEREMOVED: i32 = 0x0000_0080;
|
||||
const SHCNF_PATHW: u32 = 0x0005;
|
||||
|
||||
let root_str = format!("{}:\\", drive_letter.to_ascii_uppercase());
|
||||
let wide_root: Vec<u16> = root_str.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let event = if added {
|
||||
SHCNE_DRIVEADD
|
||||
} else {
|
||||
SHCNE_DRIVEREMOVED
|
||||
};
|
||||
unsafe {
|
||||
SHChangeNotify(
|
||||
event,
|
||||
SHCNF_PATHW,
|
||||
wide_root.as_ptr() as *const std::ffi::c_void,
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bindet ein Windows-Netzlaufwerk über WNetAddConnection2W ein (in-process, ohne argv-Token-Leak).
|
||||
#[cfg(windows)]
|
||||
pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> Result<()> {
|
||||
// Versuche vorab den WebClient-Dienst per Userland-ETW-Trigger aufzuwecken, falls er nur gestoppt (Manual) ist
|
||||
let (running, disabled) = query_webclient_service_state();
|
||||
if disabled {
|
||||
bail!(
|
||||
"Der Windows-Dienst 'WebClient' wurde durch die Systemadministration deaktiviert (StartType = Disabled)."
|
||||
);
|
||||
}
|
||||
if !running {
|
||||
debug!("WebClient-Dienst ist gestoppt — sende Userland-ETW-Wakeup-Trigger...");
|
||||
let _ = trigger_webclient_service_etw();
|
||||
}
|
||||
|
||||
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
|
||||
let mut local_name: Vec<u16> = drive_str.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let remote_url = format!("http://127.0.0.1:{}/", port);
|
||||
@@ -771,13 +1105,13 @@ pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> R
|
||||
}
|
||||
|
||||
if res != 0 {
|
||||
let webclient_hint = if res == 67 {
|
||||
"\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'."
|
||||
let webclient_hint = if res == 67 || res == 1244 {
|
||||
"\n\nHinweis: Das Einbinden von WebDAV-Netzlaufwerken erfordert den Windows-Dienst 'WebClient'."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
bail!(
|
||||
"Laufwerk {} konnte nicht eingebunden werden (Win32 Fehlercode {}){}",
|
||||
"Laufwerk {} konnte nicht über WebClient eingebunden werden (Win32 Fehlercode {}){}",
|
||||
drive_str,
|
||||
res,
|
||||
webclient_hint
|
||||
@@ -787,7 +1121,7 @@ pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Trennt ein eingebundenes Windows-Netzlaufwerk via WNetCancelConnection2W (in-process).
|
||||
/// Trennt ein eingebundenes Windows-Netzlaufwerk via WNetCancelConnection2W oder Cloud-Files DOS-Device (in-process).
|
||||
#[cfg(windows)]
|
||||
pub fn unmount_drive_wnet(drive_letter: char) -> Result<()> {
|
||||
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
|
||||
@@ -797,6 +1131,11 @@ pub fn unmount_drive_wnet(drive_letter: char) -> Result<()> {
|
||||
WNetCancelConnection2W(local_name.as_ptr(), 0, 1 /* fForce = TRUE */)
|
||||
};
|
||||
|
||||
// Falls kein WNet-Laufwerk verbunden war (2250 = ERROR_NOT_CONNECTED), prüfe auf DefineDosDeviceW (Cloud Files)
|
||||
if res == 2250 && is_dos_device_mounted(drive_letter) {
|
||||
return unmount_dos_device(drive_letter, None);
|
||||
}
|
||||
|
||||
// 0 = NO_ERROR, 2250 = ERROR_NOT_CONNECTED (bereits getrennt)
|
||||
if res != 0 && res != 2250 {
|
||||
bail!(
|
||||
|
||||
Reference in new issue
Block a user