Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d93b03322e |
No files matched your search
@@ -57,7 +57,7 @@ Die gesamte Genese dieses Projekts (Audit-Behebung aller 39 Findings, Carrier V2
|
|||||||
| [`RELEASE_PROCESS.md`](RELEASE_PROCESS.md) | Multi-Platform CI/CD Release-Architektur, Toolchain-Pins und Minisign-Signierung |
|
| [`RELEASE_PROCESS.md`](RELEASE_PROCESS.md) | Multi-Platform CI/CD Release-Architektur, Toolchain-Pins und Minisign-Signierung |
|
||||||
| [`QUICKSTART.md`](QUICKSTART.md) | Kompakte Schritt-für-Schritt-Anleitung für Endanwender |
|
| [`QUICKSTART.md`](QUICKSTART.md) | Kompakte Schritt-für-Schritt-Anleitung für Endanwender |
|
||||||
| [`INSTALL.md`](INSTALL.md) | Installationsanweisungen (Binaries, Scoop, WinGet, Cargo) |
|
| [`INSTALL.md`](INSTALL.md) | Installationsanweisungen (Binaries, Scoop, WinGet, Cargo) |
|
||||||
| [`CHANGELOG.md`](CHANGELOG.md) | Keep-a-Changelog Versionshistorie (aktuell: v0.9.5) |
|
| [`CHANGELOG.md`](CHANGELOG.md) | Keep-a-Changelog Versionshistorie (aktuell: v0.9.6) |
|
||||||
| [`deny.toml`](deny.toml) | Strikte Konfiguration für `cargo-deny` (Advisories, Bans, Lizenzen, Quellen) |
|
| [`deny.toml`](deny.toml) | Strikte Konfiguration für `cargo-deny` (Advisories, Bans, Lizenzen, Quellen) |
|
||||||
| [`.gitea/workflows/release.yaml`](.gitea/workflows/release.yaml) | Automatisierte CI/CD Release-Pipeline für Windows & Linux musl |
|
| [`.gitea/workflows/release.yaml`](.gitea/workflows/release.yaml) | Automatisierte CI/CD Release-Pipeline für Windows & Linux musl |
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,23 @@ Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokument
|
|||||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
|
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/)
|
||||||
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
|
und dieses Projekt folgt den Richtlinien von [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
## [0.9.6] - 2026-10-21
|
||||||
|
|
||||||
|
### Windows Cloud Files Engine (`cldapi.dll`) & Userland-Fallback ohne `WebClient`
|
||||||
|
Dieses Release macht `sanctum mount` unter Windows unabhängig vom Windows-Dienst `WebClient` (`mrxdav.sys`), sodass Container auch dann als regulärer Laufwerksbuchstabe (z. B. `S:` oder `Z:`) im Windows Explorer eingebunden werden können, wenn `WebClient` durch Gruppenrichtlinien oder die Systemadministration deaktiviert (`Disabled`) wurde – komplett im Userland und ohne Administratorrechte:
|
||||||
|
- **Native Windows Cloud Files API (`src/cloudfiles.rs`)**:
|
||||||
|
- Verbindet `SanctumFs` direkt mit der in Windows 10/11 integrierten Cloud-Filter-Engine (`cldapi.dll` / `cldflt.sys`) und weist das Staging-Verzeichnis via `DefineDosDeviceW` (`DDD_NO_BROADCAST_SYSTEM` + `SHChangeNotify`) dem gewünschten oder automatisch gewählten freien Laufwerksbuchstaben (`--drive` bzw. `Z:` abwärts) zu.
|
||||||
|
- **0-Byte Placeholders**: Beim Einbinden werden mittels `CfCreatePlaceholders` ausschließlich 0-Byte-Platzhalterdateien angelegt; es befindet sich beim Start kein entschlüsselter Dateiinhalt auf dem Host-Datenträger.
|
||||||
|
- **On-Demand Hydration (`CF_CALLBACK_TYPE_FETCH_DATA`)**: Erst beim Öffnen/Lesen einer Datei entschlüsselt Sanctum die angeforderten 1-MB-Chunks live aus `SanctumFs` (regulärer Tresor sowie versteckter `CarrierFs`-Tresor), überträgt sie via `CfExecute` an den Kernel und nullt den temporären Puffer (`Zeroizing`).
|
||||||
|
- **Bidirektionale Synchronisation, Auto-Dehydration & CSPRNG-Shredding**:
|
||||||
|
- Neue, geänderte oder gelöschte Dateien und Ordner auf dem Laufwerk werden unter Einhaltung des **Anti-Leak Shields** und des **Carrier-Schutzes** laufend in den verschlüsselten Container zurückgeschrieben.
|
||||||
|
- Nicht mehr genutzte hydrierte Dateien werden nach Inaktivität automatisch via `CfDehydratePlaceholder` wieder auf 0 Bytes dehydriert.
|
||||||
|
- Beim Aushängen (`unmount`, `Ctrl+C`, Sitzungssperre oder Idle-Timeout) werden etwaige auf Disk liegende Dateibytes mit kryptografischem Zufallsrauschen (`OsRng`) überschrieben (geshreddet) und das Staging-Verzeichnis samt Laufwerksbuchstaben restlos entfernt.
|
||||||
|
- **Dreistufiges Mount-Backend (`--backend auto|cloudfiles|webdav`) & ETW-Service-Trigger**:
|
||||||
|
- `auto` (Standard): Prüft ohne Administratorrechte den Status des Dienstes `WebClient`. Ist er lediglich gestoppt (`Manual`), weckt Sanctum ihn automatisch per Userland-ETW-Trigger (`22b6d684-fa63-4578-87c9-effcbe6643c7`) auf. Ist `WebClient` administrativ deaktiviert (`Disabled`) oder schlägt `WNetAddConnection2W` fehl, wechselt Sanctum nahtlos auf die `CloudFilesSession` auf demselben Laufwerksbuchstaben.
|
||||||
|
- `cloudfiles`: Erzwingt das direkte Einbinden über `cldapi.dll` + `DefineDosDeviceW` ohne `WebClient` (z. B. auch zur Umgehung des 4-GB-Limits von `WebClient`).
|
||||||
|
- `webdav`: Erzwingt das klassische Einbinden über `WNetAddConnection2W`.
|
||||||
|
|
||||||
## [0.9.5] - 2026-09-22
|
## [0.9.5] - 2026-09-22
|
||||||
|
|
||||||
### Transcript-Version & Upgrade-MAC (F-04, F-05)
|
### Transcript-Version & Upgrade-MAC (F-04, F-05)
|
||||||
|
|||||||
Generated
+1
-1
@@ -1457,7 +1457,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.9.5"
|
version = "0.9.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sanctum"
|
name = "sanctum"
|
||||||
version = "0.9.5"
|
version = "0.9.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
authors = ["Harald Pansi <harald@pansi.eu>", "Sanctum Engineering Team"]
|
||||||
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
description = "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV"
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ sanctum.exe mount --path "C:\Pfad\tresor.sanctum"
|
|||||||
# Bestimmten Laufwerksbuchstaben erzwingen:
|
# Bestimmten Laufwerksbuchstaben erzwingen:
|
||||||
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --drive S
|
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --drive S
|
||||||
|
|
||||||
|
# Mount-Backend wählen (auto | cloudfiles | webdav; Standard: auto mit Cloud-Files-Fallback falls WebClient deaktiviert ist):
|
||||||
|
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --backend cloudfiles
|
||||||
|
|
||||||
# Mit Inaktivitäts-Timeout (in Sekunden):
|
# Mit Inaktivitäts-Timeout (in Sekunden):
|
||||||
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --idle-timeout 300
|
sanctum.exe mount --path "C:\Pfad\tresor.sanctum" --idle-timeout 300
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ Sicherheitsupdates und Patches werden jeweils für die neueste Version von Sanct
|
|||||||
|
|
||||||
| Version | Status | Sicherheits-Support |
|
| Version | Status | Sicherheits-Support |
|
||||||
|:---:|:---:|:---:|
|
|:---:|:---:|:---:|
|
||||||
| **v0.9.x** | **Aktiv (Aktuell: v0.9.5)** | **Vollständig unterstützt** |
|
| **v0.9.x** | **Aktiv (Aktuell: v0.9.6)** | **Vollständig unterstützt** |
|
||||||
| <= v0.8.x | Veraltet | Nicht mehr unterstützt (Upgrade empfohlen) |
|
| <= v0.8.x | Veraltet | Nicht mehr unterstützt (Upgrade empfohlen) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": "0.9.5",
|
"version": "0.9.6",
|
||||||
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
|
"description": "Verschlüsselter Ein-Datei-Container unter Windows im reinen Userland via WebDAV",
|
||||||
"homepage": "https://gitea.pansi.eu/harald/sanctum",
|
"homepage": "https://gitea.pansi.eu/harald/sanctum",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"architecture": {
|
"architecture": {
|
||||||
"64bit": {
|
"64bit": {
|
||||||
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.5/sanctum-v0.9.5-windows-x86_64.zip",
|
"url": "https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.6/sanctum-v0.9.6-windows-x86_64.zip",
|
||||||
"hash": "f47d43e455a73d6ed0affa7af07437b82c7f61188aade9b2a46cbb4ea43968fc",
|
"hash": "f5236b24d0c8c645364e1c5d3ca9c503518646c879163cedabe160029db9a099",
|
||||||
"bin": "sanctum.exe"
|
"bin": "sanctum.exe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
|
# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.6.0.schema.json
|
||||||
PackageIdentifier: HaraldPansi.Sanctum
|
PackageIdentifier: HaraldPansi.Sanctum
|
||||||
PackageVersion: 0.9.5
|
PackageVersion: 0.9.6
|
||||||
PackageName: Sanctum
|
PackageName: Sanctum
|
||||||
Publisher: Harald Pansi
|
Publisher: Harald Pansi
|
||||||
PublisherUrl: https://gitea.pansi.eu/harald
|
PublisherUrl: https://gitea.pansi.eu/harald
|
||||||
@@ -18,7 +18,7 @@ Tags:
|
|||||||
- webdav
|
- webdav
|
||||||
- container
|
- container
|
||||||
- plausible-deniability
|
- plausible-deniability
|
||||||
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.9.5
|
ReleaseNotesUrl: https://gitea.pansi.eu/harald/sanctum/releases/tag/v0.9.6
|
||||||
Installers:
|
Installers:
|
||||||
- Architecture: x64
|
- Architecture: x64
|
||||||
InstallerType: zip
|
InstallerType: zip
|
||||||
@@ -26,7 +26,7 @@ Installers:
|
|||||||
NestedInstallerFiles:
|
NestedInstallerFiles:
|
||||||
- RelativeFilePath: sanctum.exe
|
- RelativeFilePath: sanctum.exe
|
||||||
PortableCommandAlias: sanctum
|
PortableCommandAlias: sanctum
|
||||||
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.5/sanctum-v0.9.5-windows-x86_64.zip
|
InstallerUrl: https://gitea.pansi.eu/harald/sanctum/releases/download/v0.9.6/sanctum-v0.9.6-windows-x86_64.zip
|
||||||
InstallerSha256: f47d43e455a73d6ed0affa7af07437b82c7f61188aade9b2a46cbb4ea43968fc
|
InstallerSha256: f5236b24d0c8c645364e1c5d3ca9c503518646c879163cedabe160029db9a099
|
||||||
ManifestType: singleton
|
ManifestType: singleton
|
||||||
ManifestVersion: 1.6.0
|
ManifestVersion: 1.6.0
|
||||||
+1767
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
|||||||
pub mod carrier;
|
pub mod carrier;
|
||||||
|
pub mod cloudfiles;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod mount;
|
pub mod mount;
|
||||||
pub mod pathutil;
|
pub mod pathutil;
|
||||||
|
|||||||
+11
-2
@@ -10,7 +10,9 @@ use sanctum::crypto::{
|
|||||||
mnemonic_to_dek, set_allow_legacy_names, validate_password, wrap_slot0_payload,
|
mnemonic_to_dek, set_allow_legacy_names, validate_password, wrap_slot0_payload,
|
||||||
wrap_slot1_payload, KdfParams, FORMAT_VERSION, FORMAT_VERSION_V3,
|
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::{
|
use sanctum::recovery::{
|
||||||
detect_recovery_key_slot, export_header_backup, restore_header_backup,
|
detect_recovery_key_slot, export_header_backup, restore_header_backup,
|
||||||
restore_slot_from_recovery_key,
|
restore_slot_from_recovery_key,
|
||||||
@@ -121,6 +123,10 @@ enum Commands {
|
|||||||
/// Autorisiert den Neuaufbau des Metadaten-MAC nach einem Header-Restore (F-02)
|
/// Autorisiert den Neuaufbau des Metadaten-MAC nach einem Header-Restore (F-02)
|
||||||
#[arg(long, default_value_t = false)]
|
#[arg(long, default_value_t = false)]
|
||||||
rebuild_mac: bool,
|
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
|
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
||||||
@@ -1648,7 +1654,9 @@ async fn run() -> Result<()> {
|
|||||||
anti_leak_list,
|
anti_leak_list,
|
||||||
stealth,
|
stealth,
|
||||||
rebuild_mac,
|
rebuild_mac,
|
||||||
|
backend,
|
||||||
} => {
|
} => {
|
||||||
|
let mount_backend = MountBackend::from_str_opt(&backend)?;
|
||||||
let drive_char = match drive {
|
let drive_char = match drive {
|
||||||
Some(ref d) => parse_drive_letter(d)?,
|
Some(ref d) => parse_drive_letter(d)?,
|
||||||
None => {
|
None => {
|
||||||
@@ -1688,7 +1696,7 @@ async fn run() -> Result<()> {
|
|||||||
let enable_tray = !no_tray && !stealth;
|
let enable_tray = !no_tray && !stealth;
|
||||||
let lock_on_screen_lock = !no_screen_lock;
|
let lock_on_screen_lock = !no_screen_lock;
|
||||||
let anti_leak = !no_anti_leak;
|
let anti_leak = !no_anti_leak;
|
||||||
mount_container(
|
mount_container_with_backend(
|
||||||
&path,
|
&path,
|
||||||
drive_char,
|
drive_char,
|
||||||
mount_point.as_deref(),
|
mount_point.as_deref(),
|
||||||
@@ -1702,6 +1710,7 @@ async fn run() -> Result<()> {
|
|||||||
anti_leak_list.as_deref(),
|
anti_leak_list.as_deref(),
|
||||||
stealth,
|
stealth,
|
||||||
rebuild_mac,
|
rebuild_mac,
|
||||||
|
mount_backend,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|||||||
+218
-33
@@ -29,6 +29,32 @@ pub enum ContainerAuth {
|
|||||||
RecoveryKey(Zeroizing<String>),
|
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:")
|
/// Hilfsfunktion zur Formatierung des Laufwerksbuchstabens (z. B. 'S' -> "S:")
|
||||||
pub fn format_drive(drive_letter: char) -> String {
|
pub fn format_drive(drive_letter: char) -> String {
|
||||||
format!("{}:", drive_letter.to_ascii_uppercase())
|
format!("{}:", drive_letter.to_ascii_uppercase())
|
||||||
@@ -88,6 +114,42 @@ pub async fn mount_container(
|
|||||||
anti_leak_list: Option<&Path>,
|
anti_leak_list: Option<&Path>,
|
||||||
stealth: bool,
|
stealth: bool,
|
||||||
rebuild_mac: 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<()> {
|
) -> Result<()> {
|
||||||
let drive_str = format_drive(drive_letter);
|
let drive_str = format_drive(drive_letter);
|
||||||
let _ = &drive_str;
|
let _ = &drive_str;
|
||||||
@@ -95,6 +157,7 @@ pub async fn mount_container(
|
|||||||
let _ = open_explorer;
|
let _ = open_explorer;
|
||||||
let _ = enable_tray;
|
let _ = enable_tray;
|
||||||
let _ = lock_on_screen_lock;
|
let _ = lock_on_screen_lock;
|
||||||
|
let _ = backend;
|
||||||
|
|
||||||
if !container_path.exists() {
|
if !container_path.exists() {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -346,15 +409,19 @@ pub async fn mount_container(
|
|||||||
|
|
||||||
if !stealth {
|
if !stealth {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
ui::step(
|
{
|
||||||
4,
|
let step_msg = match backend {
|
||||||
4,
|
MountBackend::CloudFiles => format!(
|
||||||
"🌐",
|
"Initialisiere Cloud-Files Userland-Engine (cldapi.dll) & binde Laufwerk {} ein...",
|
||||||
&format!(
|
drive_str
|
||||||
"Starte WebDAV (Port {}) & binde Netzlaufwerk {} ein...",
|
),
|
||||||
bound_port, drive_str
|
_ => format!(
|
||||||
),
|
"Starte WebDAV (Port {}) & binde Laufwerk {} ein...",
|
||||||
);
|
bound_port, drive_str
|
||||||
|
),
|
||||||
|
};
|
||||||
|
ui::step(4, 4, "🌐", &step_msg);
|
||||||
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
ui::step(
|
ui::step(
|
||||||
4,
|
4,
|
||||||
@@ -377,13 +444,80 @@ pub async fn mount_container(
|
|||||||
shutdown_rx,
|
shutdown_rx,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Netzlaufwerk bzw. Verzeichnis einbinden
|
// Netzlaufwerk bzw. Cloud-Files-Laufwerk einbinden
|
||||||
if let Err(e) = run_mount_command(drive_letter, bound_port, &session_token) {
|
let mut cf_session: Option<crate::cloudfiles::CloudFilesSession> = None;
|
||||||
let _ = fs_clone.sync_carrier_manifest();
|
#[cfg(windows)]
|
||||||
let _ = shutdown_tx.send(true);
|
{
|
||||||
let _ = server_handle.await;
|
match backend {
|
||||||
return Err(e);
|
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)
|
// Optional automatisch im Windows Explorer öffnen (visuelle Parität für Decoy und Hidden Vault)
|
||||||
#[cfg(windows)]
|
#[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)
|
// Windows-Sitzungssperre (Win + L Auto-Lock, nur Windows)
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
|
let (session_lock_tx, mut session_lock_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||||
@@ -479,10 +631,19 @@ pub async fn mount_container(
|
|||||||
println!();
|
println!();
|
||||||
println!(" • Container: {}", container_path.display());
|
println!(" • Container: {}", container_path.display());
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
println!(
|
{
|
||||||
" • Netzlaufwerk: {} (im Windows Explorer bereit)",
|
if using_cloudfiles {
|
||||||
ui::cyan(&drive_str)
|
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))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
if let Some(mp) = mount_point {
|
if let Some(mp) = mount_point {
|
||||||
@@ -569,7 +730,7 @@ pub async fn mount_container(
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()).ok();
|
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)]
|
#[cfg(windows)]
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
res = tokio::signal::ctrl_c() => {
|
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("[!]"));
|
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)]
|
#[cfg(unix)]
|
||||||
@@ -662,8 +829,12 @@ pub async fn mount_container(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if stealth {
|
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 _ = fs_clone.sync_carrier_manifest();
|
||||||
let _ = unmount_drive(drive_letter);
|
|
||||||
let _ = shutdown_tx.send(true);
|
let _ = shutdown_tx.send(true);
|
||||||
let _ = server_handle.await;
|
let _ = server_handle.await;
|
||||||
if let Ok(freelist) = db.freelist_count() {
|
if let Ok(freelist) = db.freelist_count() {
|
||||||
@@ -677,23 +848,37 @@ pub async fn mount_container(
|
|||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
|
println!("Sanctum: Container geschlossen und WebDAV-Server beendet.");
|
||||||
} else {
|
} else {
|
||||||
let _ = fs_clone.sync_carrier_manifest();
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
print!(
|
if let Some(mut cf) = cf_session.take() {
|
||||||
" {} Trenne Windows-Netzlaufwerk {} ... ",
|
print!(
|
||||||
ui::dim("[-]"),
|
" {} Synchronisiere & schreddere Cloud-Files-Staging für {} ... ",
|
||||||
drive_str
|
ui::dim("[-]"),
|
||||||
);
|
drive_str
|
||||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
);
|
||||||
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||||
// Automatisches Unmount
|
if let Err(e) = cf.shutdown().await {
|
||||||
if let Err(e) = unmount_drive(drive_letter) {
|
println!("{}", ui::yellow(&format!("Warnung ({e})")));
|
||||||
println!("{}", ui::yellow(&format!("Warnung ({e})")));
|
} else {
|
||||||
|
println!("{}", ui::green("OK"));
|
||||||
|
}
|
||||||
} else {
|
} 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))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
print!(" {} Beende WebDAV-Server ... ", ui::dim("[-]"));
|
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();
|
drive_str.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
unsafe {
|
unsafe {
|
||||||
WNetCancelConnection2W(wide_drive.as_ptr(), 0, 1);
|
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).
|
/// Bindet ein Windows-Netzlaufwerk über WNetAddConnection2W ein (in-process, ohne argv-Token-Leak).
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> Result<()> {
|
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 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 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);
|
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 {
|
if res != 0 {
|
||||||
let webclient_hint = if res == 67 {
|
let webclient_hint = if res == 67 || res == 1244 {
|
||||||
"\n\nHinweis: Das Einbinden von Netzlaufwerken erfordert den Windows-Dienst 'WebClient'. Prüfen Sie in einer Administrator-Konsole: 'net start WebClient'."
|
"\n\nHinweis: Das Einbinden von WebDAV-Netzlaufwerken erfordert den Windows-Dienst 'WebClient'."
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
bail!(
|
bail!(
|
||||||
"Laufwerk {} konnte nicht eingebunden werden (Win32 Fehlercode {}){}",
|
"Laufwerk {} konnte nicht über WebClient eingebunden werden (Win32 Fehlercode {}){}",
|
||||||
drive_str,
|
drive_str,
|
||||||
res,
|
res,
|
||||||
webclient_hint
|
webclient_hint
|
||||||
@@ -787,7 +1121,7 @@ pub fn mount_drive_wnet(drive_letter: char, port: u16, session_token: &str) -> R
|
|||||||
Ok(())
|
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)]
|
#[cfg(windows)]
|
||||||
pub fn unmount_drive_wnet(drive_letter: char) -> Result<()> {
|
pub fn unmount_drive_wnet(drive_letter: char) -> Result<()> {
|
||||||
let drive_str = format!("{}:", drive_letter.to_ascii_uppercase());
|
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 */)
|
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)
|
// 0 = NO_ERROR, 2250 = ERROR_NOT_CONNECTED (bereits getrennt)
|
||||||
if res != 0 && res != 2250 {
|
if res != 0 && res != 2250 {
|
||||||
bail!(
|
bail!(
|
||||||
|
|||||||
Reference in new issue
Block a user