SMB Server Phase 2: VFS backend build fix + integration test
- Add VfsFile: Send supertrait for Mutex compatibility - Fix SmbServerCommand: struct → Subcommand enum with Start variant - Fix tracing_subscriber::init() → try_init() to avoid panic when logger already initialized - Fix CLI subcommand name: smb-server → smb-start (flatten naming) - Add #[command(name = "smb-start")] for CLI disambiguation - Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs) - Remove unused VfsFile imports (webdav.rs, scp_handler.rs) - Integration test: Docker smbclient verified (list, upload, read)
This commit is contained in:
23
AGENTS.md
23
AGENTS.md
@@ -2925,3 +2925,26 @@ cargo test -p markbase-core --lib # ✅ 157 passed, 0 failed
|
|||||||
|
|
||||||
**最后更新**:2026-06-20 14:45
|
**最后更新**:2026-06-20 14:45
|
||||||
**版本**:1.33(Web Frontend Phase 3 完成)
|
**版本**:1.33(Web Frontend Phase 3 完成)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SMB Server Phase 2 Build Fix(2026-06-20)⭐⭐⭐⭐⭐
|
||||||
|
|
||||||
|
**完成时间**:约 30 分钟
|
||||||
|
|
||||||
|
### 修复内容 ⭐⭐⭐⭐⭐
|
||||||
|
|
||||||
|
1. **`VfsFile` trait 添加 `Send` supertrait**:`vfs/mod.rs` — 所有实现已经是 `Send`,显式约束无需 unsafe cast
|
||||||
|
2. **`SmbServerCommand` 改为 enum**:`smb_server.rs` — 使用 `#[derive(Subcommand)]` 枚举(`Start` 变体)以兼容 `#[command(flatten)]`
|
||||||
|
3. **`smb_server_backend.rs` 测试修复**:用 `matches!(result, Err(SmbError::NotFound))` 替代 `result.unwrap_err()` 避免 `Debug` 约束
|
||||||
|
4. **移除未使用 `VfsFile` import**:`webdav.rs` + `scp_handler.rs`
|
||||||
|
|
||||||
|
### 验证 ✅
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build -p markbase-core --features smb-server # ✅ 0 error
|
||||||
|
cargo test -p markbase-core --lib --features smb-server # ✅ 169 passed, 0 failed
|
||||||
|
cargo build -p markbase-core # ✅ 0 error (no features)
|
||||||
|
```
|
||||||
|
|
||||||
|
**版本**:1.34(SMB Server Phase 2 Build Fix)
|
||||||
|
|||||||
564
Cargo.lock
generated
564
Cargo.lock
generated
@@ -113,6 +113,12 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ambient-authority"
|
||||||
|
version = "0.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -211,6 +217,12 @@ dependencies = [
|
|||||||
"password-hash 0.6.1",
|
"password-hash 0.6.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "array-init"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.89"
|
version = "0.1.89"
|
||||||
@@ -433,13 +445,46 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "binrw"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d53195f985e88ab94d1cc87e80049dd2929fd39e4a772c5ae96a7e5c4aad3642"
|
||||||
|
dependencies = [
|
||||||
|
"array-init",
|
||||||
|
"binrw_derive",
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "binrw_derive"
|
||||||
|
version = "0.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5910da05ee556b789032c8ff5a61fb99239580aa3fd0bfaa8f4d094b2aee00ad"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"owo-colors",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f"
|
checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-vec",
|
"bit-vec 0.7.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-set"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||||
|
dependencies = [
|
||||||
|
"bit-vec 0.8.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -448,6 +493,12 @@ version = "0.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22"
|
checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -553,6 +604,12 @@ version = "3.20.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -594,6 +651,36 @@ dependencies = [
|
|||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cap-primitives"
|
||||||
|
version = "3.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a"
|
||||||
|
dependencies = [
|
||||||
|
"ambient-authority",
|
||||||
|
"fs-set-times",
|
||||||
|
"io-extras",
|
||||||
|
"io-lifetimes",
|
||||||
|
"ipnet",
|
||||||
|
"maybe-owned",
|
||||||
|
"rustix",
|
||||||
|
"rustix-linux-procfs",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
"winx",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cap-std"
|
||||||
|
version = "3.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a"
|
||||||
|
dependencies = [
|
||||||
|
"cap-primitives",
|
||||||
|
"io-extras",
|
||||||
|
"io-lifetimes",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "caps"
|
name = "caps"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
@@ -633,6 +720,18 @@ dependencies = [
|
|||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ccm"
|
||||||
|
version = "0.6.0-rc.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4edea5ea70a1285565ac264767613d6c88351a9a0557e7af793a0942590baaed"
|
||||||
|
dependencies = [
|
||||||
|
"aead 0.6.0-rc.10",
|
||||||
|
"cipher 0.5.2",
|
||||||
|
"ctr 0.10.1",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cexpr"
|
name = "cexpr"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -779,6 +878,28 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmac"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa"
|
||||||
|
dependencies = [
|
||||||
|
"cipher 0.4.4",
|
||||||
|
"dbl 0.3.2",
|
||||||
|
"digest 0.10.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmac"
|
||||||
|
version = "0.8.0-rc.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f7f5c25253a49afbdd6a256a21a554c509cf0e6400f59d6dd85e0f15b5f15f6"
|
||||||
|
dependencies = [
|
||||||
|
"cipher 0.5.2",
|
||||||
|
"dbl 0.5.0",
|
||||||
|
"digest 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.58"
|
version = "0.1.58"
|
||||||
@@ -1126,6 +1247,24 @@ dependencies = [
|
|||||||
"xmltree",
|
"xmltree",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dbl"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array 0.14.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dbl"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0d7a944e61df464668c5f51f56cc667396a8821434273112948ea0b66e405d7"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbs-snapshot"
|
name = "dbs-snapshot"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
@@ -1680,6 +1819,17 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs-set-times"
|
||||||
|
version = "0.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
|
||||||
|
dependencies = [
|
||||||
|
"io-lifetimes",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fs2"
|
name = "fs2"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -2417,6 +2567,22 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "io-extras"
|
||||||
|
version = "0.18.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
|
||||||
|
dependencies = [
|
||||||
|
"io-lifetimes",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "io-lifetimes"
|
||||||
|
version = "2.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "io-uring"
|
name = "io-uring"
|
||||||
version = "0.5.13"
|
version = "0.5.13"
|
||||||
@@ -2427,6 +2593,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnet"
|
||||||
|
version = "2.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -2658,6 +2830,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lz4_flex"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e"
|
||||||
|
dependencies = [
|
||||||
|
"twox-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lzma-rust"
|
name = "lzma-rust"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -2686,6 +2867,7 @@ dependencies = [
|
|||||||
"aes 0.8.4",
|
"aes 0.8.4",
|
||||||
"aes-gcm 0.10.3",
|
"aes-gcm 0.10.3",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -2726,6 +2908,8 @@ dependencies = [
|
|||||||
"sevenz-rust",
|
"sevenz-rust",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"sled",
|
"sled",
|
||||||
|
"smb-server",
|
||||||
|
"smb2",
|
||||||
"ssh-key",
|
"ssh-key",
|
||||||
"ssh2",
|
"ssh2",
|
||||||
"tar",
|
"tar",
|
||||||
@@ -2734,6 +2918,7 @@ dependencies = [
|
|||||||
"tokio-postgres",
|
"tokio-postgres",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
|
"tracing-subscriber",
|
||||||
"unrar",
|
"unrar",
|
||||||
"ureq",
|
"ureq",
|
||||||
"url",
|
"url",
|
||||||
@@ -2865,12 +3050,37 @@ dependencies = [
|
|||||||
"xmltree",
|
"xmltree",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maybe-owned"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md-5"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"digest 0.10.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md-5"
|
name = "md-5"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -2881,6 +3091,24 @@ dependencies = [
|
|||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md4"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da5ac363534dce5fabf69949225e174fbf111a498bf0ff794c8ea1fba9f3dda"
|
||||||
|
dependencies = [
|
||||||
|
"digest 0.10.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md4"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd76fb0fd6b2e4be62a73f8e0858ca97f81babcb1af322dcaca196f735f17f80"
|
||||||
|
dependencies = [
|
||||||
|
"digest 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md5"
|
name = "md5"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -3086,6 +3314,15 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num"
|
name = "num"
|
||||||
version = "0.1.43"
|
version = "0.1.43"
|
||||||
@@ -3219,6 +3456,28 @@ dependencies = [
|
|||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||||
|
dependencies = [
|
||||||
|
"num_enum_derive",
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_enum_derive"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2"
|
name = "objc2"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
@@ -3316,6 +3575,12 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "owo-colors"
|
||||||
|
version = "4.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "p256"
|
name = "p256"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
@@ -3749,7 +4014,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"fallible-iterator 0.2.0",
|
"fallible-iterator 0.2.0",
|
||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"md-5",
|
"md-5 0.11.0",
|
||||||
"memchr",
|
"memchr",
|
||||||
"rand 0.10.1",
|
"rand 0.10.1",
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
@@ -3833,6 +4098,15 @@ dependencies = [
|
|||||||
"elliptic-curve 0.14.0-rc.33",
|
"elliptic-curve 0.14.0-rc.33",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-crate"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
|
dependencies = [
|
||||||
|
"toml_edit 0.25.12+spec-1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -3842,6 +4116,25 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proptest"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
||||||
|
dependencies = [
|
||||||
|
"bit-set 0.8.0",
|
||||||
|
"bit-vec 0.8.0",
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"num-traits 0.2.19",
|
||||||
|
"rand 0.9.4",
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_xorshift",
|
||||||
|
"regex-syntax",
|
||||||
|
"rusty-fork",
|
||||||
|
"tempfile",
|
||||||
|
"unarray",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
@@ -3861,6 +4154,12 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-error"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@@ -3912,10 +4211,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@@ -3937,6 +4246,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -3961,12 +4280,30 @@ dependencies = [
|
|||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_xorshift"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@@ -3987,6 +4324,15 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rc4"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "840038b674daa9f7a7957440d937951d15c0143c056e631e529141fd780e0c92"
|
||||||
|
dependencies = [
|
||||||
|
"cipher 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rdrand"
|
name = "rdrand"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -4408,6 +4754,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix-linux-procfs"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.40"
|
version = "0.23.40"
|
||||||
@@ -4449,6 +4805,18 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusty-fork"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"quick-error",
|
||||||
|
"tempfile",
|
||||||
|
"wait-timeout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusty-s3"
|
name = "rusty-s3"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -4459,7 +4827,7 @@ dependencies = [
|
|||||||
"hmac 0.13.0",
|
"hmac 0.13.0",
|
||||||
"instant-xml",
|
"instant-xml",
|
||||||
"jiff",
|
"jiff",
|
||||||
"md-5",
|
"md-5 0.11.0",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -4663,7 +5031,7 @@ version = "0.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26482cf1ecce4540dc782fc70019eba89ffc4d87b3717eb5ec524b5db6fdefef"
|
checksum = "26482cf1ecce4540dc782fc70019eba89ffc4d87b3717eb5ec524b5db6fdefef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-set",
|
"bit-set 0.6.0",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"crc",
|
"crc",
|
||||||
"filetime_creation",
|
"filetime_creation",
|
||||||
@@ -4728,6 +5096,15 @@ dependencies = [
|
|||||||
"keccak",
|
"keccak",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shared-local-state"
|
name = "shared-local-state"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@@ -4824,6 +5201,60 @@ version = "1.15.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smb-server"
|
||||||
|
version = "0.4.1"
|
||||||
|
dependencies = [
|
||||||
|
"aes 0.8.4",
|
||||||
|
"async-trait",
|
||||||
|
"binrw",
|
||||||
|
"bytes",
|
||||||
|
"cap-std",
|
||||||
|
"cmac 0.7.2",
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"hex",
|
||||||
|
"hmac 0.12.1",
|
||||||
|
"md-5 0.10.6",
|
||||||
|
"md4 0.10.2",
|
||||||
|
"rc4",
|
||||||
|
"sha2 0.10.9",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smb2"
|
||||||
|
version = "0.11.3"
|
||||||
|
dependencies = [
|
||||||
|
"aes 0.9.1",
|
||||||
|
"aes-gcm 0.11.0-rc.4",
|
||||||
|
"async-trait",
|
||||||
|
"ccm",
|
||||||
|
"cmac 0.8.0-rc.5",
|
||||||
|
"digest 0.11.3",
|
||||||
|
"env_logger",
|
||||||
|
"futures-util",
|
||||||
|
"getrandom 0.4.2",
|
||||||
|
"hmac 0.13.0",
|
||||||
|
"log",
|
||||||
|
"lz4_flex",
|
||||||
|
"md-5 0.11.0",
|
||||||
|
"md4 0.11.0",
|
||||||
|
"num_enum",
|
||||||
|
"pbkdf2 0.13.0",
|
||||||
|
"proptest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha1 0.11.0",
|
||||||
|
"sha2 0.11.0",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.4.10"
|
version = "0.4.10"
|
||||||
@@ -5123,6 +5554,15 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.47"
|
version = "0.3.47"
|
||||||
@@ -5291,8 +5731,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime 0.6.11",
|
||||||
"toml_edit",
|
"toml_edit 0.22.27",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5304,6 +5744,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "1.1.1+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.22.27"
|
version = "0.22.27"
|
||||||
@@ -5313,9 +5762,30 @@ dependencies = [
|
|||||||
"indexmap",
|
"indexmap",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime 0.6.11",
|
||||||
"toml_write",
|
"toml_write",
|
||||||
"winnow",
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.25.12+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
|
"toml_parser",
|
||||||
|
"winnow 1.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_parser"
|
||||||
|
version = "1.1.2+spec-1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
|
dependencies = [
|
||||||
|
"winnow 1.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5382,14 +5852,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "twox-hash"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.0"
|
version = "1.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unarray"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -5544,6 +6056,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
@@ -5656,6 +6174,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wait-timeout"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.1+wasi-snapshot-preview1"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
@@ -6193,12 +6720,31 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winsafe"
|
name = "winsafe"
|
||||||
version = "0.0.19"
|
version = "0.0.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winx"
|
||||||
|
version = "0.36.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.1",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
|
|||||||
@@ -15,3 +15,5 @@ members = [
|
|||||||
"markbase-iscsi",
|
"markbase-iscsi",
|
||||||
"markbase-sync", "rust-iscsi-initiator",
|
"markbase-sync", "rust-iscsi-initiator",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,18 @@ ureq = "2.12" # 輕量同步 HTTP 客戶端
|
|||||||
rayon = "1.10" # Phase 4: 并行加密
|
rayon = "1.10" # Phase 4: 并行加密
|
||||||
url = "2" # URL 解析(rusty-s3 依賴)
|
url = "2" # URL 解析(rusty-s3 依賴)
|
||||||
|
|
||||||
|
# === SMB/CIFS Client (Phase 1) ===
|
||||||
|
smb2 = { path = "../vendor/smb2" } # Pure-Rust SMB2/3 client library with pipelined I/O
|
||||||
|
|
||||||
|
# === SMB/CIFS Server (Phase 2) — optional (vendored) ===
|
||||||
|
smb-server = { path = "../vendor/smb-server", optional = true, default-features = false }
|
||||||
|
async-trait = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [] # 默认不启用可选格式
|
default = [] # 默认不启用可选格式
|
||||||
optional-formats = ["unrar", "xz2", "sevenz-rust"] # 争议格式可选启用
|
optional-formats = ["unrar", "xz2", "sevenz-rust"] # 争议格式可选启用
|
||||||
|
smb-server = ["dep:smb-server"] # SMB server feature flag
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# tempfile moved to dependencies (needed for archive extraction)
|
# tempfile moved to dependencies (needed for archive extraction)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod render;
|
pub mod render;
|
||||||
|
pub mod smb_server;
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
@@ -9,12 +10,15 @@ pub enum ToolsCommands {
|
|||||||
Render(render::RenderCommand),
|
Render(render::RenderCommand),
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
Test(test::TestCommand),
|
Test(test::TestCommand),
|
||||||
|
#[command(flatten)]
|
||||||
|
SmbServer(smb_server::SmbServerCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_tools_command(cmd: ToolsCommands) -> anyhow::Result<()> {
|
pub async fn handle_tools_command(cmd: ToolsCommands) -> anyhow::Result<()> {
|
||||||
match cmd {
|
match cmd {
|
||||||
ToolsCommands::Render(c) => render::handle_render_command(c)?,
|
ToolsCommands::Render(c) => render::handle_render_command(c)?,
|
||||||
ToolsCommands::Test(c) => test::handle_test_command(c)?,
|
ToolsCommands::Test(c) => test::handle_test_command(c)?,
|
||||||
|
ToolsCommands::SmbServer(c) => smb_server::handle_smb_server_command(c).await?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
71
markbase-core/src/cli/tools/smb_server.rs
Normal file
71
markbase-core/src/cli/tools/smb_server.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum SmbServerCommand {
|
||||||
|
#[command(name = "smb-start")]
|
||||||
|
Start {
|
||||||
|
#[arg(short, long, default_value = "4445")]
|
||||||
|
port: u16,
|
||||||
|
|
||||||
|
#[arg(short, long, default_value = "/Users/accusys/momentry/var/sftpgo/data/demo")]
|
||||||
|
root: String,
|
||||||
|
|
||||||
|
#[arg(short, long, default_value = "markbase")]
|
||||||
|
share_name: String,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
read_only: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_smb_server_command(cmd: SmbServerCommand) -> anyhow::Result<()> {
|
||||||
|
#[cfg(feature = "smb-server")]
|
||||||
|
{
|
||||||
|
match cmd {
|
||||||
|
SmbServerCommand::Start { port, root, share_name, read_only } => {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use smb_server::{Access, Share, SmbServer};
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
let _ = tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(
|
||||||
|
EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| EnvFilter::new("info")),
|
||||||
|
)
|
||||||
|
.try_init();
|
||||||
|
|
||||||
|
let addr: std::net::SocketAddr =
|
||||||
|
format!("0.0.0.0:{}", port).parse()?;
|
||||||
|
let root_path = PathBuf::from(&root);
|
||||||
|
|
||||||
|
let vfs = Box::new(crate::vfs::local_fs::LocalFs::new());
|
||||||
|
let backend = crate::vfs::smb_server_backend::VfsShareBackend::new(vfs, root_path)
|
||||||
|
.read_only(read_only);
|
||||||
|
|
||||||
|
let share = Share::new(&share_name, backend)
|
||||||
|
.user("demo", Access::ReadWrite);
|
||||||
|
|
||||||
|
let server = SmbServer::builder()
|
||||||
|
.listen(addr)
|
||||||
|
.user("demo", "demo123")
|
||||||
|
.share(share)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
log::info!("SMB server listening on {}", addr);
|
||||||
|
log::info!("Share '{}' at root: {}", share_name, root);
|
||||||
|
log::info!("User: demo / demo123");
|
||||||
|
|
||||||
|
server.serve().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "smb-server"))]
|
||||||
|
{
|
||||||
|
let _ = cmd;
|
||||||
|
anyhow::bail!("SMB server support not enabled. Build with --features smb-server");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// 参考OpenSSH scp.c源码
|
// 参考OpenSSH scp.c源码
|
||||||
|
|
||||||
use crate::vfs::open_flags::OpenFlags;
|
use crate::vfs::open_flags::OpenFlags;
|
||||||
use crate::vfs::{VfsBackend, VfsFile, VfsStat};
|
use crate::vfs::{VfsBackend, VfsStat};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use std::io::{BufRead, Read, Write};
|
use std::io::{BufRead, Read, Write};
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
pub mod local_fs;
|
pub mod local_fs;
|
||||||
pub mod open_flags;
|
pub mod open_flags;
|
||||||
pub mod s3_fs;
|
pub mod s3_fs;
|
||||||
|
pub mod smb_fs;
|
||||||
|
#[cfg(feature = "smb-server")]
|
||||||
|
pub mod smb_server_backend;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -81,7 +84,7 @@ pub struct VfsDirEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 打开文件的抽象
|
/// 打开文件的抽象
|
||||||
pub trait VfsFile {
|
pub trait VfsFile: Send {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize, VfsError>;
|
fn read(&mut self, buf: &mut [u8]) -> Result<usize, VfsError>;
|
||||||
fn write(&mut self, buf: &[u8]) -> Result<usize, VfsError>;
|
fn write(&mut self, buf: &[u8]) -> Result<usize, VfsError>;
|
||||||
fn seek(&mut self, pos: std::io::SeekFrom) -> Result<u64, VfsError>;
|
fn seek(&mut self, pos: std::io::SeekFrom) -> Result<u64, VfsError>;
|
||||||
|
|||||||
539
markbase-core/src/vfs/smb_fs.rs
Normal file
539
markbase-core/src/vfs/smb_fs.rs
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
use super::open_flags::OpenFlags;
|
||||||
|
use super::{VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsStat};
|
||||||
|
use smb2::ClientConfig;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
const SMB_TIMEOUT_SECS: u64 = 30;
|
||||||
|
const FILETIME_TO_UNIX_SECS: u64 = 11_644_473_600;
|
||||||
|
|
||||||
|
fn filetime_to_systemtime(raw: u64) -> SystemTime {
|
||||||
|
let secs = raw / 10_000_000;
|
||||||
|
if secs > FILETIME_TO_UNIX_SECS {
|
||||||
|
UNIX_EPOCH + Duration::from_secs(secs - FILETIME_TO_UNIX_SECS)
|
||||||
|
} else {
|
||||||
|
UNIX_EPOCH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_smb_error(e: smb2::Error) -> VfsError {
|
||||||
|
match e.kind() {
|
||||||
|
smb2::ErrorKind::NotFound => VfsError::NotFound(e.to_string()),
|
||||||
|
smb2::ErrorKind::AlreadyExists => VfsError::AlreadyExists(e.to_string()),
|
||||||
|
smb2::ErrorKind::AccessDenied => VfsError::PermissionDenied(e.to_string()),
|
||||||
|
smb2::ErrorKind::IsADirectory => VfsError::IsADirectory(e.to_string()),
|
||||||
|
smb2::ErrorKind::NotADirectory => VfsError::NotADirectory(e.to_string()),
|
||||||
|
smb2::ErrorKind::ConnectionLost | smb2::ErrorKind::TimedOut | smb2::ErrorKind::SessionExpired => {
|
||||||
|
VfsError::Io(format!("SMB connection error: {}", e))
|
||||||
|
}
|
||||||
|
_ => VfsError::Io(format!("SMB error: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB 客户端 VFS 后端 (SMB 2/3)
|
||||||
|
pub struct SmbVfs {
|
||||||
|
runtime: Arc<tokio::runtime::Runtime>,
|
||||||
|
client: Arc<Mutex<smb2::SmbClient>>,
|
||||||
|
tree: Mutex<smb2::Tree>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmbVfs {
|
||||||
|
pub fn new(
|
||||||
|
addr: &str,
|
||||||
|
share: &str,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<Self, VfsError> {
|
||||||
|
let runtime = Arc::new(
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| VfsError::Io(format!("Failed to create tokio runtime: {}", e)))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let config = ClientConfig {
|
||||||
|
addr: addr.to_string(),
|
||||||
|
timeout: Duration::from_secs(SMB_TIMEOUT_SECS),
|
||||||
|
username: username.to_string(),
|
||||||
|
password: password.to_string(),
|
||||||
|
domain: String::new(),
|
||||||
|
auto_reconnect: false,
|
||||||
|
compression: true,
|
||||||
|
dfs_enabled: false,
|
||||||
|
dfs_target_overrides: std::collections::HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (client, tree) = runtime.block_on(async {
|
||||||
|
let mut c = smb2::SmbClient::connect(config)
|
||||||
|
.await
|
||||||
|
.map_err(|e| VfsError::Io(format!("SMB connect failed: {}", e)))?;
|
||||||
|
let t = c
|
||||||
|
.connect_share(share)
|
||||||
|
.await
|
||||||
|
.map_err(|e| VfsError::Io(format!("SMB connect_share failed: {}", e)))?;
|
||||||
|
Ok::<_, VfsError>((c, t))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
runtime,
|
||||||
|
client: Arc::new(Mutex::new(client)),
|
||||||
|
tree: Mutex::new(tree),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_to_str(path: &Path) -> String {
|
||||||
|
let s = path.to_string_lossy().to_string();
|
||||||
|
s.trim_start_matches('/').to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for SmbVfs {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
runtime: self.runtime.clone(),
|
||||||
|
client: self.client.clone(),
|
||||||
|
tree: Mutex::new(self.tree.lock().unwrap().clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VfsBackend for SmbVfs {
|
||||||
|
fn clone_boxed(&self) -> Box<dyn VfsBackend> {
|
||||||
|
Box::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_dir(&self, path: &Path) -> Result<Vec<VfsDirEntry>, VfsError> {
|
||||||
|
let smb_path = Self::path_to_str(path);
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let entries = self
|
||||||
|
.runtime
|
||||||
|
.block_on(client.list_directory(&mut *tree, &smb_path))
|
||||||
|
.map_err(map_smb_error)?;
|
||||||
|
|
||||||
|
Ok(entries
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| e.name != "." && e.name != "..")
|
||||||
|
.map(|e| VfsDirEntry {
|
||||||
|
name: e.name,
|
||||||
|
long_name: String::new(),
|
||||||
|
stat: VfsStat {
|
||||||
|
size: e.size,
|
||||||
|
mode: if e.is_directory { 0o755 } else { 0o644 },
|
||||||
|
uid: 0,
|
||||||
|
gid: 0,
|
||||||
|
atime: filetime_to_systemtime(0),
|
||||||
|
mtime: filetime_to_systemtime(e.modified.0),
|
||||||
|
is_dir: e.is_directory,
|
||||||
|
is_symlink: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_file(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
flags: &OpenFlags,
|
||||||
|
) -> Result<Box<dyn VfsFile>, VfsError> {
|
||||||
|
let smb_path = Self::path_to_str(path);
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
|
||||||
|
if flags.write || flags.create || flags.truncate {
|
||||||
|
Ok(Box::new(SmbVfsFile {
|
||||||
|
runtime: self.runtime.clone(),
|
||||||
|
client: self.client.clone(),
|
||||||
|
tree: tree.clone(),
|
||||||
|
path: smb_path,
|
||||||
|
mode: FileMode::Write,
|
||||||
|
position: 0,
|
||||||
|
write_buf: Vec::new(),
|
||||||
|
data: Vec::new(),
|
||||||
|
size: 0,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
let data = self
|
||||||
|
.runtime
|
||||||
|
.block_on(client.read_file(&mut *tree, &smb_path))
|
||||||
|
.map_err(map_smb_error)?;
|
||||||
|
let size = data.len() as u64;
|
||||||
|
Ok(Box::new(SmbVfsFile {
|
||||||
|
runtime: self.runtime.clone(),
|
||||||
|
client: self.client.clone(),
|
||||||
|
tree: tree.clone(),
|
||||||
|
path: smb_path,
|
||||||
|
mode: FileMode::Read,
|
||||||
|
position: 0,
|
||||||
|
write_buf: Vec::new(),
|
||||||
|
data,
|
||||||
|
size,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stat(&self, path: &Path) -> Result<VfsStat, VfsError> {
|
||||||
|
let smb_path = Self::path_to_str(path);
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let info = self
|
||||||
|
.runtime
|
||||||
|
.block_on(client.stat(&mut *tree, &smb_path))
|
||||||
|
.map_err(map_smb_error)?;
|
||||||
|
|
||||||
|
Ok(VfsStat {
|
||||||
|
size: info.size,
|
||||||
|
mode: if info.is_directory { 0o755 } else { 0o644 },
|
||||||
|
uid: 0,
|
||||||
|
gid: 0,
|
||||||
|
atime: filetime_to_systemtime(info.accessed.0),
|
||||||
|
mtime: filetime_to_systemtime(info.modified.0),
|
||||||
|
is_dir: info.is_directory,
|
||||||
|
is_symlink: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lstat(&self, path: &Path) -> Result<VfsStat, VfsError> {
|
||||||
|
self.stat(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dir(&self, path: &Path, _mode: u32) -> Result<(), VfsError> {
|
||||||
|
let smb_path = Self::path_to_str(path);
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
self.runtime
|
||||||
|
.block_on(client.create_directory(&mut *tree, &smb_path))
|
||||||
|
.map_err(map_smb_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_dir_all(&self, path: &Path, mode: u32) -> Result<(), VfsError> {
|
||||||
|
let mut current = path.to_path_buf();
|
||||||
|
let mut stack = Vec::new();
|
||||||
|
while let Some(parent) = current.parent() {
|
||||||
|
if parent.as_os_str().is_empty() || parent == Path::new("/") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stack.push(parent.to_path_buf());
|
||||||
|
current = parent.to_path_buf();
|
||||||
|
}
|
||||||
|
for dir in stack.into_iter().rev() {
|
||||||
|
if self.stat(&dir).is_err() {
|
||||||
|
self.create_dir(&dir, mode)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.stat(path).is_err() {
|
||||||
|
self.create_dir(path, mode)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_dir(&self, path: &Path) -> Result<(), VfsError> {
|
||||||
|
let smb_path = Self::path_to_str(path);
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
self.runtime
|
||||||
|
.block_on(client.delete_directory(&mut *tree, &smb_path))
|
||||||
|
.map_err(map_smb_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_file(&self, path: &Path) -> Result<(), VfsError> {
|
||||||
|
let smb_path = Self::path_to_str(path);
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
self.runtime
|
||||||
|
.block_on(client.delete_file(&mut *tree, &smb_path))
|
||||||
|
.map_err(map_smb_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rename(&self, from: &Path, to: &Path) -> Result<(), VfsError> {
|
||||||
|
let smb_from = Self::path_to_str(from);
|
||||||
|
let smb_to = Self::path_to_str(to);
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
self.runtime
|
||||||
|
.block_on(client.rename(&mut *tree, &smb_from, &smb_to))
|
||||||
|
.map_err(map_smb_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_stat(&self, _path: &Path, _stat: &VfsStat) -> Result<(), VfsError> {
|
||||||
|
Err(VfsError::Unsupported("SMB set_stat".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_link(&self, _path: &Path) -> Result<PathBuf, VfsError> {
|
||||||
|
Err(VfsError::Unsupported("SMB read_link".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_symlink(&self, _target: &Path, _link: &Path) -> Result<(), VfsError> {
|
||||||
|
Err(VfsError::Unsupported("SMB create_symlink".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn real_path(&self, path: &Path) -> Result<PathBuf, VfsError> {
|
||||||
|
let smb_path = Self::path_to_str(path);
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let mut tree = self.tree.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let _info = self
|
||||||
|
.runtime
|
||||||
|
.block_on(client.stat(&mut *tree, &smb_path))
|
||||||
|
.map_err(map_smb_error)?;
|
||||||
|
Ok(path.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exists(&self, path: &Path) -> bool {
|
||||||
|
let smb_path = Self::path_to_str(path);
|
||||||
|
let mut client = match self.client.lock() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
let mut tree = match self.tree.lock() {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
self.runtime
|
||||||
|
.block_on(client.stat(&mut *tree, &smb_path))
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hard_link(&self, _original: &Path, _link: &Path) -> Result<(), VfsError> {
|
||||||
|
Err(VfsError::Unsupported("SMB hard_link".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FileMode {
|
||||||
|
Read,
|
||||||
|
Write,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SmbVfsFile {
|
||||||
|
runtime: Arc<tokio::runtime::Runtime>,
|
||||||
|
client: Arc<Mutex<smb2::SmbClient>>,
|
||||||
|
tree: smb2::Tree,
|
||||||
|
path: String,
|
||||||
|
mode: FileMode,
|
||||||
|
position: u64,
|
||||||
|
write_buf: Vec<u8>,
|
||||||
|
data: Vec<u8>,
|
||||||
|
size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmbVfsFile {
|
||||||
|
fn ensure_data_loaded(&mut self) -> Result<(), VfsError> {
|
||||||
|
if self.data.is_empty() && self.size > 0 {
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let data = self
|
||||||
|
.runtime
|
||||||
|
.block_on(client.read_file(&mut self.tree, &self.path))
|
||||||
|
.map_err(map_smb_error)?;
|
||||||
|
self.size = data.len() as u64;
|
||||||
|
self.data = data;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VfsFile for SmbVfsFile {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> Result<usize, VfsError> {
|
||||||
|
self.ensure_data_loaded()?;
|
||||||
|
if self.position >= self.size {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
let start = self.position as usize;
|
||||||
|
let available = self.size as usize - start;
|
||||||
|
let to_copy = std::cmp::min(buf.len(), available);
|
||||||
|
buf[..to_copy].copy_from_slice(&self.data[start..start + to_copy]);
|
||||||
|
self.position += to_copy as u64;
|
||||||
|
Ok(to_copy)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, buf: &[u8]) -> Result<usize, VfsError> {
|
||||||
|
self.write_buf.extend_from_slice(buf);
|
||||||
|
self.position += buf.len() as u64;
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seek(&mut self, pos: std::io::SeekFrom) -> Result<u64, VfsError> {
|
||||||
|
match pos {
|
||||||
|
std::io::SeekFrom::Start(offset) => {
|
||||||
|
self.position = offset;
|
||||||
|
Ok(offset)
|
||||||
|
}
|
||||||
|
std::io::SeekFrom::End(offset) => {
|
||||||
|
let new_pos = if offset >= 0 {
|
||||||
|
self.size + offset as u64
|
||||||
|
} else {
|
||||||
|
self.size.saturating_sub((-offset) as u64)
|
||||||
|
};
|
||||||
|
self.position = new_pos;
|
||||||
|
Ok(new_pos)
|
||||||
|
}
|
||||||
|
std::io::SeekFrom::Current(offset) => {
|
||||||
|
let new_pos = if offset >= 0 {
|
||||||
|
self.position + offset as u64
|
||||||
|
} else {
|
||||||
|
self.position.saturating_sub((-offset) as u64)
|
||||||
|
};
|
||||||
|
self.position = new_pos;
|
||||||
|
Ok(new_pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> Result<(), VfsError> {
|
||||||
|
if let FileMode::Write = self.mode {
|
||||||
|
if !self.write_buf.is_empty() {
|
||||||
|
let data = std::mem::take(&mut self.write_buf);
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
self.runtime
|
||||||
|
.block_on(client.write_file(&mut self.tree, &self.path, &data))
|
||||||
|
.map_err(map_smb_error)?;
|
||||||
|
self.size = data.len() as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stat(&mut self) -> Result<VfsStat, VfsError> {
|
||||||
|
let mut client = self.client.lock().map_err(|e| VfsError::Io(e.to_string()))?;
|
||||||
|
let info = self
|
||||||
|
.runtime
|
||||||
|
.block_on(client.stat(&mut self.tree, &self.path))
|
||||||
|
.map_err(map_smb_error)?;
|
||||||
|
Ok(VfsStat {
|
||||||
|
size: info.size,
|
||||||
|
mode: if info.is_directory { 0o755 } else { 0o644 },
|
||||||
|
uid: 0,
|
||||||
|
gid: 0,
|
||||||
|
atime: filetime_to_systemtime(info.accessed.0),
|
||||||
|
mtime: filetime_to_systemtime(info.modified.0),
|
||||||
|
is_dir: info.is_directory,
|
||||||
|
is_symlink: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_len(&mut self, _size: u64) -> Result<(), VfsError> {
|
||||||
|
Err(VfsError::Unsupported("SMB set_len".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SmbVfsFile {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let FileMode::Write = self.mode {
|
||||||
|
if !self.write_buf.is_empty() {
|
||||||
|
let data = std::mem::take(&mut self.write_buf);
|
||||||
|
if let Ok(mut client) = self.client.lock() {
|
||||||
|
let _ = self
|
||||||
|
.runtime
|
||||||
|
.block_on(client.write_file(&mut self.tree, &self.path, &data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filetime_conversion() {
|
||||||
|
let raw: u64 = 133604700000000000;
|
||||||
|
let st = filetime_to_systemtime(raw);
|
||||||
|
assert!(st > UNIX_EPOCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_to_str() {
|
||||||
|
assert_eq!(SmbVfs::path_to_str(Path::new("foo/bar.txt")), "foo/bar.txt");
|
||||||
|
assert_eq!(SmbVfs::path_to_str(Path::new("/foo/bar.txt")), "foo/bar.txt");
|
||||||
|
assert_eq!(SmbVfs::path_to_str(Path::new("")), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_mapping_invalid_data() {
|
||||||
|
let err = smb2::Error::invalid_data("test");
|
||||||
|
let mapped = map_smb_error(err);
|
||||||
|
match mapped {
|
||||||
|
VfsError::Io(_) => {}
|
||||||
|
_ => panic!("Expected Io, got {:?}", mapped),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Integration test: requires Docker Samba container on port 10445.
|
||||||
|
/// Run with: docker compose -f vendor/smb2/tests/docker/internal/docker-compose.yml up -d smb-guest
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_smb_vfs_list_root() {
|
||||||
|
let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap();
|
||||||
|
let entries = vfs.read_dir(Path::new("/")).unwrap();
|
||||||
|
assert!(!entries.is_empty(), "Expected at least . and ..");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_smb_vfs_write_read_file() {
|
||||||
|
let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap();
|
||||||
|
|
||||||
|
let content = b"Hello SMB VFS!";
|
||||||
|
let path = Path::new("/smb_vfs_test.txt");
|
||||||
|
|
||||||
|
// Write
|
||||||
|
{
|
||||||
|
let flags = OpenFlags::new().write().create().truncate();
|
||||||
|
let mut file = vfs.open_file(path, &flags).unwrap();
|
||||||
|
file.write(content).unwrap();
|
||||||
|
file.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read back
|
||||||
|
let flags = OpenFlags::new().read();
|
||||||
|
let mut file = vfs.open_file(path, &flags).unwrap();
|
||||||
|
let mut buf = vec![0u8; 1024];
|
||||||
|
let n = file.read(&mut buf).unwrap();
|
||||||
|
assert_eq!(&buf[..n], content);
|
||||||
|
|
||||||
|
// Stat
|
||||||
|
let stat = vfs.stat(path).unwrap();
|
||||||
|
assert_eq!(stat.size, content.len() as u64);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
vfs.remove_file(path).unwrap();
|
||||||
|
assert!(!vfs.exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_smb_vfs_create_remove_dir() {
|
||||||
|
let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap();
|
||||||
|
let dir_path = Path::new("/smb_vfs_test_dir");
|
||||||
|
|
||||||
|
vfs.create_dir(dir_path, 0o755).unwrap();
|
||||||
|
assert!(vfs.exists(dir_path));
|
||||||
|
|
||||||
|
vfs.remove_dir(dir_path).unwrap();
|
||||||
|
assert!(!vfs.exists(dir_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_smb_vfs_rename_file() {
|
||||||
|
let vfs = SmbVfs::new("127.0.0.1:10445", "public", "", "").unwrap();
|
||||||
|
let src = Path::new("/rename_src.txt");
|
||||||
|
let dst = Path::new("/rename_dst.txt");
|
||||||
|
|
||||||
|
// Create source file
|
||||||
|
let flags = OpenFlags::new().write().create().truncate();
|
||||||
|
{
|
||||||
|
let mut file = vfs.open_file(src, &flags).unwrap();
|
||||||
|
file.write(b"rename test").unwrap();
|
||||||
|
file.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename
|
||||||
|
vfs.rename(src, dst).unwrap();
|
||||||
|
assert!(!vfs.exists(src));
|
||||||
|
assert!(vfs.exists(dst));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
vfs.remove_file(dst).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
437
markbase-core/src/vfs/smb_server_backend.rs
Normal file
437
markbase-core/src/vfs/smb_server_backend.rs
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use smb_server::{
|
||||||
|
BackendCapabilities, DirEntry, FileInfo, FileTimes, Handle, OpenIntent, OpenOptions, ShareBackend,
|
||||||
|
SmbError, SmbPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::open_flags::OpenFlags;
|
||||||
|
use super::{VfsBackend, VfsError, VfsStat};
|
||||||
|
|
||||||
|
const FILETIME_OFFSET: u64 = 116_444_736_000_000_000;
|
||||||
|
|
||||||
|
pub struct VfsShareBackend {
|
||||||
|
vfs: Arc<dyn VfsBackend>,
|
||||||
|
root: PathBuf,
|
||||||
|
read_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VfsShareBackend {
|
||||||
|
pub fn new(vfs: Box<dyn VfsBackend>, root: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
vfs: Arc::from(vfs),
|
||||||
|
root,
|
||||||
|
read_only: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_only(mut self, yes: bool) -> Self {
|
||||||
|
self.read_only = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_path(root: &Path, smb_path: &SmbPath) -> PathBuf {
|
||||||
|
if smb_path.is_root() {
|
||||||
|
return root.to_path_buf();
|
||||||
|
}
|
||||||
|
let mut result = root.to_path_buf();
|
||||||
|
for component in smb_path.components() {
|
||||||
|
result.push(component);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_error(e: VfsError) -> SmbError {
|
||||||
|
match e {
|
||||||
|
VfsError::NotFound(_) => SmbError::NotFound,
|
||||||
|
VfsError::PermissionDenied(_) => SmbError::AccessDenied,
|
||||||
|
VfsError::AlreadyExists(_) => SmbError::Exists,
|
||||||
|
VfsError::NotEmpty(_) => SmbError::NotEmpty,
|
||||||
|
VfsError::NotADirectory(_) => SmbError::NotADirectory,
|
||||||
|
VfsError::IsADirectory(_) => SmbError::IsDirectory,
|
||||||
|
VfsError::Unsupported(_) => SmbError::NotSupported,
|
||||||
|
VfsError::Io(msg) => SmbError::Io(std::io::Error::other(msg)),
|
||||||
|
VfsError::UnexpectedEof => SmbError::Io(std::io::Error::other("unexpected eof")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn system_time_to_filetime(t: SystemTime) -> u64 {
|
||||||
|
match t.duration_since(SystemTime::UNIX_EPOCH) {
|
||||||
|
Ok(d) => {
|
||||||
|
FILETIME_OFFSET
|
||||||
|
+ (d.as_secs() * 10_000_000)
|
||||||
|
+ (d.subsec_nanos() as u64 / 100)
|
||||||
|
}
|
||||||
|
Err(_) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vfs_stat_to_file_info(stat: &VfsStat, name: &str, path: &Path) -> FileInfo {
|
||||||
|
let name = if name.is_empty() {
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
name.to_string()
|
||||||
|
};
|
||||||
|
FileInfo {
|
||||||
|
name,
|
||||||
|
end_of_file: stat.size,
|
||||||
|
allocation_size: stat.size,
|
||||||
|
creation_time: system_time_to_filetime(stat.mtime),
|
||||||
|
last_access_time: system_time_to_filetime(stat.atime),
|
||||||
|
last_write_time: system_time_to_filetime(stat.mtime),
|
||||||
|
change_time: system_time_to_filetime(stat.mtime),
|
||||||
|
is_directory: stat.is_dir,
|
||||||
|
file_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vfs_error_to_io(e: VfsError) -> std::io::Error {
|
||||||
|
std::io::Error::other(e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ShareBackend for VfsShareBackend {
|
||||||
|
async fn open(&self, path: &SmbPath, opts: OpenOptions) -> Result<Box<dyn Handle>, SmbError> {
|
||||||
|
let full_path = resolve_path(&self.root, path);
|
||||||
|
|
||||||
|
if opts.directory {
|
||||||
|
match opts.intent {
|
||||||
|
OpenIntent::Create => {
|
||||||
|
if self.vfs.exists(&full_path) {
|
||||||
|
return Err(SmbError::Exists);
|
||||||
|
}
|
||||||
|
self.vfs.create_dir(&full_path, 0o755).map_err(map_error)?;
|
||||||
|
}
|
||||||
|
OpenIntent::OpenOrCreate | OpenIntent::OverwriteOrCreate => {
|
||||||
|
if !self.vfs.exists(&full_path) {
|
||||||
|
self.vfs.create_dir(&full_path, 0o755).map_err(map_error)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if !self.vfs.exists(&full_path) {
|
||||||
|
return Err(SmbError::NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let stat = self.vfs.stat(&full_path).map_err(map_error)?;
|
||||||
|
if !stat.is_dir {
|
||||||
|
return Err(SmbError::NotADirectory);
|
||||||
|
}
|
||||||
|
return Ok(Box::new(VfsHandle::Directory {
|
||||||
|
vfs: self.vfs.clone(),
|
||||||
|
path: full_path,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut flags = OpenFlags::new();
|
||||||
|
if opts.read {
|
||||||
|
flags = flags.read();
|
||||||
|
}
|
||||||
|
if opts.write {
|
||||||
|
flags = flags.write();
|
||||||
|
}
|
||||||
|
match opts.intent {
|
||||||
|
OpenIntent::Open => {}
|
||||||
|
OpenIntent::Create => {
|
||||||
|
flags = flags.create().exclusive();
|
||||||
|
}
|
||||||
|
OpenIntent::OpenOrCreate => {
|
||||||
|
flags = flags.create();
|
||||||
|
}
|
||||||
|
OpenIntent::OverwriteOrCreate => {
|
||||||
|
flags = flags.create().truncate();
|
||||||
|
}
|
||||||
|
OpenIntent::Truncate => {
|
||||||
|
flags = flags.truncate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.non_directory && self.vfs.exists(&full_path) {
|
||||||
|
let stat = self.vfs.stat(&full_path).map_err(map_error)?;
|
||||||
|
if stat.is_dir {
|
||||||
|
return Err(SmbError::IsDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = self
|
||||||
|
.vfs
|
||||||
|
.open_file(&full_path, &flags)
|
||||||
|
.map_err(map_error)?;
|
||||||
|
Ok(Box::new(VfsHandle::File {
|
||||||
|
file: Mutex::new(file),
|
||||||
|
path: full_path,
|
||||||
|
vfs: self.vfs.clone(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unlink(&self, path: &SmbPath) -> Result<(), SmbError> {
|
||||||
|
let full_path = resolve_path(&self.root, path);
|
||||||
|
if self.vfs.exists(&full_path) {
|
||||||
|
let stat = self.vfs.stat(&full_path).map_err(map_error)?;
|
||||||
|
if stat.is_dir {
|
||||||
|
return self.vfs.remove_dir(&full_path).map_err(map_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.vfs.remove_file(&full_path).map_err(map_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rename(&self, from: &SmbPath, to: &SmbPath) -> Result<(), SmbError> {
|
||||||
|
let from_path = resolve_path(&self.root, from);
|
||||||
|
let to_path = resolve_path(&self.root, to);
|
||||||
|
if self.vfs.exists(&to_path) {
|
||||||
|
return Err(SmbError::Exists);
|
||||||
|
}
|
||||||
|
self.vfs.rename(&from_path, &to_path).map_err(map_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capabilities(&self) -> BackendCapabilities {
|
||||||
|
BackendCapabilities {
|
||||||
|
is_read_only: self.read_only,
|
||||||
|
case_sensitive: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VfsHandle {
|
||||||
|
File {
|
||||||
|
file: Mutex<Box<dyn super::VfsFile + Send>>,
|
||||||
|
path: PathBuf,
|
||||||
|
vfs: Arc<dyn VfsBackend>,
|
||||||
|
},
|
||||||
|
Directory {
|
||||||
|
vfs: Arc<dyn VfsBackend>,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handle for VfsHandle {
|
||||||
|
async fn read(&self, offset: u64, len: u32) -> Result<Bytes, SmbError> {
|
||||||
|
match self {
|
||||||
|
Self::File { file, .. } => {
|
||||||
|
let mut file = file.lock().unwrap();
|
||||||
|
file.seek(std::io::SeekFrom::Start(offset))
|
||||||
|
.map_err(vfs_error_to_io)?;
|
||||||
|
let mut buf = vec![0u8; len as usize];
|
||||||
|
let n = file.read(&mut buf).map_err(map_error)?;
|
||||||
|
buf.truncate(n);
|
||||||
|
Ok(Bytes::from(buf))
|
||||||
|
}
|
||||||
|
Self::Directory { .. } => Err(SmbError::NotSupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write(&self, offset: u64, data: &[u8]) -> Result<u32, SmbError> {
|
||||||
|
match self {
|
||||||
|
Self::File { file, .. } => {
|
||||||
|
let mut file = file.lock().unwrap();
|
||||||
|
file.seek(std::io::SeekFrom::Start(offset))
|
||||||
|
.map_err(vfs_error_to_io)?;
|
||||||
|
let n = file.write(data).map_err(map_error)?;
|
||||||
|
Ok(n as u32)
|
||||||
|
}
|
||||||
|
Self::Directory { .. } => Err(SmbError::NotSupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn flush(&self) -> Result<(), SmbError> {
|
||||||
|
match self {
|
||||||
|
Self::File { file, .. } => {
|
||||||
|
let mut file = file.lock().unwrap();
|
||||||
|
file.flush().map_err(map_error)
|
||||||
|
}
|
||||||
|
Self::Directory { .. } => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stat(&self) -> Result<FileInfo, SmbError> {
|
||||||
|
match self {
|
||||||
|
Self::File { file, path, .. } => {
|
||||||
|
let mut f = file.lock().unwrap();
|
||||||
|
let vfs_stat = f.stat().map_err(map_error)?;
|
||||||
|
Ok(vfs_stat_to_file_info(&vfs_stat, "", path))
|
||||||
|
}
|
||||||
|
Self::Directory { vfs, path } => {
|
||||||
|
let vfs_stat = vfs.stat(path).map_err(map_error)?;
|
||||||
|
Ok(vfs_stat_to_file_info(&vfs_stat, "", path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_times(&self, times: FileTimes) -> Result<(), SmbError> {
|
||||||
|
let (vfs, path) = match self {
|
||||||
|
Self::File { path, vfs, .. } => (vfs, path),
|
||||||
|
Self::Directory { vfs, path } => (vfs, path),
|
||||||
|
};
|
||||||
|
let mut stat = VfsStat::new();
|
||||||
|
if let Some(t) = times.last_write_time {
|
||||||
|
stat.mtime = filetime_to_systemtime(t);
|
||||||
|
}
|
||||||
|
if let Some(t) = times.last_access_time {
|
||||||
|
stat.atime = filetime_to_systemtime(t);
|
||||||
|
}
|
||||||
|
vfs.set_stat(path, &stat).map_err(map_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn truncate(&self, len: u64) -> Result<(), SmbError> {
|
||||||
|
match self {
|
||||||
|
Self::File { file, .. } => {
|
||||||
|
let mut file = file.lock().unwrap();
|
||||||
|
file.set_len(len).map_err(map_error)
|
||||||
|
}
|
||||||
|
Self::Directory { .. } => Err(SmbError::NotSupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_dir(&self, _pattern: Option<&str>) -> Result<Vec<DirEntry>, SmbError> {
|
||||||
|
match self {
|
||||||
|
Self::File { .. } => Err(SmbError::NotADirectory),
|
||||||
|
Self::Directory { vfs, path } => {
|
||||||
|
let entries = vfs.read_dir(path).map_err(map_error)?;
|
||||||
|
let result = entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| {
|
||||||
|
let info = vfs_stat_to_file_info(&entry.stat, &entry.name, path);
|
||||||
|
DirEntry { info }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(self: Box<Self>) -> Result<(), SmbError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filetime_to_systemtime(ft: u64) -> SystemTime {
|
||||||
|
if ft < FILETIME_OFFSET {
|
||||||
|
return SystemTime::UNIX_EPOCH;
|
||||||
|
}
|
||||||
|
let delta_secs = (ft - FILETIME_OFFSET) / 10_000_000;
|
||||||
|
let delta_ns = ((ft - FILETIME_OFFSET) % 10_000_000) as u32 * 100;
|
||||||
|
SystemTime::UNIX_EPOCH
|
||||||
|
+ std::time::Duration::new(delta_secs, delta_ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use smb_server::{Share, SmbServer, Access};
|
||||||
|
|
||||||
|
use crate::vfs::local_fs::LocalFs;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_path_root() {
|
||||||
|
let root = PathBuf::from("/srv/share");
|
||||||
|
let smb = SmbPath::root();
|
||||||
|
assert_eq!(resolve_path(&root, &smb), root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_path_components() {
|
||||||
|
let root = PathBuf::from("/srv/share");
|
||||||
|
let smb: SmbPath = "dir\\sub\\file.txt".parse().unwrap();
|
||||||
|
let expected = PathBuf::from("/srv/share/dir/sub/file.txt");
|
||||||
|
assert_eq!(resolve_path(&root, &smb), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_system_time_to_filetime() {
|
||||||
|
let epoch = SystemTime::UNIX_EPOCH;
|
||||||
|
let ft = system_time_to_filetime(epoch);
|
||||||
|
assert_eq!(ft, FILETIME_OFFSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filetime_roundtrip() {
|
||||||
|
let now = SystemTime::now();
|
||||||
|
let ft = system_time_to_filetime(now);
|
||||||
|
let back = filetime_to_systemtime(ft);
|
||||||
|
let diff = if now > back {
|
||||||
|
now.duration_since(back).unwrap()
|
||||||
|
} else {
|
||||||
|
back.duration_since(now).unwrap()
|
||||||
|
};
|
||||||
|
assert!(diff.as_millis() < 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_map_errors() {
|
||||||
|
assert!(matches!(
|
||||||
|
map_error(VfsError::NotFound("x".into())),
|
||||||
|
SmbError::NotFound
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
map_error(VfsError::AlreadyExists("x".into())),
|
||||||
|
SmbError::Exists
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
map_error(VfsError::PermissionDenied("x".into())),
|
||||||
|
SmbError::AccessDenied
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
map_error(VfsError::NotEmpty("x".into())),
|
||||||
|
SmbError::NotEmpty
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
map_error(VfsError::NotADirectory("x".into())),
|
||||||
|
SmbError::NotADirectory
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
map_error(VfsError::IsADirectory("x".into())),
|
||||||
|
SmbError::IsDirectory
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vfs_share_backend_creation() {
|
||||||
|
let vfs = Box::new(LocalFs::new());
|
||||||
|
let root = PathBuf::from("/tmp");
|
||||||
|
let backend = VfsShareBackend::new(vfs, root);
|
||||||
|
assert!(!backend.capabilities().is_read_only);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_open_nonexistent_file() {
|
||||||
|
let vfs = Box::new(LocalFs::new());
|
||||||
|
let root = PathBuf::from("/nonexistent");
|
||||||
|
let backend = VfsShareBackend::new(vfs, root);
|
||||||
|
let smb_path: SmbPath = "missing.txt".parse().unwrap();
|
||||||
|
let opts = OpenOptions {
|
||||||
|
read: true,
|
||||||
|
write: false,
|
||||||
|
intent: OpenIntent::Open,
|
||||||
|
directory: false,
|
||||||
|
non_directory: false,
|
||||||
|
delete_on_close: false,
|
||||||
|
};
|
||||||
|
let result = backend.open(&smb_path, opts).await;
|
||||||
|
assert!(matches!(result, Err(SmbError::NotFound)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rejects_dotdot() {
|
||||||
|
assert!("a\\..\\b".parse::<SmbPath>().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rejects_forbidden_chars() {
|
||||||
|
for bad in ["a<b", "a>b", "a:b", "a\"b", "a|b", "a?b", "a*b"] {
|
||||||
|
assert!(bad.parse::<SmbPath>().is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::vfs::open_flags::OpenFlags;
|
use crate::vfs::open_flags::OpenFlags;
|
||||||
use crate::vfs::{VfsBackend, VfsDirEntry, VfsStat, VfsFile};
|
use crate::vfs::{VfsBackend, VfsDirEntry, VfsStat};
|
||||||
use crate::ssh_server::upload_hook::UploadHook;
|
use crate::ssh_server::upload_hook::UploadHook;
|
||||||
use bytes::{Buf, Bytes};
|
use bytes::{Buf, Bytes};
|
||||||
use dav_server::davpath::DavPath;
|
use dav_server::davpath::DavPath;
|
||||||
|
|||||||
57
vendor/smb-server/Cargo.toml
vendored
Normal file
57
vendor/smb-server/Cargo.toml
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
[package]
|
||||||
|
name = "smb-server"
|
||||||
|
version = "0.4.1"
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.95"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/paltaio/rust-smb-server"
|
||||||
|
description = "SMB2/3 file-sharing server library with pluggable storage backends."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.40", features = ["full"] }
|
||||||
|
bytes = "1.7"
|
||||||
|
async-trait = "0.1"
|
||||||
|
tracing = "0.1"
|
||||||
|
thiserror = "1"
|
||||||
|
uuid = { version = "1.10", features = ["v4"] }
|
||||||
|
binrw = "0.15"
|
||||||
|
getrandom = "0.4"
|
||||||
|
cap-std = { version = "3", optional = true }
|
||||||
|
hmac = "0.12"
|
||||||
|
sha2 = "0.10"
|
||||||
|
md-5 = "0.10"
|
||||||
|
md4 = "0.10"
|
||||||
|
aes = "0.8"
|
||||||
|
cmac = "0.7"
|
||||||
|
rc4 = "0.2"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["localfs"]
|
||||||
|
localfs = ["dep:cap-std"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
tempfile = "3"
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "integration_localfs"
|
||||||
|
path = "tests/integration_localfs.rs"
|
||||||
|
required-features = ["localfs"]
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "integration_localfs_write"
|
||||||
|
path = "tests/integration_localfs_write.rs"
|
||||||
|
required-features = ["localfs"]
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "integration_negotiate"
|
||||||
|
path = "tests/integration_negotiate.rs"
|
||||||
|
required-features = ["localfs"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
strip = true
|
||||||
238
vendor/smb-server/src/backend.rs
vendored
Normal file
238
vendor/smb-server/src/backend.rs
vendored
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
//! `ShareBackend` and `Handle` traits — the storage abstraction.
|
||||||
|
//!
|
||||||
|
//! Implementors of these traits plug into `Share::new(name, backend)`. The
|
||||||
|
//! protocol layer never exposes raw FS types to backends; everything goes
|
||||||
|
//! through validated `SmbPath`s and the small structs below.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use crate::error::{SmbError, SmbResult};
|
||||||
|
use crate::path::SmbPath;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OpenOptions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Translated SMB CREATE intent — the small set of cases v1 cares about.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum OpenIntent {
|
||||||
|
/// `FILE_OPEN` — open existing only; fail if missing.
|
||||||
|
Open,
|
||||||
|
/// `FILE_CREATE` — create new only; fail if exists.
|
||||||
|
Create,
|
||||||
|
/// `FILE_OPEN_IF` — open existing or create new.
|
||||||
|
OpenOrCreate,
|
||||||
|
/// `FILE_OVERWRITE_IF` — open existing (truncating) or create new.
|
||||||
|
OverwriteOrCreate,
|
||||||
|
/// `FILE_OVERWRITE` — open existing and truncate; fail if missing.
|
||||||
|
Truncate,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options passed to `ShareBackend::open`. v1 keeps this tight on purpose;
|
||||||
|
/// extra knobs become methods later if a backend genuinely needs them.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct OpenOptions {
|
||||||
|
/// Read access requested.
|
||||||
|
pub read: bool,
|
||||||
|
/// Write access requested.
|
||||||
|
pub write: bool,
|
||||||
|
/// CREATE disposition.
|
||||||
|
pub intent: OpenIntent,
|
||||||
|
/// `FILE_DIRECTORY_FILE` was set on CREATE — open or create a directory.
|
||||||
|
pub directory: bool,
|
||||||
|
/// `FILE_NON_DIRECTORY_FILE` was set on CREATE — fail if the target is a directory.
|
||||||
|
pub non_directory: bool,
|
||||||
|
/// `FILE_DELETE_ON_CLOSE` was set on CREATE.
|
||||||
|
pub delete_on_close: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OpenOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
read: true,
|
||||||
|
write: false,
|
||||||
|
intent: OpenIntent::Open,
|
||||||
|
directory: false,
|
||||||
|
non_directory: false,
|
||||||
|
delete_on_close: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileInfo / DirEntry / FileTimes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Filesystem-style metadata for a single file or directory.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FileInfo {
|
||||||
|
/// Display name (last component). For QUERY_INFO at the share root this
|
||||||
|
/// is the share name.
|
||||||
|
pub name: String,
|
||||||
|
/// File size in bytes.
|
||||||
|
pub end_of_file: u64,
|
||||||
|
/// Allocation size — typically `end_of_file` rounded up to a cluster size.
|
||||||
|
/// v1 backends may safely return the same value as `end_of_file`.
|
||||||
|
pub allocation_size: u64,
|
||||||
|
/// FILETIME (100ns ticks since 1601).
|
||||||
|
pub creation_time: u64,
|
||||||
|
pub last_access_time: u64,
|
||||||
|
pub last_write_time: u64,
|
||||||
|
pub change_time: u64,
|
||||||
|
/// True if this is a directory.
|
||||||
|
pub is_directory: bool,
|
||||||
|
/// Optional 64-bit unique file id (for `FileInternalInformation`). v1 may
|
||||||
|
/// return `0` if unavailable; the dispatcher will substitute the FileId.
|
||||||
|
pub file_index: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileInfo {
|
||||||
|
/// SMB2 file attributes (MS-FSCC §2.6) for this file. v1 returns
|
||||||
|
/// `FILE_ATTRIBUTE_DIRECTORY` for dirs, `FILE_ATTRIBUTE_NORMAL` (0x80) for
|
||||||
|
/// regular files. (`FILE_ATTRIBUTE_NORMAL` MUST be the only attribute set
|
||||||
|
/// when used.)
|
||||||
|
pub fn attributes(&self) -> u32 {
|
||||||
|
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
|
||||||
|
const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
|
||||||
|
if self.is_directory {
|
||||||
|
FILE_ATTRIBUTE_DIRECTORY
|
||||||
|
} else {
|
||||||
|
FILE_ATTRIBUTE_NORMAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One entry of a directory listing.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DirEntry {
|
||||||
|
pub info: FileInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional FILETIME values for `set_times`. `None` means "leave unchanged".
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct FileTimes {
|
||||||
|
pub creation_time: Option<u64>,
|
||||||
|
pub last_access_time: Option<u64>,
|
||||||
|
pub last_write_time: Option<u64>,
|
||||||
|
pub change_time: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileTimes {
|
||||||
|
/// Convenience: convert `SystemTime` into a `FileTimes` setting all four
|
||||||
|
/// fields to the same instant.
|
||||||
|
pub fn all(t: SystemTime) -> Self {
|
||||||
|
let ft = crate::utils::system_time_to_filetime(t);
|
||||||
|
Self {
|
||||||
|
creation_time: Some(ft),
|
||||||
|
last_access_time: Some(ft),
|
||||||
|
last_write_time: Some(ft),
|
||||||
|
change_time: Some(ft),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// BackendCapabilities
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Static, advertised capabilities of a backend.
|
||||||
|
///
|
||||||
|
/// Kept small intentionally — extending requires discussing with the maintainer.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct BackendCapabilities {
|
||||||
|
/// If true, all write-class operations are denied at the protocol layer
|
||||||
|
/// before reaching the backend (matches `LocalFsBackend::read_only()`).
|
||||||
|
pub is_read_only: bool,
|
||||||
|
/// True iff the backend treats names case-sensitively.
|
||||||
|
pub case_sensitive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Traits
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Pluggable storage backend mounted as a share.
|
||||||
|
///
|
||||||
|
/// Implementors must be `Send + Sync + 'static` so the server can spawn
|
||||||
|
/// per-request handlers freely.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ShareBackend: Send + Sync + 'static {
|
||||||
|
/// Open or create a file or directory. Returns a fresh handle.
|
||||||
|
async fn open(&self, path: &SmbPath, opts: OpenOptions) -> SmbResult<Box<dyn Handle>>;
|
||||||
|
|
||||||
|
/// Unlink (delete) a file. Directories: must be empty. v1 does not
|
||||||
|
/// recursively delete.
|
||||||
|
async fn unlink(&self, path: &SmbPath) -> SmbResult<()>;
|
||||||
|
|
||||||
|
/// Rename `from` to `to`. The backend must reject if `to` already exists.
|
||||||
|
async fn rename(&self, from: &SmbPath, to: &SmbPath) -> SmbResult<()>;
|
||||||
|
|
||||||
|
/// Static capabilities. The dispatcher consults these at TREE_CONNECT and
|
||||||
|
/// uses `is_read_only` to clamp authz.
|
||||||
|
fn capabilities(&self) -> BackendCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A live open file or directory handle.
|
||||||
|
///
|
||||||
|
/// One handle per `CREATE`. The handle is dropped when CLOSE arrives or the
|
||||||
|
/// session goes away.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Handle: Send + Sync {
|
||||||
|
/// Read up to `len` bytes at `offset`. May return fewer.
|
||||||
|
async fn read(&self, offset: u64, len: u32) -> SmbResult<bytes::Bytes>;
|
||||||
|
|
||||||
|
/// Write `data` at `offset`. Returns bytes written.
|
||||||
|
async fn write(&self, offset: u64, data: &[u8]) -> SmbResult<u32>;
|
||||||
|
|
||||||
|
/// Write owned `data` at `offset`. Backends that need ownership across a
|
||||||
|
/// blocking boundary can override this to avoid an extra copy.
|
||||||
|
async fn write_owned(&self, offset: u64, data: Vec<u8>) -> SmbResult<u32> {
|
||||||
|
self.write(offset, &data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flush buffered writes. May be a no-op on backends that always flush.
|
||||||
|
async fn flush(&self) -> SmbResult<()>;
|
||||||
|
|
||||||
|
/// Stat: current file info.
|
||||||
|
async fn stat(&self) -> SmbResult<FileInfo>;
|
||||||
|
|
||||||
|
/// Set timestamps. `None` fields leave the corresponding field alone.
|
||||||
|
async fn set_times(&self, times: FileTimes) -> SmbResult<()>;
|
||||||
|
|
||||||
|
/// Truncate (or extend) to `len` bytes. For directories: the protocol
|
||||||
|
/// layer rejects this before reaching the backend.
|
||||||
|
async fn truncate(&self, len: u64) -> SmbResult<()>;
|
||||||
|
|
||||||
|
/// List directory entries matching the optional pattern. v1 ignores
|
||||||
|
/// `pattern` if the backend doesn't implement matching — the dispatcher
|
||||||
|
/// post-filters as needed for QUERY_DIRECTORY.
|
||||||
|
async fn list_dir(&self, pattern: Option<&str>) -> SmbResult<Vec<DirEntry>>;
|
||||||
|
|
||||||
|
/// Close the handle. Boxed self lets implementors consume internal state.
|
||||||
|
async fn close(self: Box<Self>) -> SmbResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No-op backend used for the synthetic IPC$ share. Every method returns
|
||||||
|
/// [`SmbError::NotSupported`]. Exists so we can hand a `ShareBackend`
|
||||||
|
/// implementor to the IPC$ tree without any real storage attached.
|
||||||
|
pub(crate) struct NotSupportedBackend;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ShareBackend for NotSupportedBackend {
|
||||||
|
async fn open(&self, _path: &SmbPath, _opts: OpenOptions) -> SmbResult<Box<dyn Handle>> {
|
||||||
|
Err(SmbError::NotSupported)
|
||||||
|
}
|
||||||
|
async fn unlink(&self, _path: &SmbPath) -> SmbResult<()> {
|
||||||
|
Err(SmbError::NotSupported)
|
||||||
|
}
|
||||||
|
async fn rename(&self, _from: &SmbPath, _to: &SmbPath) -> SmbResult<()> {
|
||||||
|
Err(SmbError::NotSupported)
|
||||||
|
}
|
||||||
|
fn capabilities(&self) -> BackendCapabilities {
|
||||||
|
BackendCapabilities {
|
||||||
|
is_read_only: true,
|
||||||
|
case_sensitive: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
259
vendor/smb-server/src/builder.rs
vendored
Normal file
259
vendor/smb-server/src/builder.rs
vendored
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
//! Public builder API for `SmbServer` and `Share`.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::backend::ShareBackend;
|
||||||
|
use crate::server::{ServerConfig, ServerState, ServerUsers, ShareBindings, ShareMode, SmbServer};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Access
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Access level granted to a user on a share, or to anonymous on a public
|
||||||
|
/// share.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Access {
|
||||||
|
Read,
|
||||||
|
ReadWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Access {
|
||||||
|
pub fn allows_write(self) -> bool {
|
||||||
|
matches!(self, Access::ReadWrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp_to(self, cap: Access) -> Access {
|
||||||
|
match (self, cap) {
|
||||||
|
(Access::ReadWrite, Access::ReadWrite) => Access::ReadWrite,
|
||||||
|
_ => Access::Read,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Share
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// One share definition, attached to a single backend.
|
||||||
|
pub struct Share {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) backend: Arc<dyn ShareBackend>,
|
||||||
|
pub(crate) mode: ShareMode,
|
||||||
|
pub(crate) users: HashMap<String, Access>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Share {
|
||||||
|
/// Build a new share with the given name and backend.
|
||||||
|
pub fn new(name: impl Into<String>, backend: impl ShareBackend) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
backend: Arc::new(backend),
|
||||||
|
mode: ShareMode::AuthenticatedOnly,
|
||||||
|
users: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Anonymous + authenticated read+write.
|
||||||
|
pub fn public(mut self) -> Self {
|
||||||
|
self.mode = ShareMode::Public;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Anonymous + authenticated read-only.
|
||||||
|
pub fn public_read_only(mut self) -> Self {
|
||||||
|
self.mode = ShareMode::PublicReadOnly;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grant `access` to the given (already-registered) user. Multiple calls
|
||||||
|
/// accumulate.
|
||||||
|
pub fn user(mut self, name: impl Into<String>, access: Access) -> Self {
|
||||||
|
self.users.insert(name.into(), access);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// BuildError
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Errors raised by `SmbServerBuilder::build`.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum BuildError {
|
||||||
|
#[error("listen address must be set")]
|
||||||
|
MissingListenAddr,
|
||||||
|
#[error("share `{0}` is declared more than once")]
|
||||||
|
DuplicateShare(String),
|
||||||
|
#[error("share `{0}` mixes .public()/.public_read_only() with explicit .user(...) entries")]
|
||||||
|
PublicMixedWithUsers(String),
|
||||||
|
#[error("share `{0}` calls `.public*()` more than once")]
|
||||||
|
DoublePublic(String),
|
||||||
|
#[error("share `{share}` references unknown user `{user}`")]
|
||||||
|
UnknownUser { share: String, user: String },
|
||||||
|
#[error("user `{0}` is registered twice")]
|
||||||
|
DuplicateUser(String),
|
||||||
|
#[error("user name `{0}` is reserved (use .public()/.public_read_only() for anonymous)")]
|
||||||
|
ReservedUserName(String),
|
||||||
|
#[error("user name must be non-empty")]
|
||||||
|
EmptyUserName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SmbServerBuilder
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Builder for `SmbServer`. See `SmbServer::builder`.
|
||||||
|
pub struct SmbServerBuilder {
|
||||||
|
listen_addr: Option<SocketAddr>,
|
||||||
|
users: HashMap<String, String>, // name -> password
|
||||||
|
user_order: Vec<String>,
|
||||||
|
shares: Vec<Share>,
|
||||||
|
netbios_name: Option<String>,
|
||||||
|
max_read_size: u32,
|
||||||
|
max_write_size: u32,
|
||||||
|
server_guid: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SmbServerBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmbServerBuilder {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
listen_addr: None,
|
||||||
|
users: HashMap::new(),
|
||||||
|
user_order: Vec::new(),
|
||||||
|
shares: Vec::new(),
|
||||||
|
netbios_name: None,
|
||||||
|
max_read_size: 1024 * 1024,
|
||||||
|
max_write_size: 1024 * 1024,
|
||||||
|
server_guid: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listen(mut self, addr: SocketAddr) -> Self {
|
||||||
|
self.listen_addr = Some(addr);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user(mut self, name: impl Into<String>, password: impl Into<String>) -> Self {
|
||||||
|
let n = name.into();
|
||||||
|
if !self.users.contains_key(&n) {
|
||||||
|
self.user_order.push(n.clone());
|
||||||
|
}
|
||||||
|
self.users.insert(n, password.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share(mut self, share: Share) -> Self {
|
||||||
|
self.shares.push(share);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn netbios_name(mut self, name: impl Into<String>) -> Self {
|
||||||
|
self.netbios_name = Some(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_read_size(mut self, bytes: u32) -> Self {
|
||||||
|
self.max_read_size = bytes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_write_size(mut self, bytes: u32) -> Self {
|
||||||
|
self.max_write_size = bytes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override the random per-process server GUID. Mostly useful in tests.
|
||||||
|
pub fn server_guid(mut self, guid: Uuid) -> Self {
|
||||||
|
self.server_guid = Some(guid);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Result<SmbServer, BuildError> {
|
||||||
|
// 1. Validate users.
|
||||||
|
for name in &self.user_order {
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(BuildError::EmptyUserName);
|
||||||
|
}
|
||||||
|
if name.eq_ignore_ascii_case("anonymous") {
|
||||||
|
return Err(BuildError::ReservedUserName(name.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate shares.
|
||||||
|
let mut seen_names = std::collections::HashSet::new();
|
||||||
|
for share in &self.shares {
|
||||||
|
if !seen_names.insert(share.name.to_ascii_lowercase()) {
|
||||||
|
return Err(BuildError::DuplicateShare(share.name.clone()));
|
||||||
|
}
|
||||||
|
// Public-vs-users mutual exclusivity.
|
||||||
|
let is_public = matches!(share.mode, ShareMode::Public | ShareMode::PublicReadOnly);
|
||||||
|
if is_public && !share.users.is_empty() {
|
||||||
|
return Err(BuildError::PublicMixedWithUsers(share.name.clone()));
|
||||||
|
}
|
||||||
|
// Each per-share user must exist in the global user table.
|
||||||
|
for u in share.users.keys() {
|
||||||
|
if !self.users.contains_key(u) {
|
||||||
|
return Err(BuildError::UnknownUser {
|
||||||
|
share: share.name.clone(),
|
||||||
|
user: u.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Listen address required.
|
||||||
|
let listen = self.listen_addr.ok_or(BuildError::MissingListenAddr)?;
|
||||||
|
|
||||||
|
// 4. Decide NetBIOS name.
|
||||||
|
let netbios = self.netbios_name.unwrap_or_else(|| {
|
||||||
|
// Hostname or "SMBSERVER".
|
||||||
|
std::env::var("HOSTNAME")
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unwrap_or_else(|| "SMBSERVER".to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Build ShareBindings — keep mode + users + backend together.
|
||||||
|
let mut share_bindings: Vec<Arc<ShareBindings>> = Vec::with_capacity(self.shares.len());
|
||||||
|
for s in self.shares {
|
||||||
|
share_bindings.push(ShareBindings::new(
|
||||||
|
s.name, s.backend, s.mode, s.users, false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Materialize the user table (precompute NT hashes to avoid retaining plaintext).
|
||||||
|
let mut user_table = HashMap::new();
|
||||||
|
for name in &self.user_order {
|
||||||
|
let pw = &self.users[name];
|
||||||
|
let creds = crate::proto::auth::ntlm::UserCreds::from_password(pw);
|
||||||
|
user_table.insert(name.clone(), creds);
|
||||||
|
}
|
||||||
|
|
||||||
|
let server_guid = self.server_guid.unwrap_or_else(Uuid::new_v4);
|
||||||
|
|
||||||
|
let cfg = ServerConfig {
|
||||||
|
listen_addr: listen,
|
||||||
|
netbios_name: netbios,
|
||||||
|
max_read_size: self.max_read_size,
|
||||||
|
max_write_size: self.max_write_size,
|
||||||
|
server_guid,
|
||||||
|
};
|
||||||
|
let users = ServerUsers {
|
||||||
|
table: tokio::sync::RwLock::new(user_table),
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = ServerState::new(cfg, users, share_bindings);
|
||||||
|
Ok(SmbServer::from_state(state))
|
||||||
|
}
|
||||||
|
}
|
||||||
39
vendor/smb-server/src/conn/mod.rs
vendored
Normal file
39
vendor/smb-server/src/conn/mod.rs
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//! Per-connection task layout.
|
||||||
|
|
||||||
|
pub mod reader;
|
||||||
|
pub mod state;
|
||||||
|
pub mod writer;
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
|
use crate::server::ServerState;
|
||||||
|
use state::Connection;
|
||||||
|
|
||||||
|
/// Runs the reader and writer tasks for a single accepted connection until
|
||||||
|
/// either side hangs up. Returns once both halves are done.
|
||||||
|
pub async fn connection_loop(stream: TcpStream, server: Arc<ServerState>) -> io::Result<()> {
|
||||||
|
let (read_half, write_half) = tokio::io::split(stream);
|
||||||
|
let conn = Arc::new(Connection::new(
|
||||||
|
server.config.server_guid,
|
||||||
|
server.config.max_read_size,
|
||||||
|
server.config.max_write_size,
|
||||||
|
));
|
||||||
|
let conn_id = server.active_connections.register(&conn).await;
|
||||||
|
let (tx, rx) = mpsc::channel::<writer::FramePayload>(writer::WRITER_CHANNEL);
|
||||||
|
|
||||||
|
let writer_handle = tokio::spawn(writer::writer_task(write_half, rx));
|
||||||
|
|
||||||
|
info!("connection accepted");
|
||||||
|
let reader_result = reader::reader_task(read_half, server.clone(), conn.clone(), tx).await;
|
||||||
|
debug!(?reader_result, "reader exited");
|
||||||
|
// Wait for writer to drain.
|
||||||
|
let _ = writer_handle.await;
|
||||||
|
server.active_connections.unregister(conn_id).await;
|
||||||
|
info!("connection closed");
|
||||||
|
reader_result
|
||||||
|
}
|
||||||
80
vendor/smb-server/src/conn/reader.rs
vendored
Normal file
80
vendor/smb-server/src/conn/reader.rs
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
//! Per-connection frame reader: pulls bytes off the socket, frames them,
|
||||||
|
//! hands each frame to the dispatcher.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::framing::{FRAME_HEADER_LEN, decode_frame_header};
|
||||||
|
use tokio::io::{AsyncReadExt, ReadHalf};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
/// Read one frame's payload (without the 4-byte length prefix).
|
||||||
|
///
|
||||||
|
/// Returns `Ok(None)` on a clean EOF, `Ok(Some(bytes))` on a complete frame,
|
||||||
|
/// `Err` on partial/garbled data.
|
||||||
|
pub async fn read_one_frame(reader: &mut ReadHalf<TcpStream>) -> io::Result<Option<Vec<u8>>> {
|
||||||
|
let mut hdr = [0u8; FRAME_HEADER_LEN];
|
||||||
|
match reader.read_exact(&mut hdr).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
}
|
||||||
|
let len = match decode_frame_header(&hdr) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut payload = vec![0u8; len as usize];
|
||||||
|
reader.read_exact(&mut payload).await?;
|
||||||
|
Ok(Some(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Continuously read frames; for each, await `dispatch_one`'s response and
|
||||||
|
/// route it to the writer.
|
||||||
|
///
|
||||||
|
/// Sequential dispatch keeps v1 simple and matches the spec's "single writer
|
||||||
|
/// task / per-frame dispatch" pattern. We process one frame at a time per
|
||||||
|
/// connection in v1 — a follow-up can spawn dispatch tasks if a workload
|
||||||
|
/// proves to need credit-window concurrency.
|
||||||
|
pub async fn reader_task(
|
||||||
|
mut reader: ReadHalf<TcpStream>,
|
||||||
|
server: Arc<ServerState>,
|
||||||
|
conn: Arc<Connection>,
|
||||||
|
tx: tokio::sync::mpsc::Sender<crate::conn::writer::FramePayload>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
loop {
|
||||||
|
let frame = match read_one_frame(&mut reader).await {
|
||||||
|
Ok(Some(b)) => b,
|
||||||
|
Ok(None) => {
|
||||||
|
debug!("client closed connection");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "frame read error");
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Check shutdown after every frame.
|
||||||
|
if server
|
||||||
|
.shutting_down
|
||||||
|
.load(std::sync::atomic::Ordering::Acquire)
|
||||||
|
{
|
||||||
|
debug!("server shutting down; dropping connection");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// The dispatcher is async but we await it inline — order-preserving and
|
||||||
|
// good enough for v1.
|
||||||
|
let response = crate::dispatch::dispatch_frame(&server, &conn, &frame).await;
|
||||||
|
if let Some(bytes) = response
|
||||||
|
&& tx.send(bytes).await.is_err()
|
||||||
|
{
|
||||||
|
debug!("writer channel closed; reader exiting");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
328
vendor/smb-server/src/conn/state.rs
vendored
Normal file
328
vendor/smb-server/src/conn/state.rs
vendored
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
//! Connection / session / tree / open state held during a single TCP
|
||||||
|
//! connection's lifetime.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use crate::proto::auth::ntlm::{Identity, NtlmServer};
|
||||||
|
use crate::proto::crypto::{PreauthIntegrity, SigningAlgo};
|
||||||
|
use crate::proto::messages::{Dialect, FileId};
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::backend::Handle;
|
||||||
|
use crate::builder::Access;
|
||||||
|
use crate::path::SmbPath;
|
||||||
|
use crate::server::ShareBindings;
|
||||||
|
|
||||||
|
/// In-flight NTLM acceptor + a `is_raw_ntlmssp` flag (true = raw, false =
|
||||||
|
/// SPNEGO-wrapped). The handler hands the second-round response back in the
|
||||||
|
/// same form the client opened with.
|
||||||
|
pub type PendingAuth = Arc<Mutex<(NtlmServer, bool)>>;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Connection
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// One connection's negotiated state and its session/tree/open tables.
|
||||||
|
pub struct Connection {
|
||||||
|
pub server_guid: Uuid,
|
||||||
|
pub client_guid: tokio::sync::RwLock<Uuid>,
|
||||||
|
pub dialect: tokio::sync::RwLock<Option<Dialect>>,
|
||||||
|
pub signing_algo: tokio::sync::RwLock<SigningAlgo>,
|
||||||
|
/// Connection.PreauthIntegrityHashValue after NEGOTIATE. SMB 3.1.1
|
||||||
|
/// SESSION_SETUP exchanges fork this into `session_preauth`.
|
||||||
|
pub preauth: Mutex<PreauthIntegrity>,
|
||||||
|
/// Granted at NEGOTIATE: large MTU support flag etc.
|
||||||
|
pub max_read_size: tokio::sync::RwLock<u32>,
|
||||||
|
pub max_write_size: tokio::sync::RwLock<u32>,
|
||||||
|
|
||||||
|
/// Sessions keyed by SessionId.
|
||||||
|
pub sessions: RwLock<HashMap<u64, Arc<RwLock<Session>>>>,
|
||||||
|
|
||||||
|
/// In-flight NTLM acceptors keyed by SessionId. We keep them out of
|
||||||
|
/// `Session` because a session is created only after a successful first
|
||||||
|
/// SESSION_SETUP round — between rounds the entry lives here. The
|
||||||
|
/// `bool` records whether the client sent raw NTLMSSP (true) or
|
||||||
|
/// SPNEGO-wrapped (false) so the second-round response matches form.
|
||||||
|
pub pending_auths: RwLock<HashMap<u64, PendingAuth>>,
|
||||||
|
|
||||||
|
/// In-flight SMB 3.1.1 preauth state keyed by SessionId during
|
||||||
|
/// multi-leg SESSION_SETUP.
|
||||||
|
pub session_preauth: RwLock<HashMap<u64, PreauthIntegrity>>,
|
||||||
|
|
||||||
|
/// Monotonic SessionId allocator.
|
||||||
|
next_session_id: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Connection {
|
||||||
|
pub fn new(server_guid: Uuid, max_read_size: u32, max_write_size: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
server_guid,
|
||||||
|
client_guid: tokio::sync::RwLock::new(Uuid::nil()),
|
||||||
|
dialect: tokio::sync::RwLock::new(None),
|
||||||
|
signing_algo: tokio::sync::RwLock::new(SigningAlgo::HmacSha256),
|
||||||
|
preauth: Mutex::new(PreauthIntegrity::new()),
|
||||||
|
max_read_size: tokio::sync::RwLock::new(max_read_size),
|
||||||
|
max_write_size: tokio::sync::RwLock::new(max_write_size),
|
||||||
|
sessions: RwLock::new(HashMap::new()),
|
||||||
|
pending_auths: RwLock::new(HashMap::new()),
|
||||||
|
session_preauth: RwLock::new(HashMap::new()),
|
||||||
|
next_session_id: AtomicU64::new(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alloc_session_id(&self) -> u64 {
|
||||||
|
self.next_session_id.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close_session(&self, session_id: u64) -> bool {
|
||||||
|
let removed = {
|
||||||
|
let mut sessions = self.sessions.write().await;
|
||||||
|
sessions.remove(&session_id)
|
||||||
|
};
|
||||||
|
if let Some(sess_arc) = removed {
|
||||||
|
close_session_state(&sess_arc).await;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close_tree(&self, session_id: u64, tree_id: u32) -> bool {
|
||||||
|
let sess_arc = {
|
||||||
|
let sessions = self.sessions.read().await;
|
||||||
|
sessions.get(&session_id).cloned()
|
||||||
|
};
|
||||||
|
let Some(sess_arc) = sess_arc else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
remove_tree_from_session(&sess_arc, tree_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close_sessions_for_user(&self, user: &str) -> usize {
|
||||||
|
let to_remove = {
|
||||||
|
let sessions = self.sessions.read().await;
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
for (session_id, sess_arc) in sessions.iter() {
|
||||||
|
let sess = sess_arc.read().await;
|
||||||
|
if matches!(&sess.identity, Identity::User { user: session_user, .. } if session_user == user)
|
||||||
|
{
|
||||||
|
ids.push(*session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut removed = 0;
|
||||||
|
for session_id in to_remove {
|
||||||
|
if self.close_session(session_id).await {
|
||||||
|
removed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close_trees_for_share(&self, share_name: &str) -> usize {
|
||||||
|
self.close_matching_trees(|_, tree| tree.share.name.eq_ignore_ascii_case(share_name))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close_trees_for_user_share(&self, user: &str, share_name: &str) -> usize {
|
||||||
|
self.close_matching_trees(|sess, tree| {
|
||||||
|
matches!(&sess.identity, Identity::User { user: session_user, .. } if session_user == user)
|
||||||
|
&& tree.share.name.eq_ignore_ascii_case(share_name)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close_matching_trees(
|
||||||
|
&self,
|
||||||
|
matches_tree: impl Fn(&Session, &TreeConnect) -> bool,
|
||||||
|
) -> usize {
|
||||||
|
let sessions: Vec<_> = {
|
||||||
|
let sessions = self.sessions.read().await;
|
||||||
|
sessions.values().cloned().collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut removed = 0;
|
||||||
|
for sess_arc in sessions {
|
||||||
|
let tree_ids = {
|
||||||
|
let sess = sess_arc.read().await;
|
||||||
|
let trees = sess.trees.read().await;
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
for (tree_id, tree_arc) in trees.iter() {
|
||||||
|
let tree = tree_arc.read().await;
|
||||||
|
if matches_tree(&sess, &tree) {
|
||||||
|
ids.push(*tree_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ids
|
||||||
|
};
|
||||||
|
|
||||||
|
for tree_id in tree_ids {
|
||||||
|
if remove_tree_from_session(&sess_arc, tree_id).await {
|
||||||
|
removed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close_session_state(sess_arc: &Arc<RwLock<Session>>) {
|
||||||
|
let sess = sess_arc.write().await;
|
||||||
|
let trees: Vec<_> = sess.trees.write().await.drain().collect();
|
||||||
|
for (_tree_id, tree_arc) in trees {
|
||||||
|
close_tree_state(&tree_arc).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_tree_from_session(sess_arc: &Arc<RwLock<Session>>, tree_id: u32) -> bool {
|
||||||
|
let removed = {
|
||||||
|
let sess = sess_arc.read().await;
|
||||||
|
let mut trees = sess.trees.write().await;
|
||||||
|
trees.remove(&tree_id)
|
||||||
|
};
|
||||||
|
if let Some(tree_arc) = removed {
|
||||||
|
close_tree_state(&tree_arc).await;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close_tree_state(tree_arc: &Arc<RwLock<TreeConnect>>) {
|
||||||
|
let tree = tree_arc.write().await;
|
||||||
|
let opens: Vec<_> = tree.opens.write().await.drain().collect();
|
||||||
|
for (_fid, open_arc) in opens {
|
||||||
|
let mut open = open_arc.write().await;
|
||||||
|
if let Some(handle) = open.handle.take() {
|
||||||
|
let _ = handle.close().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Session
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct Session {
|
||||||
|
pub id: u64,
|
||||||
|
pub identity: Identity,
|
||||||
|
pub session_base_key: [u8; 16],
|
||||||
|
pub signing_key: [u8; 16],
|
||||||
|
/// Whether signing is required for this session's traffic.
|
||||||
|
pub signing_required: bool,
|
||||||
|
pub trees: RwLock<HashMap<u32, Arc<RwLock<TreeConnect>>>>,
|
||||||
|
/// 3.1.1: snapshot taken at SESSION_SETUP completion (after the request
|
||||||
|
/// hash but before the response is hashed). Used as KDF context.
|
||||||
|
pub preauth_snapshot: Option<[u8; 64]>,
|
||||||
|
|
||||||
|
next_tree_id: AtomicU32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
pub fn new(
|
||||||
|
id: u64,
|
||||||
|
identity: Identity,
|
||||||
|
session_base_key: [u8; 16],
|
||||||
|
signing_key: [u8; 16],
|
||||||
|
signing_required: bool,
|
||||||
|
preauth_snapshot: Option<[u8; 64]>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
identity,
|
||||||
|
session_base_key,
|
||||||
|
signing_key,
|
||||||
|
signing_required,
|
||||||
|
trees: RwLock::new(HashMap::new()),
|
||||||
|
preauth_snapshot,
|
||||||
|
next_tree_id: AtomicU32::new(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alloc_tree_id(&self) -> u32 {
|
||||||
|
self.next_tree_id.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_anonymous(&self) -> bool {
|
||||||
|
matches!(self.identity, Identity::Anonymous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// TreeConnect
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct TreeConnect {
|
||||||
|
pub id: u32,
|
||||||
|
pub share: Arc<ShareBindings>,
|
||||||
|
pub granted_access: Access,
|
||||||
|
pub opens: RwLock<HashMap<FileId, Arc<RwLock<Open>>>>,
|
||||||
|
next_volatile: AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TreeConnect {
|
||||||
|
pub fn new(id: u32, share: Arc<ShareBindings>, granted_access: Access) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
share,
|
||||||
|
granted_access,
|
||||||
|
opens: RwLock::new(HashMap::new()),
|
||||||
|
next_volatile: AtomicU64::new(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alloc_file_id(&self) -> FileId {
|
||||||
|
let v = self.next_volatile.fetch_add(1, Ordering::Relaxed);
|
||||||
|
FileId::new(v, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Open / DirCursor
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub struct Open {
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub handle: Option<Box<dyn Handle>>,
|
||||||
|
pub granted_access: Access,
|
||||||
|
pub last_path: SmbPath,
|
||||||
|
pub is_directory: bool,
|
||||||
|
pub delete_on_close: bool,
|
||||||
|
pub search_state: Option<DirCursor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Open {
|
||||||
|
pub fn new(
|
||||||
|
file_id: FileId,
|
||||||
|
handle: Box<dyn Handle>,
|
||||||
|
granted_access: Access,
|
||||||
|
last_path: SmbPath,
|
||||||
|
is_directory: bool,
|
||||||
|
delete_on_close: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
file_id,
|
||||||
|
handle: Some(handle),
|
||||||
|
granted_access,
|
||||||
|
last_path,
|
||||||
|
is_directory,
|
||||||
|
delete_on_close,
|
||||||
|
search_state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator state for a directory listing across multiple QUERY_DIRECTORY
|
||||||
|
/// calls. We snapshot the entries once and consume them in order; subsequent
|
||||||
|
/// calls advance `next` until exhaustion.
|
||||||
|
pub struct DirCursor {
|
||||||
|
pub entries: Vec<crate::backend::DirEntry>,
|
||||||
|
pub next: usize,
|
||||||
|
/// The pattern fixed on the first scan; `RESTART_SCANS` resets `next`.
|
||||||
|
pub pattern: Option<String>,
|
||||||
|
}
|
||||||
32
vendor/smb-server/src/conn/writer.rs
vendored
Normal file
32
vendor/smb-server/src/conn/writer.rs
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//! Per-connection writer task: serializes responses, applies signing, and
|
||||||
|
//! frames the bytes onto the wire.
|
||||||
|
|
||||||
|
use crate::proto::framing::encode_frame;
|
||||||
|
use tokio::io::{AsyncWriteExt, WriteHalf};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
/// One packet of bytes to send. Already includes the final SMB2 header +
|
||||||
|
/// body, *with signing already applied if required*.
|
||||||
|
pub type FramePayload = Vec<u8>;
|
||||||
|
|
||||||
|
/// Writer-task channel size: large enough that a slow remote rarely backs up
|
||||||
|
/// the dispatcher.
|
||||||
|
pub const WRITER_CHANNEL: usize = 64;
|
||||||
|
|
||||||
|
pub async fn writer_task(mut writer: WriteHalf<TcpStream>, mut rx: mpsc::Receiver<FramePayload>) {
|
||||||
|
while let Some(payload) = rx.recv().await {
|
||||||
|
let mut out = Vec::with_capacity(payload.len() + 4);
|
||||||
|
encode_frame(&payload, &mut out);
|
||||||
|
if let Err(e) = writer.write_all(&out).await {
|
||||||
|
error!(error = %e, "writer task: socket write failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debug!(len = out.len(), "wrote frame");
|
||||||
|
}
|
||||||
|
// Channel closed — flush and bail.
|
||||||
|
if let Err(e) = writer.shutdown().await {
|
||||||
|
debug!(error = %e, "writer shutdown error (best-effort)");
|
||||||
|
}
|
||||||
|
}
|
||||||
656
vendor/smb-server/src/dispatch.rs
vendored
Normal file
656
vendor/smb-server/src/dispatch.rs
vendored
Normal file
@@ -0,0 +1,656 @@
|
|||||||
|
//! Per-frame dispatch: parse header, route to handler, sign response, encode.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::auth::ntlm::Identity;
|
||||||
|
use crate::proto::crypto::{PreauthIntegrity, sign};
|
||||||
|
use crate::proto::header::{
|
||||||
|
Command, HeaderTail, SMB2_FLAGS_ASYNC_COMMAND, SMB2_FLAGS_RELATED_OPERATIONS,
|
||||||
|
SMB2_FLAGS_SERVER_TO_REDIR, SMB2_FLAGS_SIGNED, SMB2_HEADER_LEN, Smb2Header,
|
||||||
|
};
|
||||||
|
use crate::proto::messages::ErrorResponse;
|
||||||
|
use tracing::{Instrument, debug, debug_span, error, warn};
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::handlers;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
/// Result of a handler: a complete (unsigned) response payload + the NTSTATUS
|
||||||
|
/// to set in the header. The dispatcher patches the header, applies signing
|
||||||
|
/// (if required), and ships the bytes.
|
||||||
|
pub struct HandlerResponse {
|
||||||
|
/// Bytes after the SMB2 header — the body. The handler owns body
|
||||||
|
/// construction.
|
||||||
|
pub body: Vec<u8>,
|
||||||
|
/// NTSTATUS for the response header.
|
||||||
|
pub status: u32,
|
||||||
|
/// Optional override for `tree_id` on the response header (e.g.
|
||||||
|
/// TREE_CONNECT returns the freshly minted tree id).
|
||||||
|
pub override_tree_id: Option<u32>,
|
||||||
|
/// Optional override for `session_id` on the response header (e.g.
|
||||||
|
/// SESSION_SETUP returns the freshly minted session id).
|
||||||
|
pub override_session_id: Option<u64>,
|
||||||
|
/// If true, the dispatcher will not sign the response. Used for
|
||||||
|
/// pre-session-setup messages where no key exists yet.
|
||||||
|
pub skip_signing: bool,
|
||||||
|
/// If set, take the per-session 3.1.1 preauth snapshot after hashing the
|
||||||
|
/// SESSION_SETUP request but before hashing the response. Set by
|
||||||
|
/// SESSION_SETUP on the round that produces STATUS_SUCCESS, so the
|
||||||
|
/// session's KDF context can use the snapshot.
|
||||||
|
pub take_preauth_snapshot_for_session: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HandlerResponse {
|
||||||
|
pub fn ok(body: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
body,
|
||||||
|
status: ntstatus::STATUS_SUCCESS,
|
||||||
|
override_tree_id: None,
|
||||||
|
override_session_id: None,
|
||||||
|
skip_signing: false,
|
||||||
|
take_preauth_snapshot_for_session: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn err(status: u32) -> Self {
|
||||||
|
let er = ErrorResponse::status(status);
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
er.write_to(&mut buf).expect("error response encodes");
|
||||||
|
Self {
|
||||||
|
body: buf,
|
||||||
|
status,
|
||||||
|
override_tree_id: None,
|
||||||
|
override_session_id: None,
|
||||||
|
skip_signing: false,
|
||||||
|
take_preauth_snapshot_for_session: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top-level frame dispatch. Returns the bytes to push into the writer
|
||||||
|
/// channel, or `None` if the request elicits no response (CANCEL).
|
||||||
|
pub async fn dispatch_frame(
|
||||||
|
server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
frame: &[u8],
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
// SMB1 multi-protocol bootstrap (MS-SMB2 §3.3.5.3.1). The only SMB1 we
|
||||||
|
// accept: a NEGOTIATE_REQUEST listing "SMB 2.???" or "SMB 2.002".
|
||||||
|
// Reply with an SMB2 NEGOTIATE response and the client follows up with
|
||||||
|
// a real SMB2 NEGOTIATE.
|
||||||
|
if let Some(bytes) = handle_smb1_multi_protocol(server, conn, frame).await {
|
||||||
|
return Some(bytes);
|
||||||
|
}
|
||||||
|
if frame.len() < SMB2_HEADER_LEN {
|
||||||
|
warn!(len = frame.len(), "frame too short for SMB2 header");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sub_offset = 0;
|
||||||
|
let mut responses = Vec::new();
|
||||||
|
let mut prev_session_id = 0;
|
||||||
|
let mut prev_tree_id = 0;
|
||||||
|
let mut prev_create_file_id = None;
|
||||||
|
|
||||||
|
while sub_offset < frame.len() {
|
||||||
|
let available = &frame[sub_offset..];
|
||||||
|
if available.len() < SMB2_HEADER_LEN {
|
||||||
|
warn!(remaining = available.len(), "compound tail too short");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut req_hdr, _) = match Smb2Header::parse(available) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(error = %e, "failed to parse compound sub-header");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let next = req_hdr.next_command as usize;
|
||||||
|
let sub_len = if next == 0 {
|
||||||
|
available.len()
|
||||||
|
} else if next < SMB2_HEADER_LEN || next > available.len() {
|
||||||
|
warn!(
|
||||||
|
next,
|
||||||
|
remaining = available.len(),
|
||||||
|
"invalid compound NextCommand"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
} else {
|
||||||
|
next
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut sub_frame = available[..sub_len].to_vec();
|
||||||
|
if req_hdr.flags & SMB2_FLAGS_RELATED_OPERATIONS != 0 {
|
||||||
|
inherit_related_context(
|
||||||
|
&mut sub_frame,
|
||||||
|
&mut req_hdr,
|
||||||
|
prev_session_id,
|
||||||
|
prev_tree_id,
|
||||||
|
prev_create_file_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_session_id = req_hdr.session_id;
|
||||||
|
prev_tree_id = req_hdr.tree_id().unwrap_or(0);
|
||||||
|
|
||||||
|
if let Some(response) = dispatch_one(server, conn, &sub_frame).await {
|
||||||
|
if req_hdr.command == Command::Create {
|
||||||
|
prev_create_file_id = capture_create_file_id(&response);
|
||||||
|
}
|
||||||
|
responses.push(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
if next == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sub_offset += next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if responses.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(stitch_responses(conn, responses).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inherit_related_context(
|
||||||
|
sub_frame: &mut [u8],
|
||||||
|
req_hdr: &mut Smb2Header,
|
||||||
|
prev_session_id: u64,
|
||||||
|
prev_tree_id: u32,
|
||||||
|
prev_create_file_id: Option<[u8; 16]>,
|
||||||
|
) {
|
||||||
|
if read_u64(sub_frame, 0x28) == u64::MAX {
|
||||||
|
sub_frame[0x28..0x30].copy_from_slice(&prev_session_id.to_le_bytes());
|
||||||
|
req_hdr.session_id = prev_session_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if read_u32(sub_frame, 0x24) == u32::MAX {
|
||||||
|
sub_frame[0x24..0x28].copy_from_slice(&prev_tree_id.to_le_bytes());
|
||||||
|
if let HeaderTail::Sync { reserved, .. } = req_hdr.tail {
|
||||||
|
req_hdr.tail = HeaderTail::Sync {
|
||||||
|
reserved,
|
||||||
|
tree_id: prev_tree_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(file_id) = prev_create_file_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(body_offset) = file_id_body_offset(req_hdr.command) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let offset = SMB2_HEADER_LEN + body_offset;
|
||||||
|
if offset + 16 <= sub_frame.len()
|
||||||
|
&& read_u64(sub_frame, offset) == u64::MAX
|
||||||
|
&& read_u64(sub_frame, offset + 8) == u64::MAX
|
||||||
|
{
|
||||||
|
sub_frame[offset..offset + 16].copy_from_slice(&file_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_id_body_offset(command: Command) -> Option<usize> {
|
||||||
|
match command {
|
||||||
|
Command::Close
|
||||||
|
| Command::Flush
|
||||||
|
| Command::Lock
|
||||||
|
| Command::Ioctl
|
||||||
|
| Command::QueryDirectory
|
||||||
|
| Command::ChangeNotify
|
||||||
|
| Command::OplockBreak => Some(8),
|
||||||
|
Command::Read | Command::Write => Some(16),
|
||||||
|
Command::QueryInfo => Some(24),
|
||||||
|
Command::SetInfo => Some(16),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capture_create_file_id(response: &[u8]) -> Option<[u8; 16]> {
|
||||||
|
if response.len() < SMB2_HEADER_LEN + 80 || read_u32(response, 0x08) != ntstatus::STATUS_SUCCESS
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file_id = [0u8; 16];
|
||||||
|
let offset = SMB2_HEADER_LEN + 64;
|
||||||
|
file_id.copy_from_slice(&response[offset..offset + 16]);
|
||||||
|
Some(file_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stitch_responses(conn: &Arc<Connection>, responses: Vec<Vec<u8>>) -> Vec<u8> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut ranges = Vec::with_capacity(responses.len());
|
||||||
|
let response_count = responses.len();
|
||||||
|
|
||||||
|
for (index, mut response) in responses.into_iter().enumerate() {
|
||||||
|
let start = out.len();
|
||||||
|
let actual_len = response.len();
|
||||||
|
if index + 1 < response_count {
|
||||||
|
let next = align_8(actual_len);
|
||||||
|
response[0x14..0x18].copy_from_slice(&(next as u32).to_le_bytes());
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&response);
|
||||||
|
ranges.push((start, actual_len));
|
||||||
|
|
||||||
|
if index + 1 < response_count {
|
||||||
|
out.resize(start + align_8(actual_len), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let algo = *conn.signing_algo.read().await;
|
||||||
|
for (start, len) in ranges {
|
||||||
|
let flags = read_u32(&out, start + 0x10);
|
||||||
|
if flags & SMB2_FLAGS_SIGNED == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_id = read_u64(&out, start + 0x28);
|
||||||
|
let key = {
|
||||||
|
let sessions = conn.sessions.read().await;
|
||||||
|
sessions.get(&session_id).cloned()
|
||||||
|
};
|
||||||
|
let Some(session) = key else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let session = session.read().await;
|
||||||
|
if matches!(session.identity, Identity::Anonymous) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let signing_key = session.signing_key;
|
||||||
|
drop(session);
|
||||||
|
|
||||||
|
if let Err(e) = sign(&mut out[start..start + len], &signing_key, algo) {
|
||||||
|
error!(error = %e, "failed to sign compound response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn align_8(n: usize) -> usize {
|
||||||
|
(n + 7) & !7
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(buf: &[u8], offset: usize) -> u32 {
|
||||||
|
let mut bytes = [0u8; 4];
|
||||||
|
bytes.copy_from_slice(&buf[offset..offset + 4]);
|
||||||
|
u32::from_le_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u64(buf: &[u8], offset: usize) -> u64 {
|
||||||
|
let mut bytes = [0u8; 8];
|
||||||
|
bytes.copy_from_slice(&buf[offset..offset + 8]);
|
||||||
|
u64::from_le_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dispatch_one(
|
||||||
|
server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
frame: &[u8],
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
let (req_hdr, body_bytes) = match Smb2Header::parse(frame) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(error = %e, "failed to parse header");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let cmd = req_hdr.command;
|
||||||
|
let mid = req_hdr.message_id;
|
||||||
|
let sid = req_hdr.session_id;
|
||||||
|
let tid = req_hdr.tree_id().unwrap_or(0);
|
||||||
|
|
||||||
|
let span = debug_span!("dispatch", cmd = ?cmd, mid, sid, tid);
|
||||||
|
async move {
|
||||||
|
debug!("dispatch start");
|
||||||
|
|
||||||
|
// Verify signature on incoming request (when applicable).
|
||||||
|
if let Err(status) = verify_request_signature(server, conn, &req_hdr, frame).await {
|
||||||
|
return Some(build_response_bytes(conn, &req_hdr, HandlerResponse::err(status)).await);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CANCEL is fire-and-forget — no response.
|
||||||
|
if cmd == Command::Cancel {
|
||||||
|
debug!("CANCEL received; no response");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dialect = *conn.dialect.read().await;
|
||||||
|
let mut session_preauth = None;
|
||||||
|
|
||||||
|
// 3.1.1 preauth is connection-scoped for NEGOTIATE, then per
|
||||||
|
// SESSION_SETUP authentication exchange.
|
||||||
|
if cmd == Command::Negotiate {
|
||||||
|
let mut p = conn
|
||||||
|
.preauth
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
|
p.update(frame);
|
||||||
|
} else if cmd == Command::SessionSetup
|
||||||
|
&& dialect == Some(crate::proto::messages::Dialect::Smb311)
|
||||||
|
{
|
||||||
|
let mut p = take_session_preauth(conn, req_hdr.session_id).await;
|
||||||
|
p.update(frame);
|
||||||
|
session_preauth = Some(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = handlers::dispatch_command(server, conn, &req_hdr, body_bytes).await;
|
||||||
|
|
||||||
|
// If the handler asked for a preauth snapshot (3.1.1), take it now.
|
||||||
|
if let Some(sid) = resp.take_preauth_snapshot_for_session {
|
||||||
|
let snap = session_preauth
|
||||||
|
.as_ref()
|
||||||
|
.expect("SMB 3.1.1 SessionSetup snapshot requires per-session preauth")
|
||||||
|
.snapshot();
|
||||||
|
// Stash on the session — the handler already created it.
|
||||||
|
let sessions = conn.sessions.read().await;
|
||||||
|
if let Some(sess_arc) = sessions.get(&sid) {
|
||||||
|
let mut sess = sess_arc.write().await;
|
||||||
|
sess.preauth_snapshot = Some(snap);
|
||||||
|
// For 3.1.1, recompute signing key now that we have the snapshot.
|
||||||
|
let dialect = *conn.dialect.read().await;
|
||||||
|
if dialect == Some(crate::proto::messages::Dialect::Smb311) {
|
||||||
|
sess.signing_key =
|
||||||
|
crate::proto::crypto::signing_key_311(&sess.session_base_key, &snap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = build_response_bytes(conn, &req_hdr, resp).await;
|
||||||
|
|
||||||
|
if cmd == Command::Negotiate {
|
||||||
|
let mut p = conn
|
||||||
|
.preauth
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
|
p.update(&bytes);
|
||||||
|
} else if cmd == Command::SessionSetup
|
||||||
|
&& dialect == Some(crate::proto::messages::Dialect::Smb311)
|
||||||
|
{
|
||||||
|
if read_u32(&bytes, 0x08) == ntstatus::STATUS_MORE_PROCESSING_REQUIRED {
|
||||||
|
if let Some(mut p) = session_preauth {
|
||||||
|
p.update(&bytes);
|
||||||
|
let sid = read_u64(&bytes, 0x28);
|
||||||
|
conn.session_preauth.write().await.insert(sid, p);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
conn.session_preauth
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.remove(&req_hdr.session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(bytes)
|
||||||
|
}
|
||||||
|
.instrument(span)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn take_session_preauth(conn: &Arc<Connection>, session_id: u64) -> PreauthIntegrity {
|
||||||
|
if session_id != 0
|
||||||
|
&& let Some(preauth) = conn.session_preauth.write().await.remove(&session_id)
|
||||||
|
{
|
||||||
|
return preauth;
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.preauth
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_request_signature(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
frame: &[u8],
|
||||||
|
) -> Result<(), u32> {
|
||||||
|
if hdr.command == Command::Negotiate {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if hdr.session_id == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let sessions = conn.sessions.read().await;
|
||||||
|
let sess_arc = match sessions.get(&hdr.session_id) {
|
||||||
|
Some(s) => s.clone(),
|
||||||
|
None => {
|
||||||
|
// Unknown session.
|
||||||
|
if hdr.flags & SMB2_FLAGS_SIGNED == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
return Err(ntstatus::STATUS_USER_SESSION_DELETED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
drop(sessions);
|
||||||
|
|
||||||
|
if hdr.flags & SMB2_FLAGS_SIGNED != 0 {
|
||||||
|
let sess = sess_arc.read().await;
|
||||||
|
if matches!(sess.identity, Identity::Anonymous) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let key = sess.signing_key;
|
||||||
|
drop(sess);
|
||||||
|
let algo = *conn.signing_algo.read().await;
|
||||||
|
if let Err(e) = crate::proto::crypto::verify(frame, &key, algo) {
|
||||||
|
warn!(error = %e, "request signature verification failed");
|
||||||
|
return Err(ntstatus::STATUS_ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
} else if hdr.command != Command::SessionSetup {
|
||||||
|
let sess = sess_arc.read().await;
|
||||||
|
let need = sess.signing_required && !matches!(sess.identity, Identity::Anonymous);
|
||||||
|
drop(sess);
|
||||||
|
if need {
|
||||||
|
warn!(?hdr.command, "missing required signature on request");
|
||||||
|
return Err(ntstatus::STATUS_ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the final on-the-wire bytes: header + body, with signing applied
|
||||||
|
/// when the session has a key.
|
||||||
|
async fn build_response_bytes(
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
req_hdr: &Smb2Header,
|
||||||
|
handler_resp: HandlerResponse,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut hdr = *req_hdr;
|
||||||
|
hdr.flags |= SMB2_FLAGS_SERVER_TO_REDIR;
|
||||||
|
hdr.flags &= !SMB2_FLAGS_ASYNC_COMMAND;
|
||||||
|
hdr.next_command = 0;
|
||||||
|
hdr.channel_sequence_status = handler_resp.status;
|
||||||
|
hdr.tail = HeaderTail::sync(
|
||||||
|
handler_resp
|
||||||
|
.override_tree_id
|
||||||
|
.unwrap_or_else(|| req_hdr.tree_id().unwrap_or(0)),
|
||||||
|
);
|
||||||
|
if let Some(sid) = handler_resp.override_session_id {
|
||||||
|
hdr.session_id = sid;
|
||||||
|
}
|
||||||
|
hdr.signature = [0u8; 16];
|
||||||
|
|
||||||
|
let request_was_signed = req_hdr.flags & SMB2_FLAGS_SIGNED != 0;
|
||||||
|
// MS-SMB2 §3.3.5.5.3 step 12: SessionSetup SUCCESS must be signed for
|
||||||
|
// non-anon/non-guest sessions even though the request cannot be signed yet.
|
||||||
|
let is_session_setup_success =
|
||||||
|
req_hdr.command == Command::SessionSetup && handler_resp.status == ntstatus::STATUS_SUCCESS;
|
||||||
|
let mut should_sign = false;
|
||||||
|
let mut key = [0u8; 16];
|
||||||
|
let algo = *conn.signing_algo.read().await;
|
||||||
|
if !handler_resp.skip_signing
|
||||||
|
&& hdr.session_id != 0
|
||||||
|
&& (request_was_signed || is_session_setup_success)
|
||||||
|
{
|
||||||
|
let sessions = conn.sessions.read().await;
|
||||||
|
if let Some(sess_arc) = sessions.get(&hdr.session_id) {
|
||||||
|
let sess = sess_arc.read().await;
|
||||||
|
let is_anon = matches!(sess.identity, Identity::Anonymous);
|
||||||
|
let is_guest_response = is_session_setup_success
|
||||||
|
&& handler_resp.body.len() >= 4
|
||||||
|
&& (handler_resp.body[2] & 0x01) != 0;
|
||||||
|
if !is_anon && !is_guest_response && sess.signing_key != [0u8; 16] {
|
||||||
|
key = sess.signing_key;
|
||||||
|
should_sign = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if should_sign {
|
||||||
|
hdr.flags |= SMB2_FLAGS_SIGNED;
|
||||||
|
} else {
|
||||||
|
hdr.flags &= !SMB2_FLAGS_SIGNED;
|
||||||
|
}
|
||||||
|
let mut out = Vec::with_capacity(SMB2_HEADER_LEN + handler_resp.body.len());
|
||||||
|
if let Err(e) = hdr.write(&mut out) {
|
||||||
|
error!(error = %e, "failed to encode response header");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&handler_resp.body);
|
||||||
|
|
||||||
|
if should_sign && let Err(e) = sign(&mut out, &key, algo) {
|
||||||
|
error!(error = %e, "failed to sign response");
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect and answer an SMB1 multi-protocol NEGOTIATE_REQUEST.
|
||||||
|
///
|
||||||
|
/// SMB1 frame layout for the request we accept:
|
||||||
|
/// * `[0..4]` — magic `0xFF 'S' 'M' 'B'`
|
||||||
|
/// * `[4]` — command (0x72 = SMB_COM_NEGOTIATE)
|
||||||
|
/// * `[5..32]` — rest of SMB1 header (status, flags, pid, tid, mid …)
|
||||||
|
/// * `[32]` — `WordCount` (0 for NEGOTIATE)
|
||||||
|
/// * `[33..35]`— `ByteCount` (u16 LE)
|
||||||
|
/// * `[35..]` — dialect strings, each `0x02 <ASCII> 0x00`.
|
||||||
|
///
|
||||||
|
/// Returns `Some(reply_bytes)` only for a SMB1 NEGOTIATE that lists at least
|
||||||
|
/// one SMB2 dialect we recognise; otherwise `None` so the caller can fall
|
||||||
|
/// through to the normal SMB2 path.
|
||||||
|
async fn handle_smb1_multi_protocol(
|
||||||
|
server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
frame: &[u8],
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
if frame.len() < 35 || frame[0..4] != [0xFF, b'S', b'M', b'B'] || frame[4] != 0x72 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let body_start = 33; // 32-byte header + 1-byte WordCount(=0)
|
||||||
|
let byte_count = u16::from_le_bytes([frame[body_start], frame[body_start + 1]]) as usize;
|
||||||
|
let blob_start = body_start + 2;
|
||||||
|
let blob_end = (blob_start + byte_count).min(frame.len());
|
||||||
|
let blob = &frame[blob_start..blob_end];
|
||||||
|
|
||||||
|
let mut wants_wildcard = false;
|
||||||
|
let mut wants_smb202 = false;
|
||||||
|
let mut i = 0;
|
||||||
|
while i < blob.len() {
|
||||||
|
if blob[i] != 0x02 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
let nul = match blob[i..].iter().position(|&b| b == 0) {
|
||||||
|
Some(p) => p,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
let s = std::str::from_utf8(&blob[i..i + nul]).unwrap_or("");
|
||||||
|
match s {
|
||||||
|
"SMB 2.???" => wants_wildcard = true,
|
||||||
|
"SMB 2.002" => wants_smb202 = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += nul + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chosen = if wants_wildcard {
|
||||||
|
crate::proto::messages::Dialect::Smb2Wildcard.as_u16()
|
||||||
|
} else if wants_smb202 {
|
||||||
|
crate::proto::messages::Dialect::Smb202.as_u16()
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
chosen = %format_args!("0x{chosen:04X}"),
|
||||||
|
"SMB1 multi-protocol negotiate"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Synthesize a request header so build_response_bytes can mint the
|
||||||
|
// SERVER_TO_REDIR response. Per MS-SMB2 §3.3.5.3.1 the response uses
|
||||||
|
// message_id=0, tree_id=0xFFFF, session_id=0.
|
||||||
|
let req_hdr = Smb2Header {
|
||||||
|
command: Command::Negotiate,
|
||||||
|
message_id: 0,
|
||||||
|
session_id: 0,
|
||||||
|
tail: HeaderTail::Sync {
|
||||||
|
reserved: 0,
|
||||||
|
tree_id: 0xFFFF,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let resp = handlers::negotiate::multi_protocol_response(server, conn, chosen).await;
|
||||||
|
Some(build_response_bytes(conn, &req_hdr, resp).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn test_conn() -> Arc<Connection> {
|
||||||
|
Arc::new(Connection::new(Uuid::nil(), 1024 * 1024, 1024 * 1024))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn negotiated_preauth() -> PreauthIntegrity {
|
||||||
|
let mut preauth = PreauthIntegrity::new();
|
||||||
|
preauth.update(b"negotiate request");
|
||||||
|
preauth.update(b"negotiate response");
|
||||||
|
preauth
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn new_session_setup_preauth_starts_from_negotiate_base() {
|
||||||
|
let conn = test_conn();
|
||||||
|
let base = negotiated_preauth();
|
||||||
|
*conn.preauth.lock().expect("preauth lock") = base.clone();
|
||||||
|
|
||||||
|
let mut first_session = take_session_preauth(&conn, 0).await;
|
||||||
|
first_session.update(b"session one request");
|
||||||
|
first_session.update(b"session one response");
|
||||||
|
conn.session_preauth.write().await.insert(1, first_session);
|
||||||
|
|
||||||
|
let mut second_session = take_session_preauth(&conn, 0).await;
|
||||||
|
second_session.update(b"session two request");
|
||||||
|
|
||||||
|
let mut expected = base.clone();
|
||||||
|
expected.update(b"session two request");
|
||||||
|
|
||||||
|
let mut polluted = base;
|
||||||
|
polluted.update(b"session one request");
|
||||||
|
polluted.update(b"session one response");
|
||||||
|
polluted.update(b"session two request");
|
||||||
|
|
||||||
|
assert_eq!(second_session.snapshot(), expected.snapshot());
|
||||||
|
assert_ne!(second_session.snapshot(), polluted.snapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn followup_session_setup_consumes_stored_session_preauth() {
|
||||||
|
let conn = test_conn();
|
||||||
|
let mut stored = negotiated_preauth();
|
||||||
|
stored.update(b"session setup request");
|
||||||
|
stored.update(b"session setup more-processing response");
|
||||||
|
let expected = stored.snapshot();
|
||||||
|
conn.session_preauth.write().await.insert(7, stored);
|
||||||
|
|
||||||
|
let got = take_session_preauth(&conn, 7).await;
|
||||||
|
|
||||||
|
assert_eq!(got.snapshot(), expected);
|
||||||
|
assert!(!conn.session_preauth.read().await.contains_key(&7));
|
||||||
|
}
|
||||||
|
}
|
||||||
86
vendor/smb-server/src/error.rs
vendored
Normal file
86
vendor/smb-server/src/error.rs
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
//! Public error type for the server, plus the NTSTATUS mapping per spec §8.
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::ntstatus;
|
||||||
|
|
||||||
|
pub type SmbResult<T> = Result<T, SmbError>;
|
||||||
|
|
||||||
|
/// Errors returned by `ShareBackend` and surfaced through the SMB protocol.
|
||||||
|
///
|
||||||
|
/// `to_nt_status` maps each variant onto a single NTSTATUS code per the spec
|
||||||
|
/// §8 table. Internal protocol-layer failures (malformed frames, signing
|
||||||
|
/// errors) never become `SmbError`; the connection loop logs them and aborts.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SmbError {
|
||||||
|
#[error("not found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("path not found")]
|
||||||
|
PathNotFound,
|
||||||
|
#[error("access denied")]
|
||||||
|
AccessDenied,
|
||||||
|
#[error("exists")]
|
||||||
|
Exists,
|
||||||
|
#[error("not empty")]
|
||||||
|
NotEmpty,
|
||||||
|
#[error("is a directory")]
|
||||||
|
IsDirectory,
|
||||||
|
#[error("not a directory")]
|
||||||
|
NotADirectory,
|
||||||
|
#[error("name too long / invalid")]
|
||||||
|
NameInvalid,
|
||||||
|
#[error("sharing violation")]
|
||||||
|
Sharing,
|
||||||
|
#[error("not supported")]
|
||||||
|
NotSupported,
|
||||||
|
#[error("io: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmbError {
|
||||||
|
/// Map this error onto an NTSTATUS code per the v1 spec §8 table.
|
||||||
|
pub fn to_nt_status(&self) -> u32 {
|
||||||
|
match self {
|
||||||
|
SmbError::NotFound => ntstatus::STATUS_OBJECT_NAME_NOT_FOUND,
|
||||||
|
SmbError::PathNotFound => ntstatus::STATUS_OBJECT_PATH_NOT_FOUND,
|
||||||
|
SmbError::AccessDenied => ntstatus::STATUS_ACCESS_DENIED,
|
||||||
|
SmbError::Exists => ntstatus::STATUS_OBJECT_NAME_COLLISION,
|
||||||
|
SmbError::NotEmpty => ntstatus::STATUS_DIRECTORY_NOT_EMPTY,
|
||||||
|
SmbError::IsDirectory => ntstatus::STATUS_FILE_IS_A_DIRECTORY,
|
||||||
|
SmbError::NotADirectory => ntstatus::STATUS_NOT_A_DIRECTORY,
|
||||||
|
SmbError::NameInvalid => ntstatus::STATUS_OBJECT_NAME_INVALID,
|
||||||
|
SmbError::Sharing => ntstatus::STATUS_SHARING_VIOLATION,
|
||||||
|
SmbError::NotSupported => ntstatus::STATUS_NOT_SUPPORTED,
|
||||||
|
SmbError::Io(_) => ntstatus::STATUS_UNEXPECTED_IO_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nt_status_table_matches_spec() {
|
||||||
|
assert_eq!(SmbError::NotFound.to_nt_status(), 0xC000_000F);
|
||||||
|
assert_eq!(SmbError::PathNotFound.to_nt_status(), 0xC000_003A);
|
||||||
|
assert_eq!(SmbError::AccessDenied.to_nt_status(), 0xC000_0022);
|
||||||
|
assert_eq!(SmbError::Exists.to_nt_status(), 0xC000_0035);
|
||||||
|
assert_eq!(SmbError::NotEmpty.to_nt_status(), 0xC000_0101);
|
||||||
|
assert_eq!(SmbError::IsDirectory.to_nt_status(), 0xC000_00BA);
|
||||||
|
assert_eq!(SmbError::NotADirectory.to_nt_status(), 0xC000_0103);
|
||||||
|
assert_eq!(SmbError::NameInvalid.to_nt_status(), 0xC000_0033);
|
||||||
|
assert_eq!(SmbError::Sharing.to_nt_status(), 0xC000_0043);
|
||||||
|
assert_eq!(SmbError::NotSupported.to_nt_status(), 0xC000_00BB);
|
||||||
|
|
||||||
|
let io_err = SmbError::Io(std::io::Error::other("boom"));
|
||||||
|
assert_eq!(io_err.to_nt_status(), 0xC000_009C);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn io_err_from_blanket_works() {
|
||||||
|
let io: std::io::Error = std::io::Error::other("x");
|
||||||
|
let smb: SmbError = io.into();
|
||||||
|
assert_eq!(smb.to_nt_status(), 0xC000_009C);
|
||||||
|
}
|
||||||
|
}
|
||||||
921
vendor/smb-server/src/fs/local.rs
vendored
Normal file
921
vendor/smb-server/src/fs/local.rs
vendored
Normal file
@@ -0,0 +1,921 @@
|
|||||||
|
//! `LocalFsBackend` — a `ShareBackend` backed by a real on-disk directory.
|
||||||
|
//!
|
||||||
|
//! The share root is opened once via `cap_std::fs::Dir::open_ambient_dir` and
|
||||||
|
//! kept as the sole authority handle. All subsequent path operations are
|
||||||
|
//! resolved relative to that handle, so a malicious symlink or `..` smuggled
|
||||||
|
//! through `SmbPath` cannot escape the sandbox — `cap-std` enforces this at
|
||||||
|
//! every step.
|
||||||
|
//!
|
||||||
|
//! Per the v1 design (spec §3.4) this backend is intentionally minimal:
|
||||||
|
//!
|
||||||
|
//! - Sync FS calls are wrapped in `tokio::task::spawn_blocking` so the async
|
||||||
|
//! `ShareBackend`/`Handle` methods integrate cleanly with the dispatcher.
|
||||||
|
//! - `read_only()` flips a flag that makes write-class opens reject early
|
||||||
|
//! with `SmbError::AccessDenied`.
|
||||||
|
//! - DOS-style glob matching for `list_dir` is handled here (case-insensitive,
|
||||||
|
//! `?` and `*`), since cap-std only provides raw `entries()`.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::os::unix::fs::FileExt as _;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use cap_std::ambient_authority;
|
||||||
|
use cap_std::fs::{Dir, OpenOptions as CapOpenOptions};
|
||||||
|
use tokio::task::spawn_blocking;
|
||||||
|
|
||||||
|
use crate::backend::{
|
||||||
|
BackendCapabilities, DirEntry as SmbDirEntry, FileInfo, FileTimes, Handle, OpenIntent,
|
||||||
|
OpenOptions, ShareBackend,
|
||||||
|
};
|
||||||
|
use crate::error::{SmbError, SmbResult};
|
||||||
|
use crate::path::SmbPath;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Backend
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Local-filesystem backend, sandboxed at a single root directory.
|
||||||
|
///
|
||||||
|
/// Cheap to clone: internally an `Arc<cap_std::fs::Dir>` plus a flag.
|
||||||
|
pub struct LocalFsBackend {
|
||||||
|
root: Arc<Dir>,
|
||||||
|
read_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalFsBackend {
|
||||||
|
/// Open `path` as the share root. Errors if the path does not exist or is
|
||||||
|
/// not a directory.
|
||||||
|
pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||||
|
let dir = Dir::open_ambient_dir(path, ambient_authority())?;
|
||||||
|
Ok(Self {
|
||||||
|
root: Arc::new(dir),
|
||||||
|
read_only: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the backend as read-only. All write-class opens and writes return
|
||||||
|
/// an access-denied SMB error.
|
||||||
|
#[must_use]
|
||||||
|
pub fn read_only(mut self) -> Self {
|
||||||
|
self.read_only = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Path translation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Convert a validated `SmbPath` into a relative `PathBuf` suitable for
|
||||||
|
/// `cap_std::fs::Dir` lookups.
|
||||||
|
///
|
||||||
|
/// `SmbPath` is already validated (no `..`, no forbidden chars, no doubled
|
||||||
|
/// separators), so this is purely a join. The empty `SmbPath` (root) yields
|
||||||
|
/// `PathBuf::from(".")` — cap-std accepts this for `metadata` etc.
|
||||||
|
fn to_rel_path(path: &SmbPath) -> PathBuf {
|
||||||
|
if path.is_root() {
|
||||||
|
return PathBuf::from(".");
|
||||||
|
}
|
||||||
|
let mut out = PathBuf::new();
|
||||||
|
for c in path.components() {
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Error mapping
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn io_to_smb(err: io::Error) -> SmbError {
|
||||||
|
use io::ErrorKind::*;
|
||||||
|
match err.kind() {
|
||||||
|
NotFound => SmbError::NotFound,
|
||||||
|
PermissionDenied => SmbError::AccessDenied,
|
||||||
|
AlreadyExists => SmbError::Exists,
|
||||||
|
DirectoryNotEmpty => SmbError::NotEmpty,
|
||||||
|
IsADirectory => SmbError::IsDirectory,
|
||||||
|
NotADirectory => SmbError::NotADirectory,
|
||||||
|
InvalidInput | InvalidFilename => SmbError::NameInvalid,
|
||||||
|
_ => SmbError::Io(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a panic from `spawn_blocking` into an `io::Error`. Panics in the
|
||||||
|
/// blocking pool are exotic; we surface them as a generic `Other` rather than
|
||||||
|
/// re-panicking on the async side.
|
||||||
|
fn join_to_io(_e: tokio::task::JoinError) -> io::Error {
|
||||||
|
io::Error::other("blocking task panicked or was cancelled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FILETIME conversion
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Number of 100-nanosecond intervals between 1601-01-01 (Windows FILETIME
|
||||||
|
/// epoch) and 1970-01-01 (UNIX epoch).
|
||||||
|
const FILETIME_OFFSET: u64 = 116_444_736_000_000_000;
|
||||||
|
|
||||||
|
fn system_time_to_filetime(t: SystemTime) -> u64 {
|
||||||
|
match t.duration_since(UNIX_EPOCH) {
|
||||||
|
Ok(d) => FILETIME_OFFSET + (d.as_secs() * 10_000_000) + u64::from(d.subsec_nanos() / 100),
|
||||||
|
Err(_) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filetime_to_system_time(ft: u64) -> Option<SystemTime> {
|
||||||
|
if ft < FILETIME_OFFSET {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let unix_100ns = ft - FILETIME_OFFSET;
|
||||||
|
let secs = unix_100ns / 10_000_000;
|
||||||
|
let nanos = ((unix_100ns % 10_000_000) * 100) as u32;
|
||||||
|
UNIX_EPOCH.checked_add(Duration::new(secs, nanos))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileInfo construction
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn file_info_from_metadata(name: String, md: &cap_std::fs::Metadata) -> FileInfo {
|
||||||
|
let len = md.len();
|
||||||
|
let modified = md.modified().ok().map(|t| t.into_std());
|
||||||
|
let accessed = md.accessed().ok().map(|t| t.into_std());
|
||||||
|
let created = md.created().ok().map(|t| t.into_std());
|
||||||
|
|
||||||
|
// Fall back: if a particular timestamp isn't available on the platform,
|
||||||
|
// use whichever timestamp is available, then `now()` as last resort. SMB
|
||||||
|
// clients tolerate equal timestamps fine.
|
||||||
|
let modified = modified
|
||||||
|
.or(created)
|
||||||
|
.or(accessed)
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
let accessed = accessed.unwrap_or(modified);
|
||||||
|
let created = created.unwrap_or(modified);
|
||||||
|
|
||||||
|
FileInfo {
|
||||||
|
name,
|
||||||
|
end_of_file: len,
|
||||||
|
allocation_size: len,
|
||||||
|
creation_time: system_time_to_filetime(created),
|
||||||
|
last_access_time: system_time_to_filetime(accessed),
|
||||||
|
last_write_time: system_time_to_filetime(modified),
|
||||||
|
change_time: system_time_to_filetime(modified),
|
||||||
|
is_directory: md.is_dir(),
|
||||||
|
// `cap-std` does not expose a stable inode-style identifier in its
|
||||||
|
// public API; the dispatcher substitutes the FileId where needed.
|
||||||
|
file_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// DOS glob matching
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Match `name` against a DOS-style pattern. `?` matches any single char,
|
||||||
|
/// `*` matches any sequence (possibly empty). Comparison is case-insensitive
|
||||||
|
/// (ASCII fold) — sufficient for the v1 use-case where names are validated to
|
||||||
|
/// be free of weird Unicode tricks.
|
||||||
|
fn glob_match(pattern: &str, name: &str) -> bool {
|
||||||
|
// Walk both strings as char vectors so `?` matches a char rather than a
|
||||||
|
// byte, without going through grapheme territory.
|
||||||
|
let p: Vec<char> = pattern.chars().collect();
|
||||||
|
let n: Vec<char> = name.chars().collect();
|
||||||
|
glob_match_inner(&p, &n)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glob_match_inner(p: &[char], n: &[char]) -> bool {
|
||||||
|
let mut pi = 0usize;
|
||||||
|
let mut ni = 0usize;
|
||||||
|
let mut star: Option<(usize, usize)> = None; // (pi after '*', ni at the time)
|
||||||
|
|
||||||
|
while ni < n.len() {
|
||||||
|
if pi < p.len() && (p[pi] == '?' || ascii_eq_ci(p[pi], n[ni])) {
|
||||||
|
pi += 1;
|
||||||
|
ni += 1;
|
||||||
|
} else if pi < p.len() && p[pi] == '*' {
|
||||||
|
star = Some((pi + 1, ni));
|
||||||
|
pi += 1;
|
||||||
|
} else if let Some((sp, sn)) = star {
|
||||||
|
pi = sp;
|
||||||
|
ni = sn + 1;
|
||||||
|
star = Some((sp, sn + 1));
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while pi < p.len() && p[pi] == '*' {
|
||||||
|
pi += 1;
|
||||||
|
}
|
||||||
|
pi == p.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ascii_eq_ci(a: char, b: char) -> bool {
|
||||||
|
a.eq_ignore_ascii_case(&b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ShareBackend impl
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ShareBackend for LocalFsBackend {
|
||||||
|
async fn open(&self, path: &SmbPath, opts: OpenOptions) -> SmbResult<Box<dyn Handle>> {
|
||||||
|
// 1. Read-only check: any open that requests creation, write access,
|
||||||
|
// truncation, or overwrite is rejected up front. Pure read opens
|
||||||
|
// pass through.
|
||||||
|
let writes = opts.write
|
||||||
|
|| matches!(
|
||||||
|
opts.intent,
|
||||||
|
OpenIntent::Create
|
||||||
|
| OpenIntent::OpenOrCreate
|
||||||
|
| OpenIntent::OverwriteOrCreate
|
||||||
|
| OpenIntent::Truncate
|
||||||
|
);
|
||||||
|
if self.read_only && writes {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel = to_rel_path(path);
|
||||||
|
let root = Arc::clone(&self.root);
|
||||||
|
let read_only = self.read_only;
|
||||||
|
let directory = opts.directory;
|
||||||
|
let non_directory = opts.non_directory;
|
||||||
|
|
||||||
|
// For directories, cap-std exposes `open_dir` separately; we don't
|
||||||
|
// need an OpenOptions translation in that case.
|
||||||
|
if directory {
|
||||||
|
// Directory CREATE intents: Create / OpenOrCreate / OverwriteOrCreate
|
||||||
|
// imply mkdir; Open / Truncate require existing.
|
||||||
|
let intent = opts.intent;
|
||||||
|
let dir_handle = spawn_blocking(move || -> io::Result<Dir> {
|
||||||
|
match intent {
|
||||||
|
OpenIntent::Open => root.open_dir(&rel),
|
||||||
|
OpenIntent::Create => {
|
||||||
|
root.create_dir(&rel)?;
|
||||||
|
root.open_dir(&rel)
|
||||||
|
}
|
||||||
|
OpenIntent::OpenOrCreate => {
|
||||||
|
if !root.exists(&rel) {
|
||||||
|
root.create_dir(&rel)?;
|
||||||
|
}
|
||||||
|
root.open_dir(&rel)
|
||||||
|
}
|
||||||
|
OpenIntent::Truncate | OpenIntent::OverwriteOrCreate => {
|
||||||
|
// Truncating a directory has no meaning; reject.
|
||||||
|
Err(io::Error::from(io::ErrorKind::InvalidInput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)?;
|
||||||
|
|
||||||
|
return Ok(Box::new(LocalHandle::Dir {
|
||||||
|
name: file_name_for(path),
|
||||||
|
dir_handle: Arc::new(dir_handle),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing_is_dir = {
|
||||||
|
let root = Arc::clone(&self.root);
|
||||||
|
let rel = rel.clone();
|
||||||
|
spawn_blocking(move || -> io::Result<bool> {
|
||||||
|
match root.metadata(&rel) {
|
||||||
|
Ok(md) => Ok(md.is_dir()),
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
};
|
||||||
|
if existing_is_dir {
|
||||||
|
if non_directory {
|
||||||
|
return Err(SmbError::IsDirectory);
|
||||||
|
}
|
||||||
|
match opts.intent {
|
||||||
|
OpenIntent::Open | OpenIntent::OpenOrCreate => {
|
||||||
|
let root = Arc::clone(&self.root);
|
||||||
|
let rel = rel.clone();
|
||||||
|
let dir_handle = spawn_blocking(move || root.open_dir(&rel))
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)?;
|
||||||
|
return Ok(Box::new(LocalHandle::Dir {
|
||||||
|
name: file_name_for(path),
|
||||||
|
dir_handle: Arc::new(dir_handle),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
OpenIntent::Create => return Err(SmbError::Exists),
|
||||||
|
OpenIntent::Truncate | OpenIntent::OverwriteOrCreate => {
|
||||||
|
return Err(SmbError::IsDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Translate OpenIntent → cap-std OpenOptions.
|
||||||
|
let mut cap_opts = CapOpenOptions::new();
|
||||||
|
match opts.intent {
|
||||||
|
OpenIntent::Open => {
|
||||||
|
cap_opts.read(true).write(opts.write);
|
||||||
|
}
|
||||||
|
OpenIntent::Create => {
|
||||||
|
cap_opts.read(opts.read).write(true).create_new(true);
|
||||||
|
}
|
||||||
|
OpenIntent::Truncate => {
|
||||||
|
cap_opts.read(opts.read).write(true).truncate(true);
|
||||||
|
}
|
||||||
|
OpenIntent::OpenOrCreate => {
|
||||||
|
cap_opts.read(opts.read).write(true).create(true);
|
||||||
|
}
|
||||||
|
OpenIntent::OverwriteOrCreate => {
|
||||||
|
cap_opts
|
||||||
|
.read(opts.read)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cap_file = spawn_blocking(move || root.open_with(&rel, &cap_opts))
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)?;
|
||||||
|
|
||||||
|
// Convert to a `std::fs::File`. We only need cap-std for the safe
|
||||||
|
// *open*; once we hold a verified file handle, std's API gives us
|
||||||
|
// `set_times`, `set_len`, `sync_data`, and `FileExt::{read,write}_at`
|
||||||
|
// without pulling in extra crates.
|
||||||
|
let std_file: std::fs::File = cap_file.into_std();
|
||||||
|
|
||||||
|
Ok(Box::new(LocalHandle::File {
|
||||||
|
name: file_name_for(path),
|
||||||
|
file: Arc::new(std_file),
|
||||||
|
read_only,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unlink(&self, path: &SmbPath) -> SmbResult<()> {
|
||||||
|
if self.read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
if path.is_root() {
|
||||||
|
// Refusing to delete the share root itself.
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
let rel = to_rel_path(path);
|
||||||
|
let root = Arc::clone(&self.root);
|
||||||
|
|
||||||
|
spawn_blocking(move || -> io::Result<()> {
|
||||||
|
match root.remove_file(&rel) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::IsADirectory => {
|
||||||
|
// Caller's intent was "delete this name"; if it turned
|
||||||
|
// out to be a directory, fall back to remove_dir which
|
||||||
|
// refuses non-empty dirs (mapped to NotEmpty above).
|
||||||
|
root.remove_dir(&rel)
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rename(&self, from: &SmbPath, to: &SmbPath) -> SmbResult<()> {
|
||||||
|
if self.read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
if from.is_root() || to.is_root() {
|
||||||
|
return Err(SmbError::NameInvalid);
|
||||||
|
}
|
||||||
|
let from = to_rel_path(from);
|
||||||
|
let to_path = to_rel_path(to);
|
||||||
|
let root = Arc::clone(&self.root);
|
||||||
|
let root2 = Arc::clone(&self.root);
|
||||||
|
|
||||||
|
spawn_blocking(move || -> io::Result<()> {
|
||||||
|
// Reject overwrite — SMB rename semantics require explicit
|
||||||
|
// replace-if-exists which we do not implement in v1.
|
||||||
|
if root2.exists(&to_path) {
|
||||||
|
return Err(io::Error::from(io::ErrorKind::AlreadyExists));
|
||||||
|
}
|
||||||
|
root.rename(&from, &root2, &to_path)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capabilities(&self) -> BackendCapabilities {
|
||||||
|
BackendCapabilities {
|
||||||
|
is_read_only: self.read_only,
|
||||||
|
// POSIX filesystems are typically case-sensitive. We don't try to
|
||||||
|
// emulate case-insensitive lookup in v1 (see spec §3.4).
|
||||||
|
case_sensitive: cfg!(any(target_os = "linux", target_os = "freebsd")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Handle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Internal handle variant. `File` carries a `std::fs::File` (after cap-std
|
||||||
|
/// has done the safe open); `Dir` keeps the `cap_std::fs::Dir` so we can
|
||||||
|
/// re-list entries.
|
||||||
|
enum LocalHandle {
|
||||||
|
File {
|
||||||
|
name: String,
|
||||||
|
file: Arc<std::fs::File>,
|
||||||
|
read_only: bool,
|
||||||
|
},
|
||||||
|
Dir {
|
||||||
|
name: String,
|
||||||
|
dir_handle: Arc<Dir>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_name_for(path: &SmbPath) -> String {
|
||||||
|
path.file_name().unwrap_or("").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handle for LocalHandle {
|
||||||
|
async fn read(&self, offset: u64, len: u32) -> SmbResult<Bytes> {
|
||||||
|
match self {
|
||||||
|
LocalHandle::File { file, .. } => {
|
||||||
|
let file = Arc::clone(file);
|
||||||
|
let n = len as usize;
|
||||||
|
let bytes = spawn_blocking(move || -> io::Result<Bytes> {
|
||||||
|
let mut buf = vec![0u8; n];
|
||||||
|
let read = file.read_at(&mut buf, offset)?;
|
||||||
|
buf.truncate(read);
|
||||||
|
Ok(Bytes::from(buf))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)?;
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
LocalHandle::Dir { .. } => Err(SmbError::IsDirectory),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write(&self, offset: u64, data: &[u8]) -> SmbResult<u32> {
|
||||||
|
self.write_owned(offset, data.to_vec()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_owned(&self, offset: u64, data: Vec<u8>) -> SmbResult<u32> {
|
||||||
|
match self {
|
||||||
|
LocalHandle::File {
|
||||||
|
file, read_only, ..
|
||||||
|
} => {
|
||||||
|
if *read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
let file = Arc::clone(file);
|
||||||
|
let written = spawn_blocking(move || file.write_at(&data, offset))
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)?;
|
||||||
|
Ok(u32::try_from(written).unwrap_or(u32::MAX))
|
||||||
|
}
|
||||||
|
LocalHandle::Dir { .. } => Err(SmbError::IsDirectory),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn flush(&self) -> SmbResult<()> {
|
||||||
|
match self {
|
||||||
|
LocalHandle::File { file, .. } => {
|
||||||
|
let file = Arc::clone(file);
|
||||||
|
spawn_blocking(move || file.sync_data())
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)
|
||||||
|
}
|
||||||
|
// Flushing a directory is a no-op in SMB semantics.
|
||||||
|
LocalHandle::Dir { .. } => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stat(&self) -> SmbResult<FileInfo> {
|
||||||
|
match self {
|
||||||
|
LocalHandle::File { file, name, .. } => {
|
||||||
|
let file = Arc::clone(file);
|
||||||
|
let name = name.clone();
|
||||||
|
spawn_blocking(move || -> io::Result<FileInfo> {
|
||||||
|
let std_md = file.metadata()?;
|
||||||
|
// Synthesize a cap-std Metadata from the std one so we
|
||||||
|
// can reuse `file_info_from_metadata`. cap-primitives
|
||||||
|
// exposes `Metadata::from_just_metadata` for this.
|
||||||
|
let md = cap_std::fs::Metadata::from_just_metadata(std_md);
|
||||||
|
Ok(file_info_from_metadata(name, &md))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)
|
||||||
|
}
|
||||||
|
LocalHandle::Dir {
|
||||||
|
dir_handle, name, ..
|
||||||
|
} => {
|
||||||
|
let dir_handle = Arc::clone(dir_handle);
|
||||||
|
let name = name.clone();
|
||||||
|
spawn_blocking(move || -> io::Result<FileInfo> {
|
||||||
|
let md = dir_handle.dir_metadata()?;
|
||||||
|
Ok(file_info_from_metadata(name, &md))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_times(&self, times: FileTimes) -> SmbResult<()> {
|
||||||
|
match self {
|
||||||
|
LocalHandle::File {
|
||||||
|
file, read_only, ..
|
||||||
|
} => {
|
||||||
|
if *read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
let file = Arc::clone(file);
|
||||||
|
spawn_blocking(move || -> io::Result<()> {
|
||||||
|
let mut std_times = std::fs::FileTimes::new();
|
||||||
|
if let Some(ft) = times.last_write_time
|
||||||
|
&& let Some(t) = filetime_to_system_time(ft)
|
||||||
|
{
|
||||||
|
std_times = std_times.set_modified(t);
|
||||||
|
}
|
||||||
|
if let Some(ft) = times.last_access_time
|
||||||
|
&& let Some(t) = filetime_to_system_time(ft)
|
||||||
|
{
|
||||||
|
std_times = std_times.set_accessed(t);
|
||||||
|
}
|
||||||
|
// creation_time / change_time: stable std::fs::FileTimes
|
||||||
|
// does not expose setters for these; silently ignored.
|
||||||
|
file.set_times(std_times)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)
|
||||||
|
}
|
||||||
|
// cap-std's directory handle does not expose set_times in its
|
||||||
|
// stable API; mark as unsupported on directories.
|
||||||
|
LocalHandle::Dir { .. } => Err(SmbError::NotSupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn truncate(&self, len: u64) -> SmbResult<()> {
|
||||||
|
match self {
|
||||||
|
LocalHandle::File {
|
||||||
|
file, read_only, ..
|
||||||
|
} => {
|
||||||
|
if *read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
let file = Arc::clone(file);
|
||||||
|
spawn_blocking(move || file.set_len(len))
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)
|
||||||
|
}
|
||||||
|
// Protocol layer rejects truncate on dir handles before this; if
|
||||||
|
// it ever reaches us, surface as NotSupported.
|
||||||
|
LocalHandle::Dir { .. } => Err(SmbError::NotSupported),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_dir(&self, pattern: Option<&str>) -> SmbResult<Vec<SmbDirEntry>> {
|
||||||
|
match self {
|
||||||
|
LocalHandle::File { .. } => Err(SmbError::NotADirectory),
|
||||||
|
LocalHandle::Dir { dir_handle, .. } => {
|
||||||
|
let dir_handle = Arc::clone(dir_handle);
|
||||||
|
let pat = pattern.map(|s| s.to_owned());
|
||||||
|
spawn_blocking(move || -> io::Result<Vec<SmbDirEntry>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for entry in dir_handle.entries()? {
|
||||||
|
let entry = entry?;
|
||||||
|
let os_name = entry.file_name();
|
||||||
|
let Some(name) = os_name.to_str().map(str::to_owned) else {
|
||||||
|
// Skip non-UTF-8 names; SMB wire format is UTF-16
|
||||||
|
// and we never want to emit invalid Unicode here.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(p) = pat.as_deref() {
|
||||||
|
// Empty / "*" / "*.*" all mean "match everything"
|
||||||
|
// in DOS-speak.
|
||||||
|
if !(p.is_empty() || p == "*" || p == "*.*" || glob_match(p, &name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let md = entry.metadata()?;
|
||||||
|
let info = file_info_from_metadata(name, &md);
|
||||||
|
out.push(SmbDirEntry { info });
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)
|
||||||
|
.map_err(io_to_smb)?
|
||||||
|
.map_err(io_to_smb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(self: Box<Self>) -> SmbResult<()> {
|
||||||
|
// Drop is sufficient — closing the underlying handle is what the OS
|
||||||
|
// does when the last `Arc` ref goes away. No flush here: SMB CLOSE
|
||||||
|
// does not imply fsync.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::backend::{OpenIntent, OpenOptions};
|
||||||
|
use crate::path::SmbPath;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
fn p(s: &str) -> SmbPath {
|
||||||
|
s.parse::<SmbPath>().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opts_create() -> OpenOptions {
|
||||||
|
OpenOptions {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
intent: OpenIntent::Create,
|
||||||
|
directory: false,
|
||||||
|
non_directory: false,
|
||||||
|
delete_on_close: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opts_open_rw() -> OpenOptions {
|
||||||
|
OpenOptions {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
intent: OpenIntent::Open,
|
||||||
|
directory: false,
|
||||||
|
non_directory: false,
|
||||||
|
delete_on_close: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opts_open_ro() -> OpenOptions {
|
||||||
|
OpenOptions {
|
||||||
|
read: true,
|
||||||
|
write: false,
|
||||||
|
intent: OpenIntent::Open,
|
||||||
|
directory: false,
|
||||||
|
non_directory: false,
|
||||||
|
delete_on_close: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opts_open_dir() -> OpenOptions {
|
||||||
|
OpenOptions {
|
||||||
|
read: true,
|
||||||
|
write: false,
|
||||||
|
intent: OpenIntent::Open,
|
||||||
|
directory: true,
|
||||||
|
non_directory: false,
|
||||||
|
delete_on_close: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_write_read_stat_close() {
|
||||||
|
let td = tempdir().unwrap();
|
||||||
|
let backend = LocalFsBackend::new(td.path()).unwrap();
|
||||||
|
|
||||||
|
// Create
|
||||||
|
let h = backend.open(&p("hello.txt"), opts_create()).await.unwrap();
|
||||||
|
let n = h.write(0, b"hello world").await.unwrap();
|
||||||
|
assert_eq!(n, 11);
|
||||||
|
h.flush().await.unwrap();
|
||||||
|
|
||||||
|
// Stat
|
||||||
|
let info = h.stat().await.unwrap();
|
||||||
|
assert_eq!(info.name, "hello.txt");
|
||||||
|
assert_eq!(info.end_of_file, 11);
|
||||||
|
assert!(!info.is_directory);
|
||||||
|
assert!(info.last_write_time > 0);
|
||||||
|
h.close().await.unwrap();
|
||||||
|
|
||||||
|
// Reopen for read
|
||||||
|
let h2 = backend.open(&p("hello.txt"), opts_open_ro()).await.unwrap();
|
||||||
|
let bytes = h2.read(0, 1024).await.unwrap();
|
||||||
|
assert_eq!(&bytes[..], b"hello world");
|
||||||
|
|
||||||
|
// Short-read past EOF returns truncated
|
||||||
|
let bytes = h2.read(6, 1024).await.unwrap();
|
||||||
|
assert_eq!(&bytes[..], b"world");
|
||||||
|
|
||||||
|
// Read past EOF returns empty
|
||||||
|
let bytes = h2.read(100, 1024).await.unwrap();
|
||||||
|
assert!(bytes.is_empty());
|
||||||
|
h2.close().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_dir_finds_created_file() {
|
||||||
|
let td = tempdir().unwrap();
|
||||||
|
let backend = LocalFsBackend::new(td.path()).unwrap();
|
||||||
|
let h = backend.open(&p("a.txt"), opts_create()).await.unwrap();
|
||||||
|
h.close().await.unwrap();
|
||||||
|
|
||||||
|
let dir_h = backend
|
||||||
|
.open(&SmbPath::root(), opts_open_dir())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let entries = dir_h.list_dir(None).await.unwrap();
|
||||||
|
assert!(entries.iter().any(|e| e.info.name == "a.txt"));
|
||||||
|
dir_h.close().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn read_only_rejects_writes() {
|
||||||
|
let td = tempdir().unwrap();
|
||||||
|
// Pre-create a file via a writable backend so we have something to
|
||||||
|
// attempt to open RW.
|
||||||
|
{
|
||||||
|
let writable = LocalFsBackend::new(td.path()).unwrap();
|
||||||
|
let h = writable.open(&p("x.txt"), opts_create()).await.unwrap();
|
||||||
|
h.close().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let backend = LocalFsBackend::new(td.path()).unwrap().read_only();
|
||||||
|
assert!(backend.capabilities().is_read_only);
|
||||||
|
|
||||||
|
// RW open should be rejected.
|
||||||
|
let err = backend
|
||||||
|
.open(&p("x.txt"), opts_open_rw())
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(err, SmbError::AccessDenied));
|
||||||
|
|
||||||
|
// Create should be rejected.
|
||||||
|
let err = backend
|
||||||
|
.open(&p("y.txt"), opts_create())
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(err, SmbError::AccessDenied));
|
||||||
|
|
||||||
|
// Pure read open is fine.
|
||||||
|
let h = backend.open(&p("x.txt"), opts_open_ro()).await.unwrap();
|
||||||
|
// Writing through a handle obtained from a read-only backend would
|
||||||
|
// already be impossible — but if a backend ever yields one, the
|
||||||
|
// check still bites.
|
||||||
|
h.close().await.unwrap();
|
||||||
|
|
||||||
|
// unlink rejected.
|
||||||
|
let err = backend.unlink(&p("x.txt")).await.err().unwrap();
|
||||||
|
assert!(matches!(err, SmbError::AccessDenied));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unlink_file_then_nonempty_dir_errors() {
|
||||||
|
let td = tempdir().unwrap();
|
||||||
|
let backend = LocalFsBackend::new(td.path()).unwrap();
|
||||||
|
|
||||||
|
// Create & remove a file.
|
||||||
|
let h = backend.open(&p("doomed.txt"), opts_create()).await.unwrap();
|
||||||
|
h.close().await.unwrap();
|
||||||
|
backend.unlink(&p("doomed.txt")).await.unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
backend.unlink(&p("doomed.txt")).await.err().unwrap(),
|
||||||
|
SmbError::NotFound
|
||||||
|
));
|
||||||
|
|
||||||
|
// Create a non-empty directory; unlink should fail with NotEmpty.
|
||||||
|
std::fs::create_dir(td.path().join("dir1")).unwrap();
|
||||||
|
std::fs::write(td.path().join("dir1").join("inside"), b"x").unwrap();
|
||||||
|
|
||||||
|
let err = backend.unlink(&p("dir1")).await.err().unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(err, SmbError::NotEmpty),
|
||||||
|
"expected NotEmpty, got {err:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Empty it and retry.
|
||||||
|
std::fs::remove_file(td.path().join("dir1").join("inside")).unwrap();
|
||||||
|
backend.unlink(&p("dir1")).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rename_within_root() {
|
||||||
|
let td = tempdir().unwrap();
|
||||||
|
let backend = LocalFsBackend::new(td.path()).unwrap();
|
||||||
|
|
||||||
|
let h = backend.open(&p("old.txt"), opts_create()).await.unwrap();
|
||||||
|
h.write(0, b"data").await.unwrap();
|
||||||
|
h.close().await.unwrap();
|
||||||
|
|
||||||
|
backend.rename(&p("old.txt"), &p("new.txt")).await.unwrap();
|
||||||
|
assert!(td.path().join("new.txt").exists());
|
||||||
|
assert!(!td.path().join("old.txt").exists());
|
||||||
|
|
||||||
|
// Renaming over an existing target should fail.
|
||||||
|
let h = backend.open(&p("other.txt"), opts_create()).await.unwrap();
|
||||||
|
h.close().await.unwrap();
|
||||||
|
let err = backend
|
||||||
|
.rename(&p("other.txt"), &p("new.txt"))
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
assert!(matches!(err, SmbError::Exists), "got {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_dir_pattern_matching() {
|
||||||
|
let td = tempdir().unwrap();
|
||||||
|
let backend = LocalFsBackend::new(td.path()).unwrap();
|
||||||
|
|
||||||
|
for name in ["a.txt", "b.txt", "c.log", "README"] {
|
||||||
|
let h = backend.open(&p(name), opts_create()).await.unwrap();
|
||||||
|
h.close().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir_h = backend
|
||||||
|
.open(&SmbPath::root(), opts_open_dir())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let txts = dir_h.list_dir(Some("*.txt")).await.unwrap();
|
||||||
|
let names: Vec<_> = txts.iter().map(|e| e.info.name.as_str()).collect();
|
||||||
|
assert_eq!(names.len(), 2, "expected 2 .txt files, got {names:?}");
|
||||||
|
assert!(names.contains(&"a.txt"));
|
||||||
|
assert!(names.contains(&"b.txt"));
|
||||||
|
|
||||||
|
// Single-char wildcard.
|
||||||
|
let one = dir_h.list_dir(Some("?.log")).await.unwrap();
|
||||||
|
let names: Vec<_> = one.iter().map(|e| e.info.name.as_str()).collect();
|
||||||
|
assert_eq!(names, vec!["c.log"]);
|
||||||
|
|
||||||
|
// Case-insensitive.
|
||||||
|
let any_txt = dir_h.list_dir(Some("*.TXT")).await.unwrap();
|
||||||
|
assert_eq!(any_txt.len(), 2);
|
||||||
|
|
||||||
|
// "*" matches everything.
|
||||||
|
let all = dir_h.list_dir(Some("*")).await.unwrap();
|
||||||
|
assert_eq!(all.len(), 4);
|
||||||
|
|
||||||
|
dir_h.close().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glob_match_basics() {
|
||||||
|
assert!(glob_match("*", "anything"));
|
||||||
|
assert!(glob_match("*.txt", "foo.txt"));
|
||||||
|
assert!(!glob_match("*.txt", "foo.log"));
|
||||||
|
assert!(glob_match("a?c", "abc"));
|
||||||
|
assert!(!glob_match("a?c", "ac"));
|
||||||
|
assert!(glob_match("a*b*c", "axxxbxxxc"));
|
||||||
|
assert!(glob_match("FOO", "foo"));
|
||||||
|
assert!(glob_match("", ""));
|
||||||
|
assert!(!glob_match("", "a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filetime_round_trip() {
|
||||||
|
let now = SystemTime::now();
|
||||||
|
let ft = system_time_to_filetime(now);
|
||||||
|
let back = filetime_to_system_time(ft).unwrap();
|
||||||
|
let delta = now
|
||||||
|
.duration_since(back)
|
||||||
|
.or_else(|e| Ok::<_, std::time::SystemTimeError>(e.duration()))
|
||||||
|
.unwrap();
|
||||||
|
// 100ns granularity — round-trip should be sub-microsecond.
|
||||||
|
assert!(delta < Duration::from_micros(1), "delta = {delta:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
5
vendor/smb-server/src/fs/mod.rs
vendored
Normal file
5
vendor/smb-server/src/fs/mod.rs
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//! Local-filesystem [`ShareBackend`] for `smb-server`, sandboxed via `cap-std`.
|
||||||
|
|
||||||
|
mod local;
|
||||||
|
|
||||||
|
pub use local::LocalFsBackend;
|
||||||
19
vendor/smb-server/src/handlers/change_notify.rs
vendored
Normal file
19
vendor/smb-server/src/handlers/change_notify.rs
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//! CHANGE_NOTIFY handler — v1 always returns NOT_SUPPORTED.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
_conn: &Arc<Connection>,
|
||||||
|
_hdr: &Smb2Header,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED)
|
||||||
|
}
|
||||||
107
vendor/smb-server/src/handlers/close.rs
vendored
Normal file
107
vendor/smb-server/src/handlers/close.rs
vendored
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
//! CLOSE handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{CloseRequest, CloseResponse};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::lookup_session_tree;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
const FLAG_POSTQUERY_ATTRIB: u16 = 0x0001;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match CloseRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(s) => return HandlerResponse::err(s),
|
||||||
|
};
|
||||||
|
let removed = {
|
||||||
|
let tree = tree_arc.write().await;
|
||||||
|
let mut opens = tree.opens.write().await;
|
||||||
|
opens.remove(&req.file_id)
|
||||||
|
};
|
||||||
|
let open_arc = match removed {
|
||||||
|
Some(o) => o,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pull state out, close the handle, then optionally unlink.
|
||||||
|
let mut open = open_arc.write().await;
|
||||||
|
let handle = open.handle.take();
|
||||||
|
let path = open.last_path.clone();
|
||||||
|
let delete_on_close = open.delete_on_close;
|
||||||
|
let want_attrs = req.flags & FLAG_POSTQUERY_ATTRIB != 0;
|
||||||
|
drop(open);
|
||||||
|
|
||||||
|
// Stat before closing if needed.
|
||||||
|
let info_before_close = if want_attrs {
|
||||||
|
if let Some(h) = handle.as_ref() {
|
||||||
|
h.stat().await.ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(h) = handle {
|
||||||
|
let _ = h.close().await;
|
||||||
|
}
|
||||||
|
if delete_on_close {
|
||||||
|
let tree = tree_arc.read().await;
|
||||||
|
let backend = tree.share.backend.clone();
|
||||||
|
drop(tree);
|
||||||
|
if let Err(e) = backend.unlink(&path).await {
|
||||||
|
debug!(error = %e, "delete-on-close unlink failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = CloseResponse {
|
||||||
|
structure_size: 60,
|
||||||
|
flags: req.flags & FLAG_POSTQUERY_ATTRIB,
|
||||||
|
reserved: 0,
|
||||||
|
creation_time: info_before_close
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.creation_time)
|
||||||
|
.unwrap_or(0),
|
||||||
|
last_access_time: info_before_close
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.last_access_time)
|
||||||
|
.unwrap_or(0),
|
||||||
|
last_write_time: info_before_close
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.last_write_time)
|
||||||
|
.unwrap_or(0),
|
||||||
|
change_time: info_before_close
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.change_time)
|
||||||
|
.unwrap_or(0),
|
||||||
|
allocation_size: info_before_close
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.allocation_size)
|
||||||
|
.unwrap_or(0),
|
||||||
|
end_of_file: info_before_close
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.end_of_file)
|
||||||
|
.unwrap_or(0),
|
||||||
|
file_attributes: info_before_close
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.attributes())
|
||||||
|
.unwrap_or(0),
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
194
vendor/smb-server/src/handlers/create.rs
vendored
Normal file
194
vendor/smb-server/src/handlers/create.rs
vendored
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
//! CREATE handler — open or create a file/directory and allocate a FileId.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{CreateRequest, CreateResponse};
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
use crate::backend::{OpenIntent, OpenOptions};
|
||||||
|
use crate::builder::Access;
|
||||||
|
use crate::conn::state::{Connection, Open};
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::lookup_session_tree;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::path::SmbPath;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
use crate::utils::utf16le_to_units;
|
||||||
|
|
||||||
|
// MS-SMB2 §2.2.13 access mask flags
|
||||||
|
const FILE_READ_DATA: u32 = 0x0000_0001;
|
||||||
|
const FILE_WRITE_DATA: u32 = 0x0000_0002;
|
||||||
|
const FILE_APPEND_DATA: u32 = 0x0000_0004;
|
||||||
|
const FILE_READ_ATTRIBUTES: u32 = 0x0000_0080;
|
||||||
|
const FILE_WRITE_ATTRIBUTES: u32 = 0x0000_0100;
|
||||||
|
const DELETE: u32 = 0x0001_0000;
|
||||||
|
const GENERIC_READ: u32 = 0x8000_0000;
|
||||||
|
const GENERIC_WRITE: u32 = 0x4000_0000;
|
||||||
|
const GENERIC_ALL: u32 = 0x1000_0000;
|
||||||
|
const MAX_ALLOWED: u32 = 0x0200_0000;
|
||||||
|
|
||||||
|
// CreateOptions
|
||||||
|
const FILE_DIRECTORY_FILE: u32 = 0x0000_0001;
|
||||||
|
const FILE_NON_DIRECTORY_FILE: u32 = 0x0000_0040;
|
||||||
|
const FILE_DELETE_ON_CLOSE: u32 = 0x0000_1000;
|
||||||
|
|
||||||
|
// CreateDisposition
|
||||||
|
const FILE_SUPERSEDE: u32 = 0x0000_0000;
|
||||||
|
const FILE_OPEN: u32 = 0x0000_0001;
|
||||||
|
const FILE_CREATE: u32 = 0x0000_0002;
|
||||||
|
const FILE_OPEN_IF: u32 = 0x0000_0003;
|
||||||
|
const FILE_OVERWRITE: u32 = 0x0000_0004;
|
||||||
|
const FILE_OVERWRITE_IF: u32 = 0x0000_0005;
|
||||||
|
|
||||||
|
// CreateAction in response (MS-SMB2 §2.2.14)
|
||||||
|
const FILE_OPENED: u32 = 0x0000_0001;
|
||||||
|
const FILE_CREATED: u32 = 0x0000_0002;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match CreateRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(s) => return HandlerResponse::err(s),
|
||||||
|
};
|
||||||
|
let tree = tree_arc.read().await;
|
||||||
|
let granted = tree.granted_access;
|
||||||
|
let backend = tree.share.backend.clone();
|
||||||
|
drop(tree);
|
||||||
|
|
||||||
|
// Decode path.
|
||||||
|
let units = match utf16le_to_units(&req.name) {
|
||||||
|
Some(u) => u,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
|
||||||
|
};
|
||||||
|
let path = match SmbPath::from_utf16(&units) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translate disposition.
|
||||||
|
let intent = match req.create_disposition {
|
||||||
|
FILE_SUPERSEDE | FILE_OVERWRITE_IF => OpenIntent::OverwriteOrCreate,
|
||||||
|
FILE_OPEN => OpenIntent::Open,
|
||||||
|
FILE_CREATE => OpenIntent::Create,
|
||||||
|
FILE_OPEN_IF => OpenIntent::OpenOrCreate,
|
||||||
|
FILE_OVERWRITE => OpenIntent::Truncate,
|
||||||
|
_ => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translate desired access into read/write hints.
|
||||||
|
let want_read = req.desired_access
|
||||||
|
& (FILE_READ_DATA | FILE_READ_ATTRIBUTES | GENERIC_READ | GENERIC_ALL | MAX_ALLOWED)
|
||||||
|
!= 0;
|
||||||
|
let want_write = req.desired_access
|
||||||
|
& (FILE_WRITE_DATA
|
||||||
|
| FILE_APPEND_DATA
|
||||||
|
| FILE_WRITE_ATTRIBUTES
|
||||||
|
| DELETE
|
||||||
|
| GENERIC_WRITE
|
||||||
|
| GENERIC_ALL
|
||||||
|
| MAX_ALLOWED)
|
||||||
|
!= 0;
|
||||||
|
|
||||||
|
// Reject writes on a read-only tree.
|
||||||
|
if want_write && !granted.allows_write() {
|
||||||
|
warn!(path = %path, "write open on read-only tree");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
// Disposition that creates: requires write permission.
|
||||||
|
if !granted.allows_write()
|
||||||
|
&& matches!(
|
||||||
|
intent,
|
||||||
|
OpenIntent::Create
|
||||||
|
| OpenIntent::OpenOrCreate
|
||||||
|
| OpenIntent::OverwriteOrCreate
|
||||||
|
| OpenIntent::Truncate
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
let directory = req.create_options & FILE_DIRECTORY_FILE != 0;
|
||||||
|
let non_directory = req.create_options & FILE_NON_DIRECTORY_FILE != 0;
|
||||||
|
if directory && non_directory {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
let delete_on_close = req.create_options & FILE_DELETE_ON_CLOSE != 0;
|
||||||
|
|
||||||
|
let opts = OpenOptions {
|
||||||
|
read: want_read || !want_write,
|
||||||
|
write: want_write,
|
||||||
|
intent,
|
||||||
|
directory,
|
||||||
|
non_directory,
|
||||||
|
delete_on_close,
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = match backend.open(&path, opts).await {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(e) => {
|
||||||
|
debug!(error = %e, path = %path, "backend open failed");
|
||||||
|
return HandlerResponse::err(e.to_nt_status());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stat for the response.
|
||||||
|
let info = match handle.stat().await {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(e) => {
|
||||||
|
let _ = handle.close().await;
|
||||||
|
return HandlerResponse::err(e.to_nt_status());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allocate FileId, register Open.
|
||||||
|
let tree = tree_arc.write().await;
|
||||||
|
let file_id = tree.alloc_file_id();
|
||||||
|
let open = Open::new(
|
||||||
|
file_id,
|
||||||
|
handle,
|
||||||
|
if want_write { granted } else { Access::Read },
|
||||||
|
path,
|
||||||
|
info.is_directory,
|
||||||
|
delete_on_close,
|
||||||
|
);
|
||||||
|
let open_arc = Arc::new(tokio::sync::RwLock::new(open));
|
||||||
|
tree.opens.write().await.insert(file_id, open_arc);
|
||||||
|
drop(tree);
|
||||||
|
|
||||||
|
let create_action = match intent {
|
||||||
|
OpenIntent::Create => FILE_CREATED,
|
||||||
|
OpenIntent::OpenOrCreate | OpenIntent::OverwriteOrCreate => FILE_OPENED,
|
||||||
|
OpenIntent::Open | OpenIntent::Truncate => FILE_OPENED,
|
||||||
|
};
|
||||||
|
let resp = CreateResponse {
|
||||||
|
structure_size: 89,
|
||||||
|
oplock_level: 0,
|
||||||
|
flags: 0,
|
||||||
|
create_action,
|
||||||
|
creation_time: info.creation_time,
|
||||||
|
last_access_time: info.last_access_time,
|
||||||
|
last_write_time: info.last_write_time,
|
||||||
|
change_time: info.change_time,
|
||||||
|
allocation_size: info.allocation_size,
|
||||||
|
end_of_file: info.end_of_file,
|
||||||
|
file_attributes: info.attributes(),
|
||||||
|
reserved2: 0,
|
||||||
|
file_id,
|
||||||
|
create_contexts_offset: 0,
|
||||||
|
create_contexts_length: 0,
|
||||||
|
create_contexts: vec![],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
21
vendor/smb-server/src/handlers/echo.rs
vendored
Normal file
21
vendor/smb-server/src/handlers/echo.rs
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//! ECHO handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::EchoResponse;
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
_conn: &Arc<Connection>,
|
||||||
|
_hdr: &Smb2Header,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
EchoResponse::default().write_to(&mut buf).expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
46
vendor/smb-server/src/handlers/flush.rs
vendored
Normal file
46
vendor/smb-server/src/handlers/flush.rs
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//! FLUSH handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{FileId, FlushRequest, FlushResponse};
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::{lookup_open, lookup_session_tree};
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match FlushRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
let fid = FileId::new(req.file_id_persistent, req.file_id_volatile);
|
||||||
|
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(s) => return HandlerResponse::err(s),
|
||||||
|
};
|
||||||
|
let open_arc = match lookup_open(&tree_arc, fid).await {
|
||||||
|
Some(o) => o,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
};
|
||||||
|
let res = {
|
||||||
|
let open = open_arc.read().await;
|
||||||
|
match open.handle.as_ref() {
|
||||||
|
Some(h) => h.flush().await,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = res {
|
||||||
|
return HandlerResponse::err(e.to_nt_status());
|
||||||
|
}
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
FlushResponse::default().write_to(&mut buf).expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
59
vendor/smb-server/src/handlers/ioctl.rs
vendored
Normal file
59
vendor/smb-server/src/handlers/ioctl.rs
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//! IOCTL handler — handles FSCTL_VALIDATE_NEGOTIATE_INFO; everything else
|
||||||
|
//! returns NOT_SUPPORTED.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{Fsctl, IoctlRequest, IoctlResponse};
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::negotiate::{NEGOTIATE_CAPABILITIES, NEGOTIATE_SECURITY_MODE};
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
_hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match IoctlRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
|
||||||
|
match req.fsctl() {
|
||||||
|
Fsctl::ValidateNegotiateInfo => {
|
||||||
|
// Build VALIDATE_NEGOTIATE_INFO_RESPONSE per MS-SMB2 §2.2.32.6:
|
||||||
|
// Capabilities (4) | Guid (16) | SecurityMode (2) | Dialect (2) = 24 bytes.
|
||||||
|
let dialect = conn.dialect.read().await.map(|d| d.as_u16()).unwrap_or(0);
|
||||||
|
let mut out = Vec::with_capacity(24);
|
||||||
|
out.extend_from_slice(&NEGOTIATE_CAPABILITIES.to_le_bytes());
|
||||||
|
out.extend_from_slice(server.config.server_guid.as_bytes());
|
||||||
|
out.extend_from_slice(&NEGOTIATE_SECURITY_MODE.to_le_bytes());
|
||||||
|
out.extend_from_slice(&dialect.to_le_bytes());
|
||||||
|
|
||||||
|
let resp = IoctlResponse {
|
||||||
|
structure_size: 49,
|
||||||
|
reserved: 0,
|
||||||
|
ctl_code: req.ctl_code,
|
||||||
|
file_id: req.file_id,
|
||||||
|
input_offset: 0,
|
||||||
|
input_count: 0,
|
||||||
|
output_offset: 0x70,
|
||||||
|
output_count: out.len() as u32,
|
||||||
|
flags: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
output: out,
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).expect("IOCTL response encodes");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
|
Fsctl::DfsGetReferrals | Fsctl::DfsGetReferralsEx => {
|
||||||
|
HandlerResponse::err(ntstatus::STATUS_FS_DRIVER_REQUIRED)
|
||||||
|
}
|
||||||
|
_ => HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
|
||||||
|
}
|
||||||
|
}
|
||||||
21
vendor/smb-server/src/handlers/lock.rs
vendored
Normal file
21
vendor/smb-server/src/handlers/lock.rs
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//! LOCK handler — v1 returns success without enforcing locks.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::LockResponse;
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
_conn: &Arc<Connection>,
|
||||||
|
_hdr: &Smb2Header,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
LockResponse::default().write_to(&mut buf).expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
28
vendor/smb-server/src/handlers/logoff.rs
vendored
Normal file
28
vendor/smb-server/src/handlers/logoff.rs
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//! LOGOFF handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::LogoffResponse;
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
if hdr.session_id == 0 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_USER_SESSION_DELETED);
|
||||||
|
}
|
||||||
|
conn.close_session(hdr.session_id).await;
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
LogoffResponse::default()
|
||||||
|
.write_to(&mut buf)
|
||||||
|
.expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
64
vendor/smb-server/src/handlers/mod.rs
vendored
Normal file
64
vendor/smb-server/src/handlers/mod.rs
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
//! Per-command handlers.
|
||||||
|
//!
|
||||||
|
//! Each function here builds a `HandlerResponse` for a specific SMB2 command.
|
||||||
|
//! Handlers receive the parsed request header and a slice of the body bytes;
|
||||||
|
//! they return either a successful body or `HandlerResponse::err(ntstatus)`.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::{Command, Smb2Header};
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
mod change_notify;
|
||||||
|
mod close;
|
||||||
|
mod create;
|
||||||
|
mod echo;
|
||||||
|
mod flush;
|
||||||
|
mod ioctl;
|
||||||
|
mod lock;
|
||||||
|
mod logoff;
|
||||||
|
pub(crate) mod negotiate;
|
||||||
|
mod oplock_break;
|
||||||
|
mod query_directory;
|
||||||
|
mod query_info;
|
||||||
|
mod read;
|
||||||
|
mod session_setup;
|
||||||
|
mod set_info;
|
||||||
|
pub(crate) mod shared;
|
||||||
|
mod tree_connect;
|
||||||
|
mod tree_disconnect;
|
||||||
|
mod write;
|
||||||
|
|
||||||
|
/// Top-level command router.
|
||||||
|
pub async fn dispatch_command(
|
||||||
|
server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
match hdr.command {
|
||||||
|
Command::Negotiate => negotiate::handle(server, conn, hdr, body).await,
|
||||||
|
Command::SessionSetup => session_setup::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Logoff => logoff::handle(server, conn, hdr, body).await,
|
||||||
|
Command::TreeConnect => tree_connect::handle(server, conn, hdr, body).await,
|
||||||
|
Command::TreeDisconnect => tree_disconnect::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Create => create::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Close => close::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Flush => flush::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Read => read::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Write => write::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Lock => lock::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Ioctl => ioctl::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Echo => echo::handle(server, conn, hdr, body).await,
|
||||||
|
Command::QueryDirectory => query_directory::handle(server, conn, hdr, body).await,
|
||||||
|
Command::ChangeNotify => change_notify::handle(server, conn, hdr, body).await,
|
||||||
|
Command::QueryInfo => query_info::handle(server, conn, hdr, body).await,
|
||||||
|
Command::SetInfo => set_info::handle(server, conn, hdr, body).await,
|
||||||
|
Command::OplockBreak => oplock_break::handle(server, conn, hdr, body).await,
|
||||||
|
Command::Cancel => HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
}
|
||||||
|
}
|
||||||
223
vendor/smb-server/src/handlers/negotiate.rs
vendored
Normal file
223
vendor/smb-server/src/handlers/negotiate.rs
vendored
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
//! NEGOTIATE handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::auth::spnego::encode_init_response;
|
||||||
|
use crate::proto::crypto::SigningAlgo;
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{
|
||||||
|
Dialect, NegotiateContext, NegotiateRequest, NegotiateResponse, PreauthIntegrityCapabilities,
|
||||||
|
SigningCapabilities,
|
||||||
|
};
|
||||||
|
use tracing::info;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
use crate::utils::{fill_random, now_filetime};
|
||||||
|
|
||||||
|
// MS-SMB2 §2.2.4 SecurityMode bits. Keep SIGNING_REQUIRED clear: anonymous
|
||||||
|
// Linux cifs mounts do not send enough NTLM material for the server to derive
|
||||||
|
// matching SMB3 signing keys.
|
||||||
|
pub(crate) const NEGOTIATE_SECURITY_MODE: u16 = 0x0001;
|
||||||
|
|
||||||
|
const CAP_DFS: u32 = 0x0000_0001;
|
||||||
|
const CAP_LEASING: u32 = 0x0000_0002;
|
||||||
|
const CAP_LARGE_MTU: u32 = 0x0000_0004;
|
||||||
|
pub(crate) const NEGOTIATE_CAPABILITIES: u32 = CAP_DFS | CAP_LEASING | CAP_LARGE_MTU;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
_hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match NegotiateRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pick the highest dialect we support that the client offered.
|
||||||
|
const SUPPORTED: &[u16] = &[0x0202, 0x0210, 0x0300, 0x0302, 0x0311];
|
||||||
|
let mut chosen: Option<u16> = None;
|
||||||
|
for &d in &req.dialects {
|
||||||
|
if SUPPORTED.contains(&d) {
|
||||||
|
chosen = match chosen {
|
||||||
|
None => Some(d),
|
||||||
|
Some(prev) if d > prev => Some(d),
|
||||||
|
Some(prev) => Some(prev),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let chosen = match chosen {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
|
||||||
|
};
|
||||||
|
let dialect = match Dialect::from_u16(chosen) {
|
||||||
|
Some(dialect) => dialect,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
|
||||||
|
};
|
||||||
|
*conn.dialect.write().await = Some(dialect);
|
||||||
|
*conn.client_guid.write().await = Uuid::from_bytes(req.client_guid);
|
||||||
|
*conn.signing_algo.write().await = match dialect {
|
||||||
|
Dialect::Smb202 | Dialect::Smb210 => SigningAlgo::HmacSha256,
|
||||||
|
_ => SigningAlgo::AesCmac,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build SPNEGO security blob (mech-list-only, advertising NTLMSSP).
|
||||||
|
let security_blob = encode_init_response();
|
||||||
|
let security_buffer_offset: u16 = 64 + 64; // SMB2 header + fixed NEG response (64 bytes)
|
||||||
|
let security_buffer_length: u16 = security_blob.len() as u16;
|
||||||
|
|
||||||
|
// For 3.1.1 build negotiate contexts.
|
||||||
|
let mut contexts_bytes: Vec<u8> = Vec::new();
|
||||||
|
let mut context_count: u16 = 0;
|
||||||
|
let mut negotiate_context_offset: u32 = 0;
|
||||||
|
|
||||||
|
if dialect == Dialect::Smb311 {
|
||||||
|
// PREAUTH_INTEGRITY_CAPABILITIES
|
||||||
|
let mut salt = [0u8; 32];
|
||||||
|
fill_random(&mut salt);
|
||||||
|
let preauth_caps = PreauthIntegrityCapabilities {
|
||||||
|
hash_algorithm_count: 1,
|
||||||
|
salt_length: 32,
|
||||||
|
hash_algorithms: vec![PreauthIntegrityCapabilities::HASH_SHA512],
|
||||||
|
salt: salt.to_vec(),
|
||||||
|
};
|
||||||
|
let preauth_data = {
|
||||||
|
use binrw::BinWrite;
|
||||||
|
let mut c = std::io::Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(&preauth_caps, &mut c).expect("preauth negotiate context encodes");
|
||||||
|
c.into_inner()
|
||||||
|
};
|
||||||
|
let preauth_ctx = NegotiateContext {
|
||||||
|
context_type: NegotiateContext::TYPE_PREAUTH_INTEGRITY,
|
||||||
|
data_length: preauth_data.len() as u16,
|
||||||
|
reserved: 0,
|
||||||
|
data: preauth_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
// SIGNING_CAPABILITIES — advertise AES-CMAC.
|
||||||
|
let signing_caps = SigningCapabilities {
|
||||||
|
signing_algorithm_count: 1,
|
||||||
|
signing_algorithms: vec![SigningCapabilities::ALGORITHM_AES_CMAC],
|
||||||
|
};
|
||||||
|
let signing_data = {
|
||||||
|
use binrw::BinWrite;
|
||||||
|
let mut c = std::io::Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(&signing_caps, &mut c).expect("signing negotiate context encodes");
|
||||||
|
c.into_inner()
|
||||||
|
};
|
||||||
|
let signing_ctx = NegotiateContext {
|
||||||
|
context_type: NegotiateContext::TYPE_SIGNING,
|
||||||
|
data_length: signing_data.len() as u16,
|
||||||
|
reserved: 0,
|
||||||
|
data: signing_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ctxs = vec![preauth_ctx, signing_ctx];
|
||||||
|
if let Err(e) = NegotiateContext::encode_list(&ctxs, &mut contexts_bytes) {
|
||||||
|
tracing::error!(error = %e, "encode_list failed");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
context_count = ctxs.len() as u16;
|
||||||
|
|
||||||
|
// The contexts go after security buffer, 8-byte aligned.
|
||||||
|
let post_security = security_buffer_offset as u32 + security_buffer_length as u32;
|
||||||
|
// Round up to next multiple of 8 from the start of the SMB2 header.
|
||||||
|
negotiate_context_offset = (post_security + 7) & !7;
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_read_size = *conn.max_read_size.read().await;
|
||||||
|
let max_write_size = *conn.max_write_size.read().await;
|
||||||
|
let max_transact_size = max_read_size; // common practice
|
||||||
|
|
||||||
|
let resp = NegotiateResponse {
|
||||||
|
structure_size: 65,
|
||||||
|
security_mode: NEGOTIATE_SECURITY_MODE,
|
||||||
|
dialect_revision: chosen,
|
||||||
|
negotiate_context_count_or_reserved: context_count,
|
||||||
|
server_guid: *server.config.server_guid.as_bytes(),
|
||||||
|
capabilities: NEGOTIATE_CAPABILITIES,
|
||||||
|
max_transact_size,
|
||||||
|
max_read_size,
|
||||||
|
max_write_size,
|
||||||
|
system_time: now_filetime(),
|
||||||
|
server_start_time: server.server_start_filetime,
|
||||||
|
security_buffer_offset,
|
||||||
|
security_buffer_length,
|
||||||
|
negotiate_context_offset_or_reserved2: negotiate_context_offset,
|
||||||
|
security_buffer: security_blob,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut body_out = Vec::new();
|
||||||
|
if let Err(e) = resp.write_to(&mut body_out) {
|
||||||
|
tracing::error!(error = %e, "encode NEGOTIATE response");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
// Append padding to align contexts at `negotiate_context_offset`.
|
||||||
|
if dialect == Dialect::Smb311 && context_count > 0 {
|
||||||
|
let cur = 64 + body_out.len() as u32; // header + body so far
|
||||||
|
if cur < negotiate_context_offset {
|
||||||
|
let pad = (negotiate_context_offset - cur) as usize;
|
||||||
|
body_out.extend(std::iter::repeat_n(0u8, pad));
|
||||||
|
}
|
||||||
|
body_out.extend_from_slice(&contexts_bytes);
|
||||||
|
}
|
||||||
|
info!(?dialect, "NEGOTIATE complete");
|
||||||
|
let mut hr = HandlerResponse::ok(body_out);
|
||||||
|
hr.skip_signing = true;
|
||||||
|
hr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the SMB2 NEGOTIATE response sent in reply to an SMB1 multi-protocol
|
||||||
|
/// NEGOTIATE_REQUEST that listed an SMB2 dialect (MS-SMB2 §3.3.5.3.1).
|
||||||
|
///
|
||||||
|
/// We do NOT commit the connection dialect here — the client will follow up
|
||||||
|
/// with a real SMB2 NEGOTIATE which goes through [`handle`]. This response
|
||||||
|
/// only tells the client "yes, I speak SMB2; send me an SMB2 NEGOTIATE next".
|
||||||
|
pub async fn multi_protocol_response(
|
||||||
|
server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
chosen: u16,
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let security_blob = encode_init_response();
|
||||||
|
let security_buffer_offset: u16 = 64 + 64;
|
||||||
|
let security_buffer_length: u16 = security_blob.len() as u16;
|
||||||
|
let max_read_size = *conn.max_read_size.read().await;
|
||||||
|
let max_write_size = *conn.max_write_size.read().await;
|
||||||
|
let max_transact_size = max_read_size;
|
||||||
|
|
||||||
|
let resp = NegotiateResponse {
|
||||||
|
structure_size: 65,
|
||||||
|
security_mode: NEGOTIATE_SECURITY_MODE,
|
||||||
|
dialect_revision: chosen,
|
||||||
|
negotiate_context_count_or_reserved: 0,
|
||||||
|
server_guid: *server.config.server_guid.as_bytes(),
|
||||||
|
capabilities: 0,
|
||||||
|
max_transact_size,
|
||||||
|
max_read_size,
|
||||||
|
max_write_size,
|
||||||
|
system_time: now_filetime(),
|
||||||
|
server_start_time: server.server_start_filetime,
|
||||||
|
security_buffer_offset,
|
||||||
|
security_buffer_length,
|
||||||
|
negotiate_context_offset_or_reserved2: 0,
|
||||||
|
security_buffer: security_blob,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut body_out = Vec::new();
|
||||||
|
if let Err(e) = resp.write_to(&mut body_out) {
|
||||||
|
tracing::error!(error = %e, "encode multi-protocol NEGOTIATE response");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
info!(
|
||||||
|
chosen = %format_args!("0x{chosen:04X}"),
|
||||||
|
"SMB1 multi-protocol -> SMB2"
|
||||||
|
);
|
||||||
|
let mut hr = HandlerResponse::ok(body_out);
|
||||||
|
hr.skip_signing = true;
|
||||||
|
hr
|
||||||
|
}
|
||||||
27
vendor/smb-server/src/handlers/oplock_break.rs
vendored
Normal file
27
vendor/smb-server/src/handlers/oplock_break.rs
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//! OPLOCK_BREAK handler — acknowledge breaks without granting oplocks.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::FileId;
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
_conn: &Arc<Connection>,
|
||||||
|
_hdr: &Smb2Header,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
// Echo back the same shape as the notification — structure_size=24, level=0.
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
buf.extend_from_slice(&24u16.to_le_bytes()); // structure_size
|
||||||
|
buf.push(0); // OplockLevel
|
||||||
|
buf.push(0); // Reserved
|
||||||
|
buf.extend_from_slice(&0u32.to_le_bytes()); // Reserved2
|
||||||
|
buf.extend_from_slice(&FileId::any().persistent.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&FileId::any().volatile.to_le_bytes());
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
136
vendor/smb-server/src/handlers/query_directory.rs
vendored
Normal file
136
vendor/smb-server/src/handlers/query_directory.rs
vendored
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
//! QUERY_DIRECTORY handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{FileInfoClass, QueryDirectoryRequest, QueryDirectoryResponse};
|
||||||
|
|
||||||
|
use crate::conn::state::{Connection, DirCursor};
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::{lookup_open, lookup_session_tree};
|
||||||
|
use crate::info_class::{align8, encode_dir_entry};
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
use crate::utils::utf16le_to_string;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match QueryDirectoryRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
if FileInfoClass::from_u8(req.file_information_class).is_none() {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS);
|
||||||
|
}
|
||||||
|
let class_byte = req.file_information_class;
|
||||||
|
|
||||||
|
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(s) => return HandlerResponse::err(s),
|
||||||
|
};
|
||||||
|
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
|
||||||
|
Some(o) => o,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pattern_str = utf16le_to_string(&req.file_name);
|
||||||
|
let pattern: Option<String> = if pattern_str.is_empty() || pattern_str == "*" {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(pattern_str)
|
||||||
|
};
|
||||||
|
|
||||||
|
let restart = req.flags & QueryDirectoryRequest::FLAG_RESTART_SCANS != 0
|
||||||
|
|| req.flags & QueryDirectoryRequest::FLAG_REOPEN != 0;
|
||||||
|
let single_entry = req.flags & QueryDirectoryRequest::FLAG_RETURN_SINGLE_ENTRY != 0;
|
||||||
|
|
||||||
|
// Populate or refresh the cursor.
|
||||||
|
{
|
||||||
|
let mut open = open_arc.write().await;
|
||||||
|
if !open.is_directory {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
if open.search_state.is_none() || restart {
|
||||||
|
let entries = match open.handle.as_ref() {
|
||||||
|
Some(h) => h.list_dir(pattern.as_deref()).await,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
};
|
||||||
|
let entries = match entries {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(e) => return HandlerResponse::err(e.to_nt_status()),
|
||||||
|
};
|
||||||
|
open.search_state = Some(DirCursor {
|
||||||
|
entries,
|
||||||
|
next: 0,
|
||||||
|
pattern: pattern.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode entries into the output buffer.
|
||||||
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
|
let mut last_offset_pos: Option<usize> = None;
|
||||||
|
let cap = req.output_buffer_length as usize;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut open = open_arc.write().await;
|
||||||
|
let cursor = open.search_state.as_mut().expect("populated above");
|
||||||
|
loop {
|
||||||
|
if cursor.next >= cursor.entries.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let entry = &cursor.entries[cursor.next];
|
||||||
|
let file_index = entry.info.file_index;
|
||||||
|
let mut bytes = encode_dir_entry(class_byte, entry, file_index);
|
||||||
|
if bytes.is_empty() {
|
||||||
|
cursor.next += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine total size with padding for chaining.
|
||||||
|
let entry_aligned = align8(bytes.len());
|
||||||
|
// If this is *not* the first entry, we already padded the previous
|
||||||
|
// entry up to entry_aligned. We commit only if total fits.
|
||||||
|
let prev_len = buf.len();
|
||||||
|
let total_after = prev_len + entry_aligned;
|
||||||
|
if total_after > cap && !buf.is_empty() {
|
||||||
|
// No room for this entry; stop.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Patch previous NextEntryOffset.
|
||||||
|
if let Some(prev_off) = last_offset_pos {
|
||||||
|
let delta = (prev_len - prev_off) as u32;
|
||||||
|
buf[prev_off..prev_off + 4].copy_from_slice(&delta.to_le_bytes());
|
||||||
|
}
|
||||||
|
// Track NextEntryOffset position for the entry we are appending.
|
||||||
|
last_offset_pos = Some(prev_len);
|
||||||
|
// Append the entry, then pad to 8.
|
||||||
|
let target_len = prev_len + entry_aligned;
|
||||||
|
buf.append(&mut bytes);
|
||||||
|
while buf.len() < target_len {
|
||||||
|
buf.push(0);
|
||||||
|
}
|
||||||
|
cursor.next += 1;
|
||||||
|
if single_entry {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if buf.is_empty() {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_NO_MORE_FILES);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = QueryDirectoryResponse {
|
||||||
|
structure_size: 9,
|
||||||
|
output_buffer_offset: 64 + 8,
|
||||||
|
output_buffer_length: buf.len() as u32,
|
||||||
|
buffer: buf,
|
||||||
|
};
|
||||||
|
let mut out = Vec::new();
|
||||||
|
resp.write_to(&mut out).expect("encode");
|
||||||
|
HandlerResponse::ok(out)
|
||||||
|
}
|
||||||
144
vendor/smb-server/src/handlers/query_info.rs
vendored
Normal file
144
vendor/smb-server/src/handlers/query_info.rs
vendored
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
//! QUERY_INFO handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{InfoType, QueryInfoRequest, QueryInfoResponse};
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::{lookup_open, lookup_session_tree};
|
||||||
|
use crate::info_class as ic;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
const FILE_DEVICE_DISK: u32 = 0x0000_0007;
|
||||||
|
const FILE_REMOTE_DEVICE: u32 = 0x0000_0010;
|
||||||
|
|
||||||
|
// FS attribute flags (MS-FSCC §2.5.1)
|
||||||
|
const FILE_CASE_SENSITIVE_SEARCH: u32 = 0x0000_0001;
|
||||||
|
const FILE_CASE_PRESERVED_NAMES: u32 = 0x0000_0002;
|
||||||
|
const FILE_UNICODE_ON_DISK: u32 = 0x0000_0004;
|
||||||
|
const FILE_PERSISTENT_ACLS: u32 = 0x0000_0008;
|
||||||
|
const FILE_FILE_COMPRESSION: u32 = 0x0000_0010;
|
||||||
|
const FILE_SUPPORTS_HARD_LINKS: u32 = 0x0040_0000;
|
||||||
|
const FILE_SUPPORTS_EXTENDED_ATTRIBUTES: u32 = 0x0080_0000;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match QueryInfoRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
let info_type = match req.info_type_enum() {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(s) => return HandlerResponse::err(s),
|
||||||
|
};
|
||||||
|
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
|
||||||
|
Some(o) => o,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pull the file index (we use FileId.volatile as the unique handle id).
|
||||||
|
let (file_index, info_res) = {
|
||||||
|
let open = open_arc.read().await;
|
||||||
|
let fid = open.file_id;
|
||||||
|
match open.handle.as_ref() {
|
||||||
|
Some(h) => (fid.volatile, h.stat().await),
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let buf: Vec<u8> = match info_type {
|
||||||
|
InfoType::File => {
|
||||||
|
let info = match info_res {
|
||||||
|
Ok(i) => i,
|
||||||
|
Err(e) => return HandlerResponse::err(e.to_nt_status()),
|
||||||
|
};
|
||||||
|
match req.file_information_class {
|
||||||
|
ic::FILE_BASIC_INFORMATION => ic::encode_file_basic_information(&info),
|
||||||
|
ic::FILE_STANDARD_INFORMATION => ic::encode_file_standard_information(&info),
|
||||||
|
ic::FILE_INTERNAL_INFORMATION => ic::encode_file_internal_information(file_index),
|
||||||
|
ic::FILE_EA_INFORMATION => ic::encode_file_ea_information(),
|
||||||
|
ic::FILE_FULL_EA_INFORMATION => {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_NO_EAS_ON_FILE);
|
||||||
|
}
|
||||||
|
ic::FILE_ACCESS_INFORMATION => ic::encode_file_access_information(0x001F_01FF),
|
||||||
|
ic::FILE_POSITION_INFORMATION => ic::encode_file_position_information(),
|
||||||
|
ic::FILE_MODE_INFORMATION => ic::encode_file_mode_information(0),
|
||||||
|
ic::FILE_ALIGNMENT_INFORMATION => ic::encode_file_alignment_information(),
|
||||||
|
ic::FILE_NAME_INFORMATION => ic::encode_file_name_information(&info.name),
|
||||||
|
ic::FILE_ALL_INFORMATION => {
|
||||||
|
ic::encode_file_all_information(&info, file_index, 0x001F_01FF)
|
||||||
|
}
|
||||||
|
ic::FILE_NETWORK_OPEN_INFORMATION => {
|
||||||
|
ic::encode_file_network_open_information(&info)
|
||||||
|
}
|
||||||
|
ic::FILE_STREAM_INFORMATION => ic::encode_file_stream_information(&info),
|
||||||
|
_ => return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InfoType::FileSystem => {
|
||||||
|
// For FS info we use the open's tree's backend for context.
|
||||||
|
let creation_time = info_res.as_ref().map(|i| i.creation_time).unwrap_or(0);
|
||||||
|
match req.file_information_class {
|
||||||
|
ic::FS_VOLUME_INFORMATION => {
|
||||||
|
ic::encode_fs_volume_information(creation_time, 0xCAFE_BABE, "smb-server")
|
||||||
|
}
|
||||||
|
ic::FS_SIZE_INFORMATION => {
|
||||||
|
// 1 PiB free pseudo-volume, 4 KiB cluster.
|
||||||
|
ic::encode_fs_size_information(
|
||||||
|
1u64 << 40, // total
|
||||||
|
1u64 << 39, // free
|
||||||
|
1, // sectors per cluster
|
||||||
|
4096, // bytes per sector
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ic::FS_DEVICE_INFORMATION => {
|
||||||
|
ic::encode_fs_device_information(FILE_DEVICE_DISK, FILE_REMOTE_DEVICE)
|
||||||
|
}
|
||||||
|
ic::FS_ATTRIBUTE_INFORMATION => ic::encode_fs_attribute_information(
|
||||||
|
FILE_CASE_SENSITIVE_SEARCH
|
||||||
|
| FILE_CASE_PRESERVED_NAMES
|
||||||
|
| FILE_UNICODE_ON_DISK
|
||||||
|
| FILE_PERSISTENT_ACLS
|
||||||
|
| FILE_FILE_COMPRESSION
|
||||||
|
| FILE_SUPPORTS_HARD_LINKS
|
||||||
|
| FILE_SUPPORTS_EXTENDED_ATTRIBUTES,
|
||||||
|
255,
|
||||||
|
"NTFS",
|
||||||
|
),
|
||||||
|
ic::FS_FULL_SIZE_INFORMATION => {
|
||||||
|
ic::encode_fs_full_size_information(1u64 << 40, 1u64 << 39, 1u64 << 39, 1, 4096)
|
||||||
|
}
|
||||||
|
_ => return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InfoType::Security => ic::encode_minimal_security_descriptor(),
|
||||||
|
InfoType::Quota => return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
|
||||||
|
};
|
||||||
|
|
||||||
|
if buf.len() as u32 > req.output_buffer_length {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = QueryInfoResponse {
|
||||||
|
structure_size: 9,
|
||||||
|
output_buffer_offset: 64 + 8,
|
||||||
|
output_buffer_length: buf.len() as u32,
|
||||||
|
buffer: buf,
|
||||||
|
};
|
||||||
|
let mut out = Vec::new();
|
||||||
|
resp.write_to(&mut out)
|
||||||
|
.expect("QUERY_INFO response encodes");
|
||||||
|
HandlerResponse::ok(out)
|
||||||
|
}
|
||||||
62
vendor/smb-server/src/handlers/read.rs
vendored
Normal file
62
vendor/smb-server/src/handlers/read.rs
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
//! READ handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{ReadRequest, ReadResponse};
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::{lookup_open, lookup_session_tree};
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match ReadRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
let max_read = *conn.max_read_size.read().await;
|
||||||
|
if req.length > max_read {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(s) => return HandlerResponse::err(s),
|
||||||
|
};
|
||||||
|
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
|
||||||
|
Some(o) => o,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
};
|
||||||
|
let result = {
|
||||||
|
let open = open_arc.read().await;
|
||||||
|
match open.handle.as_ref() {
|
||||||
|
Some(h) => h.read(req.offset, req.length).await,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let bytes = match result {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => return HandlerResponse::err(e.to_nt_status()),
|
||||||
|
};
|
||||||
|
if bytes.is_empty() && req.length > 0 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_END_OF_FILE);
|
||||||
|
}
|
||||||
|
let resp = ReadResponse {
|
||||||
|
structure_size: 17,
|
||||||
|
data_offset: ReadResponse::STANDARD_DATA_OFFSET,
|
||||||
|
reserved: 0,
|
||||||
|
data_length: bytes.len() as u32,
|
||||||
|
data_remaining: 0,
|
||||||
|
flags: 0,
|
||||||
|
data: bytes.to_vec(),
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
262
vendor/smb-server/src/handlers/session_setup.rs
vendored
Normal file
262
vendor/smb-server/src/handlers/session_setup.rs
vendored
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
//! SESSION_SETUP handler — drives the SPNEGO + NTLMv2 state machine.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::auth::ntlm::{Identity, NtlmServer, NtlmTargetInfo, UserCreds};
|
||||||
|
use crate::proto::auth::spnego::{
|
||||||
|
NegState, OID_NTLMSSP, decode_init_token, decode_resp_token, encode_resp_token,
|
||||||
|
};
|
||||||
|
use crate::proto::crypto::signing_key_30;
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{Dialect, SessionSetupRequest, SessionSetupResponse};
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::conn::state::{Connection, Session};
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
use crate::utils::{fill_random, now_filetime};
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match SessionSetupRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
|
||||||
|
let blob = req.security_buffer;
|
||||||
|
if blob.is_empty() {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||||
|
let mut first8 = String::with_capacity(16);
|
||||||
|
for b in blob.iter().take(8) {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
write!(&mut first8, "{b:02x}").expect("writing to String cannot fail");
|
||||||
|
}
|
||||||
|
tracing::debug!(
|
||||||
|
first8 = %first8,
|
||||||
|
len = blob.len(),
|
||||||
|
sid = hdr.session_id,
|
||||||
|
"session setup blob"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide which form the security blob takes:
|
||||||
|
// * GSS-API NegTokenInit — starts with 0x60.
|
||||||
|
// * SPNEGO NegTokenResp — starts with 0xa1 ([1] context tag).
|
||||||
|
// * Raw NTLMSSP message — starts with "NTLMSSP\0" (RFC 4178
|
||||||
|
// §4.2.1 lets the client skip SPNEGO once the mech is settled; both
|
||||||
|
// Win11 reauth and Linux cifs.ko use this form).
|
||||||
|
const NTLMSSP_MAGIC: &[u8] = b"NTLMSSP\0";
|
||||||
|
let inner_token: Vec<u8>;
|
||||||
|
let is_first_round: bool;
|
||||||
|
let is_raw_ntlmssp: bool;
|
||||||
|
if blob.starts_with(NTLMSSP_MAGIC) {
|
||||||
|
// Raw NTLMSSP. Decide round by message-type at offset 8.
|
||||||
|
let msg_type = if blob.len() >= 12 {
|
||||||
|
u32::from_le_bytes([blob[8], blob[9], blob[10], blob[11]])
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
// 1 = NEGOTIATE (first), 3 = AUTHENTICATE (second). 2 is server-only.
|
||||||
|
is_first_round = msg_type == 1;
|
||||||
|
is_raw_ntlmssp = true;
|
||||||
|
inner_token = blob.to_vec();
|
||||||
|
} else if blob[0] == 0x60 {
|
||||||
|
// GSS-API outer wrapper — NegTokenInit.
|
||||||
|
let init = match decode_init_token(&blob) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(error = %e, "SPNEGO init decode failed");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !init.mech_types.iter().any(|m| m == OID_NTLMSSP) {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED);
|
||||||
|
}
|
||||||
|
inner_token = init.mech_token.unwrap_or_default();
|
||||||
|
is_first_round = true;
|
||||||
|
is_raw_ntlmssp = false;
|
||||||
|
} else {
|
||||||
|
// NegTokenResp follow-up.
|
||||||
|
let resp = match decode_resp_token(&blob) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(error = %e, "SPNEGO resp decode failed");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
inner_token = resp.response_token.unwrap_or_default();
|
||||||
|
is_first_round = false;
|
||||||
|
is_raw_ntlmssp = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_first_round {
|
||||||
|
// Allocate a fresh session id and start the NTLM state machine.
|
||||||
|
let new_sid = conn.alloc_session_id();
|
||||||
|
let mut server_challenge = [0u8; 8];
|
||||||
|
fill_random(&mut server_challenge);
|
||||||
|
let netbios = server.config.netbios_name.clone();
|
||||||
|
let mut acceptor = NtlmServer::new(
|
||||||
|
server_challenge,
|
||||||
|
NtlmTargetInfo::new(netbios.clone(), netbios.clone(), netbios, "", ""),
|
||||||
|
now_filetime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 1: parse client NEGOTIATE.
|
||||||
|
if let Err(e) = acceptor.step1_negotiate(&inner_token) {
|
||||||
|
warn!(error = %e, "NTLM step1 failed");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
|
||||||
|
}
|
||||||
|
let challenge_blob = acceptor.challenge();
|
||||||
|
// Reply form mirrors the request: raw NTLMSSP if the client skipped
|
||||||
|
// SPNEGO, else SPNEGO-wrapped.
|
||||||
|
let outbound = if is_raw_ntlmssp {
|
||||||
|
challenge_blob
|
||||||
|
} else {
|
||||||
|
encode_resp_token(
|
||||||
|
NegState::AcceptIncomplete,
|
||||||
|
Some(OID_NTLMSSP),
|
||||||
|
Some(&challenge_blob),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stash the acceptor for the next round; remember the form so the
|
||||||
|
// success response can match.
|
||||||
|
{
|
||||||
|
let mut pa = conn.pending_auths.write().await;
|
||||||
|
pa.insert(
|
||||||
|
new_sid,
|
||||||
|
Arc::new(std::sync::Mutex::new((acceptor, is_raw_ntlmssp))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body_out =
|
||||||
|
build_session_setup_response(ntstatus::STATUS_MORE_PROCESSING_REQUIRED, &outbound, 0);
|
||||||
|
return HandlerResponse {
|
||||||
|
body: body_out,
|
||||||
|
status: ntstatus::STATUS_MORE_PROCESSING_REQUIRED,
|
||||||
|
override_tree_id: None,
|
||||||
|
override_session_id: Some(new_sid),
|
||||||
|
skip_signing: true, // no key yet
|
||||||
|
take_preauth_snapshot_for_session: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow-up round: look up pending acceptor by session id from header.
|
||||||
|
let sid = hdr.session_id;
|
||||||
|
if sid == 0 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
let acceptor_arc = {
|
||||||
|
let mut pa = conn.pending_auths.write().await;
|
||||||
|
pa.remove(&sid)
|
||||||
|
};
|
||||||
|
let acceptor_arc = match acceptor_arc {
|
||||||
|
Some(a) => a,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_USER_SESSION_DELETED),
|
||||||
|
};
|
||||||
|
let users = server.users.table.read().await.clone();
|
||||||
|
let (outcome, raw_form) = {
|
||||||
|
let pair = acceptor_arc
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
|
let (acceptor, raw_form) = (&pair.0, pair.1);
|
||||||
|
let lookup = |u: &str, _d: &str| -> Option<UserCreds> { users.get(u).cloned() };
|
||||||
|
let outcome = match acceptor.authenticate(&inner_token, lookup) {
|
||||||
|
Ok(o) => o,
|
||||||
|
Err(e) => {
|
||||||
|
info!(error = %e, "NTLM authenticate failed");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(outcome, raw_form)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Anonymous gating.
|
||||||
|
if matches!(outcome.identity, Identity::Anonymous) && !server.anonymous_allowed().await {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_base_key = outcome.session_key;
|
||||||
|
let dialect = *conn.dialect.read().await;
|
||||||
|
let signing_key = match dialect {
|
||||||
|
Some(Dialect::Smb311) => [0u8; 16],
|
||||||
|
Some(_) => signing_key_30(&session_base_key),
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
|
||||||
|
let session_flags = if matches!(outcome.identity, Identity::Anonymous) {
|
||||||
|
SessionSetupResponse::FLAG_IS_GUEST
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let signing_required = false;
|
||||||
|
|
||||||
|
let session = Session::new(
|
||||||
|
sid,
|
||||||
|
outcome.identity.clone(),
|
||||||
|
session_base_key,
|
||||||
|
signing_key,
|
||||||
|
signing_required,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let session_arc = Arc::new(tokio::sync::RwLock::new(session));
|
||||||
|
{
|
||||||
|
let mut sessions = conn.sessions.write().await;
|
||||||
|
sessions.insert(sid, session_arc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty buffer for raw NTLMSSP path; SPNEGO accept-completed for SPNEGO.
|
||||||
|
let success_buf: Vec<u8> = if raw_form {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
empty_completed()
|
||||||
|
};
|
||||||
|
let body_out =
|
||||||
|
build_session_setup_response(ntstatus::STATUS_SUCCESS, &success_buf, session_flags);
|
||||||
|
|
||||||
|
let take_snapshot = if dialect == Some(Dialect::Smb311) {
|
||||||
|
Some(sid)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
info!(?outcome.identity, "session established");
|
||||||
|
|
||||||
|
HandlerResponse {
|
||||||
|
body: body_out,
|
||||||
|
status: ntstatus::STATUS_SUCCESS,
|
||||||
|
override_tree_id: None,
|
||||||
|
override_session_id: Some(sid),
|
||||||
|
// Anonymous responses are not signed (no key). Signed responses for
|
||||||
|
// authenticated sessions get signed by the dispatcher's normal path.
|
||||||
|
skip_signing: matches!(outcome.identity, Identity::Anonymous),
|
||||||
|
take_preauth_snapshot_for_session: take_snapshot,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_session_setup_response(_status: u32, spnego_blob: &[u8], session_flags: u16) -> Vec<u8> {
|
||||||
|
let resp = SessionSetupResponse {
|
||||||
|
structure_size: 9,
|
||||||
|
session_flags,
|
||||||
|
security_buffer_offset: 64 + 8, // SMB2 header + fixed prefix
|
||||||
|
security_buffer_length: spnego_blob.len() as u16,
|
||||||
|
security_buffer: spnego_blob.to_vec(),
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf)
|
||||||
|
.expect("SESSION_SETUP response encodes");
|
||||||
|
debug!(len = buf.len(), "SESSION_SETUP response built");
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_completed() -> Vec<u8> {
|
||||||
|
encode_resp_token(NegState::AcceptCompleted, None, None, None)
|
||||||
|
}
|
||||||
143
vendor/smb-server/src/handlers/set_info.rs
vendored
Normal file
143
vendor/smb-server/src/handlers/set_info.rs
vendored
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
//! SET_INFO handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{InfoType, SetInfoRequest, SetInfoResponse};
|
||||||
|
|
||||||
|
use crate::backend::FileTimes;
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::{lookup_open, lookup_session_tree};
|
||||||
|
use crate::info_class as ic;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::path::SmbPath;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
use crate::utils::utf16le_to_units;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match SetInfoRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
let info_type = match InfoType::from_u8(req.info_type) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS),
|
||||||
|
};
|
||||||
|
if !matches!(info_type, InfoType::File) {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(s) => return HandlerResponse::err(s),
|
||||||
|
};
|
||||||
|
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
|
||||||
|
Some(o) => o,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
};
|
||||||
|
|
||||||
|
let class = req.file_information_class;
|
||||||
|
let buffer = req.buffer;
|
||||||
|
let backend = {
|
||||||
|
let tree = tree_arc.read().await;
|
||||||
|
tree.share.backend.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match class {
|
||||||
|
ic::FILE_BASIC_INFORMATION => {
|
||||||
|
if buffer.len() < 36 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
|
||||||
|
}
|
||||||
|
let creation = u64::from_le_bytes(buffer[0..8].try_into().unwrap());
|
||||||
|
let access = u64::from_le_bytes(buffer[8..16].try_into().unwrap());
|
||||||
|
let write = u64::from_le_bytes(buffer[16..24].try_into().unwrap());
|
||||||
|
let change = u64::from_le_bytes(buffer[24..32].try_into().unwrap());
|
||||||
|
// 0 means "do not change", -1 (u64::MAX) means "do not change" too per spec.
|
||||||
|
let to_some = |v: u64| {
|
||||||
|
if v == 0 || v == u64::MAX {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(v)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let times = FileTimes {
|
||||||
|
creation_time: to_some(creation),
|
||||||
|
last_access_time: to_some(access),
|
||||||
|
last_write_time: to_some(write),
|
||||||
|
change_time: to_some(change),
|
||||||
|
};
|
||||||
|
let open = open_arc.read().await;
|
||||||
|
match open.handle.as_ref() {
|
||||||
|
Some(h) => h.set_times(times).await,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ic::FILE_END_OF_FILE_INFORMATION => {
|
||||||
|
if buffer.len() < 8 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
|
||||||
|
}
|
||||||
|
let new_len = u64::from_le_bytes(buffer[0..8].try_into().unwrap());
|
||||||
|
let open = open_arc.read().await;
|
||||||
|
match open.handle.as_ref() {
|
||||||
|
Some(h) => h.truncate(new_len).await,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ic::FILE_DISPOSITION_INFORMATION => {
|
||||||
|
if buffer.is_empty() {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
|
||||||
|
}
|
||||||
|
let mut open = open_arc.write().await;
|
||||||
|
open.delete_on_close = buffer[0] != 0;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ic::FILE_RENAME_INFORMATION => {
|
||||||
|
// FILE_RENAME_INFORMATION layout (MS-FSCC §2.4.37):
|
||||||
|
// ReplaceIfExists (1) | Reserved (7) | RootDirectory (8) | FileNameLength (4) | FileName...
|
||||||
|
if buffer.len() < 20 {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
|
||||||
|
}
|
||||||
|
let name_len = u32::from_le_bytes(buffer[16..20].try_into().unwrap()) as usize;
|
||||||
|
if buffer.len() < 20 + name_len {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
|
||||||
|
}
|
||||||
|
let name_bytes = &buffer[20..20 + name_len];
|
||||||
|
let units = match utf16le_to_units(name_bytes) {
|
||||||
|
Some(u) => u,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
|
||||||
|
};
|
||||||
|
let new_path = match SmbPath::from_utf16(&units) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
|
||||||
|
};
|
||||||
|
let from = open_arc.read().await.last_path.clone();
|
||||||
|
match backend.rename(&from, &new_path).await {
|
||||||
|
Ok(()) => {
|
||||||
|
open_arc.write().await.last_path = new_path;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ic::FILE_ALLOCATION_INFORMATION => {
|
||||||
|
// We don't preallocate; respond OK.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
return HandlerResponse::err(e.to_nt_status());
|
||||||
|
}
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
SetInfoResponse::default()
|
||||||
|
.write_to(&mut buf)
|
||||||
|
.expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
46
vendor/smb-server/src/handlers/shared.rs
vendored
Normal file
46
vendor/smb-server/src/handlers/shared.rs
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//! Internal helpers shared across handlers — tree/open lookup, etc.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::FileId;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::conn::state::{Connection, Open, Session, TreeConnect};
|
||||||
|
use crate::ntstatus;
|
||||||
|
|
||||||
|
/// Look up the session and tree referenced by `hdr`, returning the tree
|
||||||
|
/// inside the session. Returns the appropriate NTSTATUS on miss.
|
||||||
|
pub async fn lookup_session_tree(
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
) -> Result<Arc<RwLock<TreeConnect>>, u32> {
|
||||||
|
let tid = hdr.tree_id().ok_or(ntstatus::STATUS_INVALID_PARAMETER)?;
|
||||||
|
let sess_arc = lookup_session(conn, hdr.session_id).await?;
|
||||||
|
let sess = sess_arc.read().await;
|
||||||
|
let trees = sess.trees.read().await;
|
||||||
|
trees
|
||||||
|
.get(&tid)
|
||||||
|
.cloned()
|
||||||
|
.ok_or(ntstatus::STATUS_NETWORK_NAME_DELETED)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lookup_session(conn: &Arc<Connection>, sid: u64) -> Result<Arc<RwLock<Session>>, u32> {
|
||||||
|
if sid == 0 {
|
||||||
|
return Err(ntstatus::STATUS_USER_SESSION_DELETED);
|
||||||
|
}
|
||||||
|
let sessions = conn.sessions.read().await;
|
||||||
|
sessions
|
||||||
|
.get(&sid)
|
||||||
|
.cloned()
|
||||||
|
.ok_or(ntstatus::STATUS_USER_SESSION_DELETED)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lookup_open(
|
||||||
|
tree: &Arc<RwLock<TreeConnect>>,
|
||||||
|
file_id: FileId,
|
||||||
|
) -> Option<Arc<RwLock<Open>>> {
|
||||||
|
let tree = tree.read().await;
|
||||||
|
let opens = tree.opens.read().await;
|
||||||
|
opens.get(&file_id).cloned()
|
||||||
|
}
|
||||||
140
vendor/smb-server/src/handlers/tree_connect.rs
vendored
Normal file
140
vendor/smb-server/src/handlers/tree_connect.rs
vendored
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
//! TREE_CONNECT handler — share lookup + authorization.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::auth::ntlm::Identity;
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{TreeConnectRequest, TreeConnectResponse};
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::builder::Access;
|
||||||
|
use crate::conn::state::{Connection, TreeConnect};
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::lookup_session;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::{ServerState, ShareMode};
|
||||||
|
|
||||||
|
const SHARE_TYPE_DISK: u8 = 0x01;
|
||||||
|
const SHARE_TYPE_PIPE: u8 = 0x02;
|
||||||
|
|
||||||
|
const FILE_GENERIC_READ: u32 = 0x0012_0089;
|
||||||
|
const FILE_GENERIC_EXECUTE: u32 = 0x0012_00A0;
|
||||||
|
const FILE_ALL_ACCESS: u32 = 0x001F_01FF;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match TreeConnectRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
let path = req.path_str().unwrap_or_default();
|
||||||
|
tracing::debug!(%path, "tree connect path");
|
||||||
|
let share_name = match extract_share_name(&path) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
tracing::warn!(%path, "tree connect: empty share name");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_BAD_NETWORK_NAME);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tracing::debug!(%share_name, "tree connect lookup");
|
||||||
|
let sess_arc = match lookup_session(conn, hdr.session_id).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(s) => return HandlerResponse::err(s),
|
||||||
|
};
|
||||||
|
let sess = sess_arc.read().await;
|
||||||
|
let identity = sess.identity.clone();
|
||||||
|
drop(sess);
|
||||||
|
|
||||||
|
// IPC$: synthetic share. Accept at TREE_CONNECT (Windows always probes
|
||||||
|
// it before mounting an actual share); downstream CREATE/IOCTL on it
|
||||||
|
// return NotSupported via the no-op backend.
|
||||||
|
let share = if share_name.eq_ignore_ascii_case("IPC$") {
|
||||||
|
crate::server::ShareBindings::ipc()
|
||||||
|
} else {
|
||||||
|
match server.find_share(&share_name).await {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_BAD_NETWORK_NAME),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Authorize.
|
||||||
|
let acl = share.acl.read().await;
|
||||||
|
let granted = match authorize(&acl.mode, &acl.users, &identity) {
|
||||||
|
Some(a) => a,
|
||||||
|
None => {
|
||||||
|
warn!(?identity, share = %share.name, "TREE_CONNECT denied");
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
drop(acl);
|
||||||
|
// Backend cap.
|
||||||
|
let granted = if share.backend.capabilities().is_read_only {
|
||||||
|
granted.clamp_to(Access::Read)
|
||||||
|
} else {
|
||||||
|
granted
|
||||||
|
};
|
||||||
|
|
||||||
|
let tree_id = sess_arc.read().await.alloc_tree_id();
|
||||||
|
let tc = Arc::new(tokio::sync::RwLock::new(TreeConnect::new(
|
||||||
|
tree_id,
|
||||||
|
share.clone(),
|
||||||
|
granted,
|
||||||
|
)));
|
||||||
|
{
|
||||||
|
let sess = sess_arc.read().await;
|
||||||
|
let mut trees = sess.trees.write().await;
|
||||||
|
trees.insert(tree_id, tc);
|
||||||
|
}
|
||||||
|
|
||||||
|
let maximal_access = match granted {
|
||||||
|
Access::Read => FILE_GENERIC_READ | FILE_GENERIC_EXECUTE,
|
||||||
|
Access::ReadWrite => FILE_ALL_ACCESS,
|
||||||
|
};
|
||||||
|
let resp = TreeConnectResponse {
|
||||||
|
structure_size: 16,
|
||||||
|
share_type: if share.is_ipc {
|
||||||
|
SHARE_TYPE_PIPE
|
||||||
|
} else {
|
||||||
|
SHARE_TYPE_DISK
|
||||||
|
},
|
||||||
|
reserved: 0,
|
||||||
|
share_flags: 0,
|
||||||
|
capabilities: 0,
|
||||||
|
maximal_access,
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).expect("encode");
|
||||||
|
info!(tree_id, share = %share.name, ?granted, "tree connect");
|
||||||
|
let mut hr = HandlerResponse::ok(buf);
|
||||||
|
hr.override_tree_id = Some(tree_id);
|
||||||
|
hr
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_share_name(unc: &str) -> Option<String> {
|
||||||
|
// \\server\share or \\server\share\
|
||||||
|
let trimmed = unc.trim_end_matches(['\\', '/']);
|
||||||
|
let parts: Vec<&str> = trimmed
|
||||||
|
.split(['\\', '/'])
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
parts.last().map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorize(
|
||||||
|
mode: &ShareMode,
|
||||||
|
users: &std::collections::HashMap<String, Access>,
|
||||||
|
identity: &Identity,
|
||||||
|
) -> Option<Access> {
|
||||||
|
match mode {
|
||||||
|
ShareMode::Public => Some(Access::ReadWrite),
|
||||||
|
ShareMode::PublicReadOnly => Some(Access::Read),
|
||||||
|
ShareMode::AuthenticatedOnly => match identity {
|
||||||
|
Identity::Anonymous => None,
|
||||||
|
Identity::User { user, .. } => users.get(user).copied(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
36
vendor/smb-server/src/handlers/tree_disconnect.rs
vendored
Normal file
36
vendor/smb-server/src/handlers/tree_disconnect.rs
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//! TREE_DISCONNECT handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::TreeDisconnectResponse;
|
||||||
|
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::lookup_session;
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
_body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let tid = match hdr.tree_id() {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
|
||||||
|
if lookup_session(conn, hdr.session_id).await.is_err() {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_USER_SESSION_DELETED);
|
||||||
|
}
|
||||||
|
if !conn.close_tree(hdr.session_id, tid).await {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_NETWORK_NAME_DELETED);
|
||||||
|
}
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
TreeDisconnectResponse::default()
|
||||||
|
.write_to(&mut buf)
|
||||||
|
.expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
60
vendor/smb-server/src/handlers/write.rs
vendored
Normal file
60
vendor/smb-server/src/handlers/write.rs
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
//! WRITE handler.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::proto::header::Smb2Header;
|
||||||
|
use crate::proto::messages::{WriteRequest, WriteResponse};
|
||||||
|
|
||||||
|
use crate::builder::Access;
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::dispatch::HandlerResponse;
|
||||||
|
use crate::handlers::shared::{lookup_open, lookup_session_tree};
|
||||||
|
use crate::ntstatus;
|
||||||
|
use crate::server::ServerState;
|
||||||
|
|
||||||
|
pub async fn handle(
|
||||||
|
_server: &Arc<ServerState>,
|
||||||
|
conn: &Arc<Connection>,
|
||||||
|
hdr: &Smb2Header,
|
||||||
|
body: &[u8],
|
||||||
|
) -> HandlerResponse {
|
||||||
|
let req = match WriteRequest::parse(body) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
||||||
|
};
|
||||||
|
let max_write = *conn.max_write_size.read().await;
|
||||||
|
if req.length > max_write {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(s) => return HandlerResponse::err(s),
|
||||||
|
};
|
||||||
|
let granted = {
|
||||||
|
let tree = tree_arc.read().await;
|
||||||
|
tree.granted_access
|
||||||
|
};
|
||||||
|
if !matches!(granted, Access::ReadWrite) {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
|
||||||
|
Some(o) => o,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
};
|
||||||
|
let result = {
|
||||||
|
let open = open_arc.read().await;
|
||||||
|
match open.handle.as_ref() {
|
||||||
|
Some(h) => h.write_owned(req.offset, req.data).await,
|
||||||
|
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let count = match result {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => return HandlerResponse::err(e.to_nt_status()),
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
WriteResponse::new(count)
|
||||||
|
.write_to(&mut buf)
|
||||||
|
.expect("encode");
|
||||||
|
HandlerResponse::ok(buf)
|
||||||
|
}
|
||||||
470
vendor/smb-server/src/info_class.rs
vendored
Normal file
470
vendor/smb-server/src/info_class.rs
vendored
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
//! File / FileSystem / Security info-class encoders used by QUERY_INFO,
|
||||||
|
//! SET_INFO, and QUERY_DIRECTORY.
|
||||||
|
//!
|
||||||
|
//! These are byte-for-byte wire encodings per MS-FSCC §2.4 (file info) /
|
||||||
|
//! §2.5 (filesystem info) / MS-DTYP §2.4 (security descriptor).
|
||||||
|
|
||||||
|
use crate::backend::{DirEntry, FileInfo};
|
||||||
|
use crate::utils::utf16le;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// File info classes (MS-FSCC §2.4)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub const FILE_DIRECTORY_INFORMATION: u8 = 0x01;
|
||||||
|
pub const FILE_FULL_DIRECTORY_INFORMATION: u8 = 0x02;
|
||||||
|
pub const FILE_BOTH_DIRECTORY_INFORMATION: u8 = 0x03;
|
||||||
|
pub const FILE_BASIC_INFORMATION: u8 = 0x04;
|
||||||
|
pub const FILE_STANDARD_INFORMATION: u8 = 0x05;
|
||||||
|
pub const FILE_INTERNAL_INFORMATION: u8 = 0x06;
|
||||||
|
pub const FILE_EA_INFORMATION: u8 = 0x07;
|
||||||
|
pub const FILE_ACCESS_INFORMATION: u8 = 0x08;
|
||||||
|
pub const FILE_NAME_INFORMATION: u8 = 0x09;
|
||||||
|
pub const FILE_NAMES_INFORMATION: u8 = 0x0C;
|
||||||
|
pub const FILE_POSITION_INFORMATION: u8 = 0x0E;
|
||||||
|
pub const FILE_FULL_EA_INFORMATION: u8 = 0x0F;
|
||||||
|
pub const FILE_MODE_INFORMATION: u8 = 0x10;
|
||||||
|
pub const FILE_ALIGNMENT_INFORMATION: u8 = 0x11;
|
||||||
|
pub const FILE_ALL_INFORMATION: u8 = 0x12;
|
||||||
|
pub const FILE_ALLOCATION_INFORMATION: u8 = 0x13;
|
||||||
|
pub const FILE_END_OF_FILE_INFORMATION: u8 = 0x14;
|
||||||
|
pub const FILE_STREAM_INFORMATION: u8 = 0x16;
|
||||||
|
pub const FILE_DISPOSITION_INFORMATION: u8 = 0x0D;
|
||||||
|
pub const FILE_RENAME_INFORMATION: u8 = 0x0A;
|
||||||
|
pub const FILE_NETWORK_OPEN_INFORMATION: u8 = 0x22;
|
||||||
|
pub const FILE_ID_BOTH_DIRECTORY_INFORMATION: u8 = 0x25;
|
||||||
|
pub const FILE_ID_FULL_DIRECTORY_INFORMATION: u8 = 0x26;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileBasicInformation (MS-FSCC §2.4.7) — 40 bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_basic_information(info: &FileInfo) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(40);
|
||||||
|
out.extend_from_slice(&info.creation_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.last_access_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.last_write_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.change_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.attributes().to_le_bytes());
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // Reserved
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileStandardInformation (MS-FSCC §2.4.41) — 24 bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_standard_information(info: &FileInfo) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(24);
|
||||||
|
out.extend_from_slice(&info.allocation_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.end_of_file.to_le_bytes());
|
||||||
|
out.extend_from_slice(&1u32.to_le_bytes()); // NumberOfLinks = 1
|
||||||
|
out.push(0); // DeletePending
|
||||||
|
out.push(if info.is_directory { 1 } else { 0 }); // Directory
|
||||||
|
out.extend_from_slice(&0u16.to_le_bytes()); // Reserved
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileInternalInformation (MS-FSCC §2.4.20) — 8 bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_internal_information(file_index: u64) -> Vec<u8> {
|
||||||
|
file_index.to_le_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileEaInformation (MS-FSCC §2.4.12) — 4 bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_ea_information() -> Vec<u8> {
|
||||||
|
0u32.to_le_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileAccessInformation (MS-FSCC §2.4.1) — 4 bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_access_information(access_mask: u32) -> Vec<u8> {
|
||||||
|
access_mask.to_le_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FilePositionInformation (MS-FSCC §2.4.32) — 8 bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_position_information() -> Vec<u8> {
|
||||||
|
0u64.to_le_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileModeInformation (MS-FSCC §2.4.24) — 4 bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_mode_information(mode: u32) -> Vec<u8> {
|
||||||
|
mode.to_le_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileAlignmentInformation (MS-FSCC §2.4.3) — 4 bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_alignment_information() -> Vec<u8> {
|
||||||
|
// FILE_BYTE_ALIGNMENT (0) — no alignment requirement.
|
||||||
|
0u32.to_le_bytes().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileNameInformation (MS-FSCC §2.4.27) — 4 bytes + UTF-16LE name
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_name_information(name: &str) -> Vec<u8> {
|
||||||
|
let n = utf16le(name);
|
||||||
|
let mut out = Vec::with_capacity(4 + n.len());
|
||||||
|
out.extend_from_slice(&(n.len() as u32).to_le_bytes());
|
||||||
|
out.extend_from_slice(&n);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileAllInformation (MS-FSCC §2.4.2) — concatenation of basic, standard,
|
||||||
|
// internal, EA, access, position, mode, alignment, name.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_all_information(info: &FileInfo, file_index: u64, access_mask: u32) -> Vec<u8> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(&encode_file_basic_information(info));
|
||||||
|
out.extend_from_slice(&encode_file_standard_information(info));
|
||||||
|
out.extend_from_slice(&encode_file_internal_information(file_index));
|
||||||
|
out.extend_from_slice(&encode_file_ea_information());
|
||||||
|
out.extend_from_slice(&encode_file_access_information(access_mask));
|
||||||
|
out.extend_from_slice(&encode_file_position_information());
|
||||||
|
out.extend_from_slice(&encode_file_mode_information(0));
|
||||||
|
out.extend_from_slice(&encode_file_alignment_information());
|
||||||
|
out.extend_from_slice(&encode_file_name_information(&info.name));
|
||||||
|
// Linux cifs checks FileAllInformation against its struct with
|
||||||
|
// FileName[1], so the empty-name root case must still be at least 101
|
||||||
|
// bytes.
|
||||||
|
if out.len() < 101 {
|
||||||
|
out.push(0);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileNetworkOpenInformation (MS-FSCC §2.4.30) — 56 bytes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_network_open_information(info: &FileInfo) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(56);
|
||||||
|
out.extend_from_slice(&info.creation_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.last_access_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.last_write_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.change_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.allocation_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.end_of_file.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.attributes().to_le_bytes());
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // Reserved
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FileStreamInformation (MS-FSCC §2.4.43) — for non-directories, one default
|
||||||
|
// stream entry (`::$DATA`); for directories, empty buffer.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub fn encode_file_stream_information(info: &FileInfo) -> Vec<u8> {
|
||||||
|
if info.is_directory {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let stream_name = utf16le("::$DATA");
|
||||||
|
let stream_name_len = stream_name.len() as u32;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // NextEntryOffset = 0
|
||||||
|
out.extend_from_slice(&stream_name_len.to_le_bytes()); // StreamNameLength
|
||||||
|
out.extend_from_slice(&info.end_of_file.to_le_bytes()); // StreamSize
|
||||||
|
out.extend_from_slice(&info.allocation_size.to_le_bytes()); // StreamAllocationSize
|
||||||
|
out.extend_from_slice(&stream_name);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// FS info classes (MS-FSCC §2.5)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
pub const FS_VOLUME_INFORMATION: u8 = 0x01;
|
||||||
|
pub const FS_SIZE_INFORMATION: u8 = 0x03;
|
||||||
|
pub const FS_DEVICE_INFORMATION: u8 = 0x04;
|
||||||
|
pub const FS_ATTRIBUTE_INFORMATION: u8 = 0x05;
|
||||||
|
pub const FS_FULL_SIZE_INFORMATION: u8 = 0x07;
|
||||||
|
|
||||||
|
/// FileFsVolumeInformation (MS-FSCC §2.5.9). Volume creation time, serial,
|
||||||
|
/// label.
|
||||||
|
pub fn encode_fs_volume_information(creation_time: u64, serial: u32, label: &str) -> Vec<u8> {
|
||||||
|
let label_u16 = utf16le(label);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(&creation_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&serial.to_le_bytes());
|
||||||
|
out.extend_from_slice(&(label_u16.len() as u32).to_le_bytes());
|
||||||
|
out.push(0); // SupportsObjects
|
||||||
|
out.push(0); // Reserved
|
||||||
|
out.extend_from_slice(&label_u16);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FileFsSizeInformation (MS-FSCC §2.5.7) — 24 bytes.
|
||||||
|
pub fn encode_fs_size_information(
|
||||||
|
total_alloc_units: u64,
|
||||||
|
avail_alloc_units: u64,
|
||||||
|
sectors_per_unit: u32,
|
||||||
|
bytes_per_sector: u32,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(24);
|
||||||
|
out.extend_from_slice(&total_alloc_units.to_le_bytes());
|
||||||
|
out.extend_from_slice(&avail_alloc_units.to_le_bytes());
|
||||||
|
out.extend_from_slice(§ors_per_unit.to_le_bytes());
|
||||||
|
out.extend_from_slice(&bytes_per_sector.to_le_bytes());
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FileFsDeviceInformation (MS-FSCC §2.5.10) — 8 bytes.
|
||||||
|
pub fn encode_fs_device_information(device_type: u32, characteristics: u32) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(8);
|
||||||
|
out.extend_from_slice(&device_type.to_le_bytes());
|
||||||
|
out.extend_from_slice(&characteristics.to_le_bytes());
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FileFsAttributeInformation (MS-FSCC §2.5.1) — variable.
|
||||||
|
pub fn encode_fs_attribute_information(
|
||||||
|
attributes: u32,
|
||||||
|
max_component_len: u32,
|
||||||
|
fs_name: &str,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let name_u16 = utf16le(fs_name);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(&attributes.to_le_bytes());
|
||||||
|
out.extend_from_slice(&max_component_len.to_le_bytes());
|
||||||
|
out.extend_from_slice(&(name_u16.len() as u32).to_le_bytes());
|
||||||
|
out.extend_from_slice(&name_u16);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FileFsFullSizeInformation (MS-FSCC §2.5.4) — 32 bytes.
|
||||||
|
pub fn encode_fs_full_size_information(
|
||||||
|
total_alloc_units: u64,
|
||||||
|
caller_avail_alloc_units: u64,
|
||||||
|
actual_avail_alloc_units: u64,
|
||||||
|
sectors_per_unit: u32,
|
||||||
|
bytes_per_sector: u32,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(32);
|
||||||
|
out.extend_from_slice(&total_alloc_units.to_le_bytes());
|
||||||
|
out.extend_from_slice(&caller_avail_alloc_units.to_le_bytes());
|
||||||
|
out.extend_from_slice(&actual_avail_alloc_units.to_le_bytes());
|
||||||
|
out.extend_from_slice(§ors_per_unit.to_le_bytes());
|
||||||
|
out.extend_from_slice(&bytes_per_sector.to_le_bytes());
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Minimal SECURITY_DESCRIPTOR with owner=Everyone, DACL=Everyone allowed.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Build a minimal absolute-form SECURITY_DESCRIPTOR per MS-DTYP §2.4.6.
|
||||||
|
///
|
||||||
|
/// Owner = Everyone (S-1-1-0). No group. DACL = single Allow ACE granting
|
||||||
|
/// `0x001F_01FF` (FILE_ALL_ACCESS) to Everyone. Self-relative format so it
|
||||||
|
/// embeds cleanly in the QUERY_INFO buffer.
|
||||||
|
pub fn encode_minimal_security_descriptor() -> Vec<u8> {
|
||||||
|
// SID Everyone (S-1-1-0): 1, 1, [0,0,0,0,0,1], [0,0,0,0]
|
||||||
|
// Total length: 1 (Revision) + 1 (SubAuthorityCount=1) + 6 (Identifier) + 4 (subauth) = 12
|
||||||
|
let everyone: Vec<u8> = vec![
|
||||||
|
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build ACE: AccessAllowedAce
|
||||||
|
// Header: 4 bytes (Type=0, Flags=0, Size)
|
||||||
|
// Mask: 4 bytes
|
||||||
|
// Sid: variable
|
||||||
|
let mut ace = Vec::new();
|
||||||
|
ace.push(0x00); // ACCESS_ALLOWED_ACE_TYPE
|
||||||
|
ace.push(0x00); // AceFlags
|
||||||
|
let ace_size: u16 = (4 + 4 + everyone.len()) as u16;
|
||||||
|
ace.extend_from_slice(&ace_size.to_le_bytes());
|
||||||
|
ace.extend_from_slice(&0x001F_01FFu32.to_le_bytes()); // FILE_ALL_ACCESS
|
||||||
|
ace.extend_from_slice(&everyone);
|
||||||
|
|
||||||
|
// ACL: Revision (1), Sbz1 (1), AclSize (2), AceCount (2), Sbz2 (2), then ACEs.
|
||||||
|
let acl_size: u16 = (8 + ace.len()) as u16;
|
||||||
|
let mut dacl = Vec::new();
|
||||||
|
dacl.push(0x02); // Revision = ACL_REVISION
|
||||||
|
dacl.push(0x00); // Sbz1
|
||||||
|
dacl.extend_from_slice(&acl_size.to_le_bytes());
|
||||||
|
dacl.extend_from_slice(&1u16.to_le_bytes()); // AceCount
|
||||||
|
dacl.extend_from_slice(&0u16.to_le_bytes()); // Sbz2
|
||||||
|
dacl.extend_from_slice(&ace);
|
||||||
|
|
||||||
|
// SECURITY_DESCRIPTOR (self-relative):
|
||||||
|
// Revision (1), Sbz1 (1), Control (2),
|
||||||
|
// OwnerOffset (4), GroupOffset (4), SaclOffset (4), DaclOffset (4)
|
||||||
|
// Then concatenated entities.
|
||||||
|
const SE_DACL_PRESENT: u16 = 0x0004;
|
||||||
|
const SE_SELF_RELATIVE: u16 = 0x8000;
|
||||||
|
let mut sd = Vec::new();
|
||||||
|
sd.push(0x01); // Revision = SECURITY_DESCRIPTOR_REVISION
|
||||||
|
sd.push(0x00); // Sbz1
|
||||||
|
sd.extend_from_slice(&(SE_DACL_PRESENT | SE_SELF_RELATIVE).to_le_bytes());
|
||||||
|
let header_len: u32 = 20;
|
||||||
|
let owner_off = header_len;
|
||||||
|
let group_off = 0u32;
|
||||||
|
let sacl_off = 0u32;
|
||||||
|
let dacl_off = owner_off + everyone.len() as u32;
|
||||||
|
sd.extend_from_slice(&owner_off.to_le_bytes());
|
||||||
|
sd.extend_from_slice(&group_off.to_le_bytes());
|
||||||
|
sd.extend_from_slice(&sacl_off.to_le_bytes());
|
||||||
|
sd.extend_from_slice(&dacl_off.to_le_bytes());
|
||||||
|
sd.extend_from_slice(&everyone);
|
||||||
|
sd.extend_from_slice(&dacl);
|
||||||
|
sd
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Directory information classes (MS-FSCC §2.4.{8,14,17,30,31})
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Encode a single FileBothDirectoryInformation entry. Returns the encoded
|
||||||
|
/// bytes. The caller patches `NextEntryOffset` for chained entries.
|
||||||
|
pub fn encode_dir_entry(class: u8, entry: &DirEntry, file_index: u64) -> Vec<u8> {
|
||||||
|
let info = &entry.info;
|
||||||
|
let name_u16 = utf16le(&info.name);
|
||||||
|
match class {
|
||||||
|
FILE_DIRECTORY_INFORMATION => {
|
||||||
|
// 64 bytes fixed + name
|
||||||
|
let mut out = Vec::new();
|
||||||
|
write_dir_entry_prefix(&mut out, info, file_index, name_u16.len());
|
||||||
|
out.extend_from_slice(&name_u16);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
FILE_FULL_DIRECTORY_INFORMATION => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
write_dir_entry_prefix(&mut out, info, file_index, name_u16.len());
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // EaSize
|
||||||
|
out.extend_from_slice(&name_u16);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
FILE_BOTH_DIRECTORY_INFORMATION => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
write_dir_entry_prefix(&mut out, info, file_index, name_u16.len());
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // EaSize
|
||||||
|
out.push(0); // ShortNameLength
|
||||||
|
out.push(0); // Reserved1
|
||||||
|
// ShortName: 24 bytes (12 UTF-16 chars).
|
||||||
|
out.extend_from_slice(&[0u8; 24]);
|
||||||
|
out.extend_from_slice(&name_u16);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
FILE_ID_BOTH_DIRECTORY_INFORMATION => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
write_dir_entry_prefix(&mut out, info, file_index, name_u16.len());
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // EaSize
|
||||||
|
out.push(0); // ShortNameLength
|
||||||
|
out.push(0); // Reserved1
|
||||||
|
out.extend_from_slice(&[0u8; 24]); // ShortName
|
||||||
|
out.extend_from_slice(&0u16.to_le_bytes()); // Reserved2
|
||||||
|
out.extend_from_slice(&file_index.to_le_bytes()); // FileId
|
||||||
|
out.extend_from_slice(&name_u16);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
FILE_ID_FULL_DIRECTORY_INFORMATION => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
write_dir_entry_prefix(&mut out, info, file_index, name_u16.len());
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // EaSize
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // Reserved
|
||||||
|
out.extend_from_slice(&file_index.to_le_bytes()); // FileId
|
||||||
|
out.extend_from_slice(&name_u16);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
FILE_NAMES_INFORMATION => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
out.extend_from_slice(&(file_index as u32).to_le_bytes());
|
||||||
|
out.extend_from_slice(&(name_u16.len() as u32).to_le_bytes());
|
||||||
|
out.extend_from_slice(&name_u16);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_dir_entry_prefix(out: &mut Vec<u8>, info: &FileInfo, file_index: u64, name_len: usize) {
|
||||||
|
out.extend_from_slice(&0u32.to_le_bytes()); // NextEntryOffset (patched later)
|
||||||
|
out.extend_from_slice(&(file_index as u32).to_le_bytes()); // FileIndex
|
||||||
|
out.extend_from_slice(&info.creation_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.last_access_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.last_write_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.change_time.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.end_of_file.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.allocation_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(&info.attributes().to_le_bytes());
|
||||||
|
out.extend_from_slice(&(name_len as u32).to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Round up `n` to the next multiple of 8.
|
||||||
|
pub fn align8(n: usize) -> usize {
|
||||||
|
(n + 7) & !7
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn fake_info() -> FileInfo {
|
||||||
|
FileInfo {
|
||||||
|
name: "file.txt".to_string(),
|
||||||
|
end_of_file: 100,
|
||||||
|
allocation_size: 100,
|
||||||
|
creation_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_access_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_write_time: 0x01D9_0000_0000_0000,
|
||||||
|
change_time: 0x01D9_0000_0000_0000,
|
||||||
|
is_directory: false,
|
||||||
|
file_index: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_information_is_40_bytes() {
|
||||||
|
let bytes = encode_file_basic_information(&fake_info());
|
||||||
|
assert_eq!(bytes.len(), 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standard_information_is_24_bytes() {
|
||||||
|
let bytes = encode_file_standard_information(&fake_info());
|
||||||
|
assert_eq!(bytes.len(), 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn network_open_information_is_56_bytes() {
|
||||||
|
let bytes = encode_file_network_open_information(&fake_info());
|
||||||
|
assert_eq!(bytes.len(), 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_all_information_empty_name_keeps_linux_minimum_size() {
|
||||||
|
let mut info = fake_info();
|
||||||
|
info.name.clear();
|
||||||
|
let bytes = encode_file_all_information(&info, 1, 0x001F_01FF);
|
||||||
|
assert_eq!(bytes.len(), 101);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn security_descriptor_is_self_relative() {
|
||||||
|
let sd = encode_minimal_security_descriptor();
|
||||||
|
// Revision=1, then Control bits 8000 set => self-relative.
|
||||||
|
assert_eq!(sd[0], 0x01);
|
||||||
|
let control = u16::from_le_bytes([sd[2], sd[3]]);
|
||||||
|
assert!(control & 0x8000 != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
vendor/smb-server/src/lib.rs
vendored
Normal file
52
vendor/smb-server/src/lib.rs
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//! SMB2/3 file-sharing server with pluggable storage backends.
|
||||||
|
//!
|
||||||
|
//! See `docs/superpowers/specs/2026-04-27-rust-smb-server-design.md` for the
|
||||||
|
//! v1 design. The public API is small on purpose:
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use smb_server::{SmbServer, Share, Access, ShareBackend};
|
||||||
|
//! # async fn run<B: ShareBackend>(backend: B) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
//! SmbServer::builder()
|
||||||
|
//! .listen("0.0.0.0:4445".parse()?)
|
||||||
|
//! .user("alice", "password")
|
||||||
|
//! .share(Share::new("home", backend).user("alice", Access::ReadWrite))
|
||||||
|
//! .build()?
|
||||||
|
//! .serve()
|
||||||
|
//! .await?;
|
||||||
|
//! # Ok(()) }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
mod backend;
|
||||||
|
mod builder;
|
||||||
|
pub(crate) mod conn;
|
||||||
|
mod dispatch;
|
||||||
|
mod error;
|
||||||
|
#[cfg(feature = "localfs")]
|
||||||
|
mod fs;
|
||||||
|
mod handlers;
|
||||||
|
pub(crate) mod info_class;
|
||||||
|
pub mod ntstatus;
|
||||||
|
mod path;
|
||||||
|
mod proto;
|
||||||
|
mod server;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use backend::{BackendCapabilities, DirEntry, FileInfo, FileTimes, Handle, OpenIntent, OpenOptions, ShareBackend};
|
||||||
|
pub use error::SmbError;
|
||||||
|
pub use path::SmbPath;
|
||||||
|
pub use builder::{Access, Share};
|
||||||
|
#[cfg(feature = "localfs")]
|
||||||
|
pub use fs::LocalFsBackend;
|
||||||
|
pub use proto::auth::ntlm::Identity;
|
||||||
|
pub use server::{ConfigHandle, ShareMode, ShutdownHandle, SmbServer};
|
||||||
|
|
||||||
|
pub mod wire {
|
||||||
|
pub use crate::proto::header;
|
||||||
|
pub use crate::proto::messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
mod dynamic_config;
|
||||||
|
mod memfs;
|
||||||
|
}
|
||||||
41
vendor/smb-server/src/ntstatus.rs
vendored
Normal file
41
vendor/smb-server/src/ntstatus.rs
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//! NTSTATUS constants used by SMB2 handlers.
|
||||||
|
//!
|
||||||
|
//! Cross-referenced with MS-ERREF §2.3.1 (NTSTATUS Values). Only the codes the
|
||||||
|
//! v1 server actually emits or recognizes live here — kept tight on purpose.
|
||||||
|
|
||||||
|
pub const STATUS_SUCCESS: u32 = 0x0000_0000;
|
||||||
|
pub const STATUS_PENDING: u32 = 0x0000_0103;
|
||||||
|
pub const STATUS_NOTIFY_CLEANUP: u32 = 0x0000_010B;
|
||||||
|
pub const STATUS_NOTIFY_ENUM_DIR: u32 = 0x0000_010C;
|
||||||
|
pub const STATUS_BUFFER_OVERFLOW: u32 = 0x8000_0005;
|
||||||
|
pub const STATUS_NO_MORE_FILES: u32 = 0x8000_0006;
|
||||||
|
|
||||||
|
pub const STATUS_INVALID_HANDLE: u32 = 0xC000_0008;
|
||||||
|
pub const STATUS_INVALID_PARAMETER: u32 = 0xC000_000D;
|
||||||
|
pub const STATUS_NO_SUCH_FILE: u32 = 0xC000_000F;
|
||||||
|
pub const STATUS_OBJECT_NAME_NOT_FOUND: u32 = 0xC000_000F;
|
||||||
|
pub const STATUS_INVALID_DEVICE_REQUEST: u32 = 0xC000_0010;
|
||||||
|
pub const STATUS_END_OF_FILE: u32 = 0xC000_0011;
|
||||||
|
pub const STATUS_MORE_PROCESSING_REQUIRED: u32 = 0xC000_0016;
|
||||||
|
pub const STATUS_ACCESS_DENIED: u32 = 0xC000_0022;
|
||||||
|
pub const STATUS_BUFFER_TOO_SMALL: u32 = 0xC000_0023;
|
||||||
|
pub const STATUS_OBJECT_NAME_INVALID: u32 = 0xC000_0033;
|
||||||
|
pub const STATUS_OBJECT_NAME_COLLISION: u32 = 0xC000_0035;
|
||||||
|
pub const STATUS_OBJECT_PATH_NOT_FOUND: u32 = 0xC000_003A;
|
||||||
|
pub const STATUS_OBJECT_PATH_SYNTAX_BAD: u32 = 0xC000_003B;
|
||||||
|
pub const STATUS_SHARING_VIOLATION: u32 = 0xC000_0043;
|
||||||
|
pub const STATUS_DELETE_PENDING: u32 = 0xC000_0056;
|
||||||
|
pub const STATUS_LOGON_FAILURE: u32 = 0xC000_006D;
|
||||||
|
pub const STATUS_FS_DRIVER_REQUIRED: u32 = 0xC000_019C;
|
||||||
|
pub const STATUS_NOT_SUPPORTED: u32 = 0xC000_00BB;
|
||||||
|
pub const STATUS_FILE_IS_A_DIRECTORY: u32 = 0xC000_00BA;
|
||||||
|
pub const STATUS_NETWORK_NAME_DELETED: u32 = 0xC000_00C9;
|
||||||
|
pub const STATUS_BAD_NETWORK_NAME: u32 = 0xC000_00CC;
|
||||||
|
pub const STATUS_UNEXPECTED_IO_ERROR: u32 = 0xC000_009C;
|
||||||
|
pub const STATUS_DIRECTORY_NOT_EMPTY: u32 = 0xC000_0101;
|
||||||
|
pub const STATUS_NOT_A_DIRECTORY: u32 = 0xC000_0103;
|
||||||
|
pub const STATUS_USER_SESSION_DELETED: u32 = 0xC000_015C;
|
||||||
|
pub const STATUS_INFO_LENGTH_MISMATCH: u32 = 0xC000_0004;
|
||||||
|
pub const STATUS_FILE_CLOSED: u32 = 0xC000_0128;
|
||||||
|
pub const STATUS_INVALID_INFO_CLASS: u32 = 0xC000_0003;
|
||||||
|
pub const STATUS_NO_EAS_ON_FILE: u32 = 0xC000_0052;
|
||||||
280
vendor/smb-server/src/path.rs
vendored
Normal file
280
vendor/smb-server/src/path.rs
vendored
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
//! `SmbPath` — validated, normalized SMB path used between dispatcher and
|
||||||
|
//! backend.
|
||||||
|
//!
|
||||||
|
//! Construction is exclusively from a `&[u16]` (UTF-16LE-decoded) buffer, per
|
||||||
|
//! spec §7. The protocol layer turns wire bytes into `&[u16]`; this module
|
||||||
|
//! turns `&[u16]` into a path that backends can blindly trust.
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::error::{SmbError, SmbResult};
|
||||||
|
|
||||||
|
/// A validated, component-list path. No `..`, no Windows-forbidden chars, no
|
||||||
|
/// alternate streams. Always relative to the share root — the empty path is
|
||||||
|
/// the root.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct SmbPath {
|
||||||
|
components: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmbPath {
|
||||||
|
/// The share root.
|
||||||
|
pub fn root() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct from a UTF-16 code-unit slice (already decoded from UTF-16LE
|
||||||
|
/// wire bytes).
|
||||||
|
pub fn from_utf16(units: &[u16]) -> SmbResult<Self> {
|
||||||
|
// 1. Convert to UTF-8 lossily — but reject if conversion produced any
|
||||||
|
// replacement characters that didn't exist in the input. We test
|
||||||
|
// the round-trip: invalid surrogates are rejected.
|
||||||
|
let s = decode_utf16_strict(units)?;
|
||||||
|
s.parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_components(s: &str) -> SmbResult<Self> {
|
||||||
|
// Strip a leading separator (clients sometimes prefix `\` or `/`).
|
||||||
|
let trimmed = s
|
||||||
|
.strip_prefix('\\')
|
||||||
|
.or_else(|| s.strip_prefix('/'))
|
||||||
|
.unwrap_or(s);
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Ok(Self::root());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Reject forbidden characters anywhere in the path.
|
||||||
|
for ch in trimmed.chars() {
|
||||||
|
if ch == '\0' || ('\u{0001}'..='\u{001F}').contains(&ch) {
|
||||||
|
return Err(SmbError::NameInvalid);
|
||||||
|
}
|
||||||
|
// Allow `\` and `/` as separators, reject the rest of the
|
||||||
|
// Windows-forbidden set anywhere.
|
||||||
|
match ch {
|
||||||
|
'<' | '>' | ':' | '"' | '|' | '?' | '*' => return Err(SmbError::NameInvalid),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Split on `\` or `/`; reject `..` and empty components; skip `.`.
|
||||||
|
let mut components = Vec::new();
|
||||||
|
for raw in trimmed.split(['\\', '/']) {
|
||||||
|
if raw.is_empty() {
|
||||||
|
// Doubled separator like `foo\\bar` — reject.
|
||||||
|
return Err(SmbError::NameInvalid);
|
||||||
|
}
|
||||||
|
if raw == "." {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if raw == ".." {
|
||||||
|
return Err(SmbError::NameInvalid);
|
||||||
|
}
|
||||||
|
// 4. Reject reserved DOS device names.
|
||||||
|
if is_reserved_dos_name(raw) {
|
||||||
|
return Err(SmbError::NameInvalid);
|
||||||
|
}
|
||||||
|
components.push(raw.to_string());
|
||||||
|
}
|
||||||
|
Ok(Self { components })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Path components in order. Empty for the root.
|
||||||
|
pub fn components(&self) -> &[String] {
|
||||||
|
&self.components
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this the share root?
|
||||||
|
pub fn is_root(&self) -> bool {
|
||||||
|
self.components.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the parent path, or `None` if this is the root.
|
||||||
|
pub fn parent(&self) -> Option<SmbPath> {
|
||||||
|
if self.is_root() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut parent = self.components.clone();
|
||||||
|
parent.pop();
|
||||||
|
Some(SmbPath { components: parent })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the last component, if any.
|
||||||
|
pub fn file_name(&self) -> Option<&str> {
|
||||||
|
self.components.last().map(|s| s.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a single, already-validated last component to this path.
|
||||||
|
pub fn join(&self, last: &str) -> SmbResult<SmbPath> {
|
||||||
|
// Run `last` through the same validator (treating it as a single-
|
||||||
|
// component path).
|
||||||
|
let extra = last.parse::<SmbPath>()?;
|
||||||
|
let mut out = self.clone();
|
||||||
|
out.components.extend(extra.components);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render as a backslash-separated string. Empty for root.
|
||||||
|
pub fn display_backslash(&self) -> String {
|
||||||
|
self.components.join("\\")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for SmbPath {
|
||||||
|
type Err = SmbError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::parse_components(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SmbPath {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.is_root() {
|
||||||
|
f.write_str("\\")
|
||||||
|
} else {
|
||||||
|
f.write_str(&self.display_backslash())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_reserved_dos_name(s: &str) -> bool {
|
||||||
|
// Strip extension before checking, e.g. "CON.txt" is also reserved.
|
||||||
|
let stem = match s.rsplit_once('.') {
|
||||||
|
Some((stem, _)) => stem,
|
||||||
|
None => s,
|
||||||
|
};
|
||||||
|
let upper = stem.to_ascii_uppercase();
|
||||||
|
matches!(upper.as_str(), "CON" | "PRN" | "AUX" | "NUL") || matches_com_or_lpt(&upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_com_or_lpt(s: &str) -> bool {
|
||||||
|
if s.len() != 4 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
let prefix = &bytes[..3];
|
||||||
|
let last = bytes[3] as char;
|
||||||
|
if !matches!(last, '1'..='9') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
prefix == b"COM" || prefix == b"LPT"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_utf16_strict(units: &[u16]) -> SmbResult<String> {
|
||||||
|
// Reject unpaired surrogates explicitly. `String::from_utf16` does this
|
||||||
|
// already; we surface its error as NameInvalid.
|
||||||
|
String::from_utf16(units).map_err(|_| SmbError::NameInvalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn utf16(s: &str) -> Vec<u16> {
|
||||||
|
s.encode_utf16().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn root_paths() {
|
||||||
|
assert!("".parse::<SmbPath>().unwrap().is_root());
|
||||||
|
assert!("\\".parse::<SmbPath>().unwrap().is_root());
|
||||||
|
assert!("/".parse::<SmbPath>().unwrap().is_root());
|
||||||
|
assert!(SmbPath::from_utf16(&utf16("")).unwrap().is_root());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_paths_split() {
|
||||||
|
let p = "dir\\sub\\file.txt".parse::<SmbPath>().unwrap();
|
||||||
|
assert_eq!(p.components(), &["dir", "sub", "file.txt"]);
|
||||||
|
assert_eq!(p.display_backslash(), "dir\\sub\\file.txt");
|
||||||
|
assert!(!p.is_root());
|
||||||
|
assert_eq!(p.file_name(), Some("file.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn forward_slash_accepted() {
|
||||||
|
let p = "a/b/c".parse::<SmbPath>().unwrap();
|
||||||
|
assert_eq!(p.components(), &["a", "b", "c"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dot_components_skipped() {
|
||||||
|
let p = "a\\.\\b".parse::<SmbPath>().unwrap();
|
||||||
|
assert_eq!(p.components(), &["a", "b"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parent_returns_one_component_less() {
|
||||||
|
let p = "a\\b\\c".parse::<SmbPath>().unwrap();
|
||||||
|
let parent = p.parent().unwrap();
|
||||||
|
assert_eq!(parent.components(), &["a", "b"]);
|
||||||
|
let grand = parent.parent().unwrap();
|
||||||
|
assert_eq!(grand.components(), &["a"]);
|
||||||
|
let root = grand.parent().unwrap();
|
||||||
|
assert!(root.is_root());
|
||||||
|
assert!(root.parent().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn join_appends_component() {
|
||||||
|
let p = "a".parse::<SmbPath>().unwrap();
|
||||||
|
let q = p.join("b").unwrap();
|
||||||
|
assert_eq!(q.components(), &["a", "b"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_double_dot() {
|
||||||
|
assert!("a\\..\\b".parse::<SmbPath>().is_err());
|
||||||
|
assert!("..".parse::<SmbPath>().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_double_separator() {
|
||||||
|
assert!("a\\\\b".parse::<SmbPath>().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_forbidden_chars() {
|
||||||
|
for bad in ["a<b", "a>b", "a:b", "a\"b", "a|b", "a?b", "a*b"] {
|
||||||
|
assert!(bad.parse::<SmbPath>().is_err(), "{bad}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_control_chars() {
|
||||||
|
let s = format!("a{}b", '\u{0001}');
|
||||||
|
assert!(s.parse::<SmbPath>().is_err());
|
||||||
|
let s = format!("a{}b", '\u{0000}');
|
||||||
|
assert!(s.parse::<SmbPath>().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_reserved_dos_names() {
|
||||||
|
for bad in [
|
||||||
|
"CON", "con", "PRN", "AUX", "NUL", "COM1", "LPT9", "Con.txt", "NUL.dat",
|
||||||
|
] {
|
||||||
|
assert!(bad.parse::<SmbPath>().is_err(), "{bad}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allows_lookalike_names() {
|
||||||
|
// Not reserved.
|
||||||
|
assert!("CON1".parse::<SmbPath>().is_ok());
|
||||||
|
assert!("LPT".parse::<SmbPath>().is_ok());
|
||||||
|
assert!("LPT0".parse::<SmbPath>().is_ok()); // 0 is not in the 1-9 range
|
||||||
|
assert!("NUL_FILE.txt".parse::<SmbPath>().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unpaired_surrogate() {
|
||||||
|
let units: [u16; 2] = [0xD800, 0x0061]; // unpaired high surrogate
|
||||||
|
assert!(SmbPath::from_utf16(&units).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trip_via_utf16() {
|
||||||
|
let p = SmbPath::from_utf16(&utf16("a\\b")).unwrap();
|
||||||
|
assert_eq!(p.components(), &["a", "b"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
vendor/smb-server/src/proto/auth.rs
vendored
Normal file
11
vendor/smb-server/src/proto/auth.rs
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//! NTLMv2 server-side authentication and minimal SPNEGO outer envelope.
|
||||||
|
//!
|
||||||
|
//! See:
|
||||||
|
//! * MS-NLMP — NT LAN Manager (NTLM) Authentication Protocol
|
||||||
|
//! * MS-SPNG — Simple and Protected GSS-API Negotiation Mechanism
|
||||||
|
//!
|
||||||
|
//! v1 implements **only** the NTLM (NTLMSSP) mechanism inside SPNEGO.
|
||||||
|
//! Kerberos is out of scope (revisit in v0.2).
|
||||||
|
|
||||||
|
pub mod ntlm;
|
||||||
|
pub mod spnego;
|
||||||
1053
vendor/smb-server/src/proto/auth/ntlm.rs
vendored
Normal file
1053
vendor/smb-server/src/proto/auth/ntlm.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
524
vendor/smb-server/src/proto/auth/spnego.rs
vendored
Normal file
524
vendor/smb-server/src/proto/auth/spnego.rs
vendored
Normal file
@@ -0,0 +1,524 @@
|
|||||||
|
//! Minimal hand-rolled DER codec for SPNEGO (MS-SPNG / RFC 4178).
|
||||||
|
//!
|
||||||
|
//! v1 advertises **only** the NTLMSSP mechanism. We don't pull in a full
|
||||||
|
//! ASN.1 crate; this is a tiny subset of DER for the few SPNEGO tokens we
|
||||||
|
//! need to encode/decode during SESSION_SETUP.
|
||||||
|
//!
|
||||||
|
//! ASN.1 sketch:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! GSSAPI-Token (RFC 2743) ::= [APPLICATION 0] IMPLICIT SEQUENCE {
|
||||||
|
//! thisMech OBJECT IDENTIFIER, -- SPNEGO 1.3.6.1.5.5.2
|
||||||
|
//! innerContextToken ANY DEFINED BY thisMech
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! NegotiationToken ::= CHOICE {
|
||||||
|
//! negTokenInit [0] NegTokenInit,
|
||||||
|
//! negTokenResp [1] NegTokenResp
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! NegTokenInit ::= SEQUENCE {
|
||||||
|
//! mechTypes [0] MechTypeList,
|
||||||
|
//! reqFlags [1] ContextFlags OPTIONAL,
|
||||||
|
//! mechToken [2] OCTET STRING OPTIONAL,
|
||||||
|
//! mechListMIC [3] OCTET STRING OPTIONAL
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! NegTokenResp ::= SEQUENCE {
|
||||||
|
//! negState [0] ENUMERATED OPTIONAL,
|
||||||
|
//! supportedMech [1] OBJECT IDENTIFIER OPTIONAL,
|
||||||
|
//! responseToken [2] OCTET STRING OPTIONAL,
|
||||||
|
//! mechListMIC [3] OCTET STRING OPTIONAL
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::proto::error::{ProtoError, ProtoResult};
|
||||||
|
|
||||||
|
// --- Universal & well-known tags --------------------------------------------
|
||||||
|
|
||||||
|
const TAG_SEQUENCE: u8 = 0x30; // SEQUENCE OF / SEQUENCE (constructed)
|
||||||
|
const TAG_OBJECT: u8 = 0x06; // OBJECT IDENTIFIER
|
||||||
|
const TAG_OCTET: u8 = 0x04; // OCTET STRING
|
||||||
|
const TAG_ENUMERATED: u8 = 0x0a; // ENUMERATED
|
||||||
|
|
||||||
|
const TAG_APP_0: u8 = 0x60; // [APPLICATION 0] IMPLICIT — GSS-API outer
|
||||||
|
const TAG_CTX_0: u8 = 0xa0;
|
||||||
|
const TAG_CTX_1: u8 = 0xa1;
|
||||||
|
const TAG_CTX_2: u8 = 0xa2;
|
||||||
|
const TAG_CTX_3: u8 = 0xa3;
|
||||||
|
|
||||||
|
// --- OIDs ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// SPNEGO `1.3.6.1.5.5.2` encoded as the *content* of an OBJECT IDENTIFIER
|
||||||
|
/// (i.e. **without** the leading 0x06 tag + length).
|
||||||
|
pub const OID_SPNEGO: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
|
||||||
|
|
||||||
|
/// NTLMSSP `1.3.6.1.4.1.311.2.2.10` encoded as OID *content*.
|
||||||
|
pub const OID_NTLMSSP: &[u8] = &[0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0a];
|
||||||
|
|
||||||
|
// --- NegState --------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Values of the `negState` field in NegTokenResp.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum NegState {
|
||||||
|
AcceptCompleted = 0,
|
||||||
|
AcceptIncomplete = 1,
|
||||||
|
Reject = 2,
|
||||||
|
RequestMic = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NegState {
|
||||||
|
fn from_byte(b: u8) -> ProtoResult<Self> {
|
||||||
|
match b {
|
||||||
|
0 => Ok(NegState::AcceptCompleted),
|
||||||
|
1 => Ok(NegState::AcceptIncomplete),
|
||||||
|
2 => Ok(NegState::Reject),
|
||||||
|
3 => Ok(NegState::RequestMic),
|
||||||
|
_ => Err(ProtoError::Auth("invalid NegState")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- DER length helpers ----------------------------------------------------
|
||||||
|
|
||||||
|
/// Encode a DER length (definite-length form, MS-SPNG always uses definite).
|
||||||
|
fn der_len(n: usize, out: &mut Vec<u8>) {
|
||||||
|
if n < 0x80 {
|
||||||
|
out.push(n as u8);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Long form. Find minimum number of bytes.
|
||||||
|
let mut tmp = [0u8; 8];
|
||||||
|
let mut nb = 0;
|
||||||
|
let mut v = n;
|
||||||
|
while v > 0 {
|
||||||
|
tmp[nb] = (v & 0xff) as u8;
|
||||||
|
v >>= 8;
|
||||||
|
nb += 1;
|
||||||
|
}
|
||||||
|
out.push(0x80 | nb as u8);
|
||||||
|
for i in (0..nb).rev() {
|
||||||
|
out.push(tmp[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a DER length from `buf` starting at `pos`. Returns `(length, next_pos)`.
|
||||||
|
fn read_len(buf: &[u8], pos: usize) -> ProtoResult<(usize, usize)> {
|
||||||
|
if pos >= buf.len() {
|
||||||
|
return Err(ProtoError::Auth("DER length truncated"));
|
||||||
|
}
|
||||||
|
let first = buf[pos];
|
||||||
|
if first < 0x80 {
|
||||||
|
return Ok((first as usize, pos + 1));
|
||||||
|
}
|
||||||
|
let nb = (first & 0x7f) as usize;
|
||||||
|
if nb == 0 || nb > 4 {
|
||||||
|
// Indefinite (nb=0) — never used by SPNEGO.
|
||||||
|
// We cap at 4 bytes (max ~4 GiB), more than enough for tokens.
|
||||||
|
return Err(ProtoError::Auth("DER length form unsupported"));
|
||||||
|
}
|
||||||
|
if pos + 1 + nb > buf.len() {
|
||||||
|
return Err(ProtoError::Auth("DER length truncated"));
|
||||||
|
}
|
||||||
|
let mut v = 0usize;
|
||||||
|
for i in 0..nb {
|
||||||
|
v = (v << 8) | buf[pos + 1 + i] as usize;
|
||||||
|
}
|
||||||
|
Ok((v, pos + 1 + nb))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `(tag, content_slice, next_pos)` at `pos`. Verifies the expected tag.
|
||||||
|
fn read_tlv(buf: &[u8], pos: usize, expected_tag: u8) -> ProtoResult<(&[u8], usize)> {
|
||||||
|
if pos >= buf.len() {
|
||||||
|
return Err(ProtoError::Auth("DER tag truncated"));
|
||||||
|
}
|
||||||
|
if buf[pos] != expected_tag {
|
||||||
|
return Err(ProtoError::Auth("unexpected DER tag"));
|
||||||
|
}
|
||||||
|
let (len, after_len) = read_len(buf, pos + 1)?;
|
||||||
|
let end = after_len + len;
|
||||||
|
if end > buf.len() {
|
||||||
|
return Err(ProtoError::Auth("DER content truncated"));
|
||||||
|
}
|
||||||
|
Ok((&buf[after_len..end], end))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read any TLV (returning its tag plus the content slice & end position).
|
||||||
|
fn read_any_tlv(buf: &[u8], pos: usize) -> ProtoResult<(u8, &[u8], usize)> {
|
||||||
|
if pos >= buf.len() {
|
||||||
|
return Err(ProtoError::Auth("DER tag truncated"));
|
||||||
|
}
|
||||||
|
let tag = buf[pos];
|
||||||
|
let (len, after_len) = read_len(buf, pos + 1)?;
|
||||||
|
let end = after_len + len;
|
||||||
|
if end > buf.len() {
|
||||||
|
return Err(ProtoError::Auth("DER content truncated"));
|
||||||
|
}
|
||||||
|
Ok((tag, &buf[after_len..end], end))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TLV writer helper -----------------------------------------------------
|
||||||
|
|
||||||
|
fn write_tlv(tag: u8, content: &[u8], out: &mut Vec<u8>) {
|
||||||
|
out.push(tag);
|
||||||
|
der_len(content.len(), out);
|
||||||
|
out.extend_from_slice(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Public API ------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Decoded `NegTokenInit` payload — only the bits we care about.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NegTokenInit {
|
||||||
|
/// List of mechanism OIDs (each entry is the OID content bytes, no 0x06 tag).
|
||||||
|
pub mech_types: Vec<Vec<u8>>,
|
||||||
|
/// `mechToken [2]` if present — typically the NTLMSSP NEGOTIATE_MESSAGE bytes.
|
||||||
|
pub mech_token: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decoded `NegTokenResp` payload.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct NegTokenResp {
|
||||||
|
pub neg_state: Option<NegState>,
|
||||||
|
/// `supportedMech [1]` (OID content bytes).
|
||||||
|
pub supported_mech: Option<Vec<u8>>,
|
||||||
|
/// `responseToken [2]` — typically inner NTLMSSP CHALLENGE/AUTHENTICATE bytes.
|
||||||
|
pub response_token: Option<Vec<u8>>,
|
||||||
|
pub mech_list_mic: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode the **initial** SPNEGO blob from the client. This is wrapped in
|
||||||
|
/// the GSS-API outer `[APPLICATION 0]` tag, contains a `thisMech` OID
|
||||||
|
/// (SPNEGO), and a `[0] NegTokenInit`.
|
||||||
|
///
|
||||||
|
/// Returns the parsed `NegTokenInit`.
|
||||||
|
pub fn decode_init_token(buf: &[u8]) -> ProtoResult<NegTokenInit> {
|
||||||
|
// [APPLICATION 0] IMPLICIT SEQUENCE { thisMech OID, NegotiationToken }
|
||||||
|
let (gss_inner, _end) = read_tlv(buf, 0, TAG_APP_0)?;
|
||||||
|
|
||||||
|
// thisMech
|
||||||
|
let (mech, after_mech) = read_tlv(gss_inner, 0, TAG_OBJECT)?;
|
||||||
|
if mech != OID_SPNEGO {
|
||||||
|
return Err(ProtoError::Auth("not an SPNEGO token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// NegotiationToken — choice tagged [0] for init.
|
||||||
|
let (init_inner, _) = read_tlv(gss_inner, after_mech, TAG_CTX_0)?;
|
||||||
|
parse_neg_token_init_body(init_inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_neg_token_init_body(inner: &[u8]) -> ProtoResult<NegTokenInit> {
|
||||||
|
// Inner is a SEQUENCE.
|
||||||
|
let (seq_body, _) = read_tlv(inner, 0, TAG_SEQUENCE)?;
|
||||||
|
let mut pos = 0usize;
|
||||||
|
let mut mech_types: Vec<Vec<u8>> = Vec::new();
|
||||||
|
let mut mech_token: Option<Vec<u8>> = None;
|
||||||
|
|
||||||
|
while pos < seq_body.len() {
|
||||||
|
let (tag, content, next) = read_any_tlv(seq_body, pos)?;
|
||||||
|
match tag {
|
||||||
|
TAG_CTX_0 => {
|
||||||
|
// mechTypes [0] MechTypeList ::= SEQUENCE OF MechType (OID)
|
||||||
|
let (mt_seq, _) = read_tlv(content, 0, TAG_SEQUENCE)?;
|
||||||
|
let mut p = 0usize;
|
||||||
|
while p < mt_seq.len() {
|
||||||
|
let (oid, e) = read_tlv(mt_seq, p, TAG_OBJECT)?;
|
||||||
|
mech_types.push(oid.to_vec());
|
||||||
|
p = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TAG_CTX_1 => {
|
||||||
|
// reqFlags — ignored.
|
||||||
|
}
|
||||||
|
TAG_CTX_2 => {
|
||||||
|
// mechToken [2] OCTET STRING
|
||||||
|
let (oct, _) = read_tlv(content, 0, TAG_OCTET)?;
|
||||||
|
mech_token = Some(oct.to_vec());
|
||||||
|
}
|
||||||
|
TAG_CTX_3 => {
|
||||||
|
// mechListMIC — ignored on init.
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Unknown — skip silently (forward-compat).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(NegTokenInit {
|
||||||
|
mech_types,
|
||||||
|
mech_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a subsequent `NegTokenResp`. These are sent without the GSS-API
|
||||||
|
/// outer wrapper — they begin directly with the `[1]` choice tag.
|
||||||
|
pub fn decode_resp_token(buf: &[u8]) -> ProtoResult<NegTokenResp> {
|
||||||
|
let (resp_inner, _) = read_tlv(buf, 0, TAG_CTX_1)?;
|
||||||
|
let (seq_body, _) = read_tlv(resp_inner, 0, TAG_SEQUENCE)?;
|
||||||
|
let mut pos = 0usize;
|
||||||
|
let mut out = NegTokenResp::default();
|
||||||
|
|
||||||
|
while pos < seq_body.len() {
|
||||||
|
let (tag, content, next) = read_any_tlv(seq_body, pos)?;
|
||||||
|
match tag {
|
||||||
|
TAG_CTX_0 => {
|
||||||
|
let (en, _) = read_tlv(content, 0, TAG_ENUMERATED)?;
|
||||||
|
if en.len() != 1 {
|
||||||
|
return Err(ProtoError::Auth("NegState ENUMERATED not 1 byte"));
|
||||||
|
}
|
||||||
|
out.neg_state = Some(NegState::from_byte(en[0])?);
|
||||||
|
}
|
||||||
|
TAG_CTX_1 => {
|
||||||
|
let (oid, _) = read_tlv(content, 0, TAG_OBJECT)?;
|
||||||
|
out.supported_mech = Some(oid.to_vec());
|
||||||
|
}
|
||||||
|
TAG_CTX_2 => {
|
||||||
|
let (oct, _) = read_tlv(content, 0, TAG_OCTET)?;
|
||||||
|
out.response_token = Some(oct.to_vec());
|
||||||
|
}
|
||||||
|
TAG_CTX_3 => {
|
||||||
|
let (oct, _) = read_tlv(content, 0, TAG_OCTET)?;
|
||||||
|
out.mech_list_mic = Some(oct.to_vec());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode the **initial** server response to NEGOTIATE — a GSS-API-wrapped
|
||||||
|
/// `NegTokenInit` advertising NTLMSSP only. Used during SMB2 NEGOTIATE
|
||||||
|
/// when the server publishes its security blob.
|
||||||
|
pub fn encode_init_response() -> Vec<u8> {
|
||||||
|
// mechTypes SEQUENCE { OID NTLMSSP }
|
||||||
|
let mut mech_types_seq = Vec::new();
|
||||||
|
write_tlv(TAG_OBJECT, OID_NTLMSSP, &mut mech_types_seq);
|
||||||
|
let mut mech_types_outer = Vec::new();
|
||||||
|
write_tlv(TAG_SEQUENCE, &mech_types_seq, &mut mech_types_outer);
|
||||||
|
// mechTypes is [0] tagged.
|
||||||
|
let mut mech_types_ctx0 = Vec::new();
|
||||||
|
write_tlv(TAG_CTX_0, &mech_types_outer, &mut mech_types_ctx0);
|
||||||
|
|
||||||
|
// NegTokenInit SEQUENCE { mechTypes [0] }
|
||||||
|
let mut neg_token_init = Vec::new();
|
||||||
|
write_tlv(TAG_SEQUENCE, &mech_types_ctx0, &mut neg_token_init);
|
||||||
|
|
||||||
|
// [0] NegTokenInit (negotiationToken choice)
|
||||||
|
let mut choice_init = Vec::new();
|
||||||
|
write_tlv(TAG_CTX_0, &neg_token_init, &mut choice_init);
|
||||||
|
|
||||||
|
// Inside [APPLICATION 0]: { OID SPNEGO, [0] NegTokenInit }
|
||||||
|
let mut gss_inner = Vec::new();
|
||||||
|
write_tlv(TAG_OBJECT, OID_SPNEGO, &mut gss_inner);
|
||||||
|
gss_inner.extend_from_slice(&choice_init);
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
write_tlv(TAG_APP_0, &gss_inner, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a `NegTokenResp` wrapping the server's response token (typically
|
||||||
|
/// the NTLMSSP CHALLENGE_MESSAGE or a final empty-token AcceptCompleted).
|
||||||
|
///
|
||||||
|
/// `supported_mech` is included only with `AcceptIncomplete` (i.e. the very
|
||||||
|
/// first response to a NegTokenInit) — per RFC 4178 §4.2.2.
|
||||||
|
pub fn encode_resp_token(
|
||||||
|
state: NegState,
|
||||||
|
supported_mech: Option<&[u8]>,
|
||||||
|
response_token: Option<&[u8]>,
|
||||||
|
mech_list_mic: Option<&[u8]>,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut seq = Vec::new();
|
||||||
|
|
||||||
|
// [0] negState
|
||||||
|
{
|
||||||
|
let mut en = Vec::new();
|
||||||
|
write_tlv(TAG_ENUMERATED, &[state as u8], &mut en);
|
||||||
|
write_tlv(TAG_CTX_0, &en, &mut seq);
|
||||||
|
}
|
||||||
|
// [1] supportedMech
|
||||||
|
if let Some(oid) = supported_mech {
|
||||||
|
let mut o = Vec::new();
|
||||||
|
write_tlv(TAG_OBJECT, oid, &mut o);
|
||||||
|
write_tlv(TAG_CTX_1, &o, &mut seq);
|
||||||
|
}
|
||||||
|
// [2] responseToken
|
||||||
|
if let Some(tok) = response_token {
|
||||||
|
let mut o = Vec::new();
|
||||||
|
write_tlv(TAG_OCTET, tok, &mut o);
|
||||||
|
write_tlv(TAG_CTX_2, &o, &mut seq);
|
||||||
|
}
|
||||||
|
// [3] mechListMIC
|
||||||
|
if let Some(mic) = mech_list_mic {
|
||||||
|
let mut o = Vec::new();
|
||||||
|
write_tlv(TAG_OCTET, mic, &mut o);
|
||||||
|
write_tlv(TAG_CTX_3, &o, &mut seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut inner = Vec::new();
|
||||||
|
write_tlv(TAG_SEQUENCE, &seq, &mut inner);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
write_tlv(TAG_CTX_1, &inner, &mut out);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Tests
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn der_len_short() {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
der_len(0x42, &mut v);
|
||||||
|
assert_eq!(v, [0x42]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn der_len_long_one_byte() {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
der_len(0xC8, &mut v);
|
||||||
|
assert_eq!(v, [0x81, 0xC8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn der_len_long_two_byte() {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
der_len(0x1234, &mut v);
|
||||||
|
assert_eq!(v, [0x82, 0x12, 0x34]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_len_round_trip() {
|
||||||
|
for n in [0usize, 1, 0x7F, 0x80, 0xFF, 0x100, 0xFFFF, 0x10000] {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
der_len(n, &mut buf);
|
||||||
|
let (got, next) = read_len(&buf, 0).unwrap();
|
||||||
|
assert_eq!(got, n);
|
||||||
|
assert_eq!(next, buf.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_response_is_decodable() {
|
||||||
|
let blob = encode_init_response();
|
||||||
|
// Must start with [APPLICATION 0] (0x60) tag.
|
||||||
|
assert_eq!(blob[0], TAG_APP_0);
|
||||||
|
// Decode with our own decoder going via decode_init_token.
|
||||||
|
// We craft a synthetic "init" by appending an empty mechToken? — not
|
||||||
|
// needed; decode_init_token tolerates absence. Test that the OID and
|
||||||
|
// the [0] mechTypes are reachable.
|
||||||
|
let init = decode_init_token(&blob).unwrap();
|
||||||
|
assert_eq!(init.mech_types.len(), 1);
|
||||||
|
assert_eq!(init.mech_types[0], OID_NTLMSSP);
|
||||||
|
assert!(init.mech_token.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resp_token_round_trip_with_response() {
|
||||||
|
let payload = b"\x01\x02\x03\x04inner-blob";
|
||||||
|
let enc = encode_resp_token(
|
||||||
|
NegState::AcceptIncomplete,
|
||||||
|
Some(OID_NTLMSSP),
|
||||||
|
Some(payload),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let dec = decode_resp_token(&enc).unwrap();
|
||||||
|
assert_eq!(dec.neg_state, Some(NegState::AcceptIncomplete));
|
||||||
|
assert_eq!(dec.supported_mech.as_deref(), Some(OID_NTLMSSP));
|
||||||
|
assert_eq!(dec.response_token.as_deref(), Some(&payload[..]));
|
||||||
|
assert!(dec.mech_list_mic.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resp_token_round_trip_completed() {
|
||||||
|
let enc = encode_resp_token(NegState::AcceptCompleted, None, None, None);
|
||||||
|
let dec = decode_resp_token(&enc).unwrap();
|
||||||
|
assert_eq!(dec.neg_state, Some(NegState::AcceptCompleted));
|
||||||
|
assert!(dec.supported_mech.is_none());
|
||||||
|
assert!(dec.response_token.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resp_token_with_mic() {
|
||||||
|
let mic = vec![0xAAu8; 16];
|
||||||
|
let enc = encode_resp_token(NegState::AcceptCompleted, None, None, Some(&mic));
|
||||||
|
let dec = decode_resp_token(&enc).unwrap();
|
||||||
|
assert_eq!(dec.mech_list_mic.as_deref(), Some(mic.as_slice()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a NegTokenInit by hand (containing a mechToken) and decode it.
|
||||||
|
#[test]
|
||||||
|
fn decode_init_with_mech_token() {
|
||||||
|
let inner_token = b"NTLMSSP\x00fakeNegotiate";
|
||||||
|
|
||||||
|
// mechTypes
|
||||||
|
let mut mts = Vec::new();
|
||||||
|
write_tlv(TAG_OBJECT, OID_NTLMSSP, &mut mts);
|
||||||
|
let mut mts_seq = Vec::new();
|
||||||
|
write_tlv(TAG_SEQUENCE, &mts, &mut mts_seq);
|
||||||
|
let mut mts_ctx0 = Vec::new();
|
||||||
|
write_tlv(TAG_CTX_0, &mts_seq, &mut mts_ctx0);
|
||||||
|
|
||||||
|
// mechToken [2] OCTET STRING
|
||||||
|
let mut mt_oct = Vec::new();
|
||||||
|
write_tlv(TAG_OCTET, inner_token, &mut mt_oct);
|
||||||
|
let mut mt_ctx2 = Vec::new();
|
||||||
|
write_tlv(TAG_CTX_2, &mt_oct, &mut mt_ctx2);
|
||||||
|
|
||||||
|
// SEQUENCE { [0] mechTypes, [2] mechToken }
|
||||||
|
let mut seq = Vec::new();
|
||||||
|
seq.extend_from_slice(&mts_ctx0);
|
||||||
|
seq.extend_from_slice(&mt_ctx2);
|
||||||
|
|
||||||
|
let mut neg_token_init = Vec::new();
|
||||||
|
write_tlv(TAG_SEQUENCE, &seq, &mut neg_token_init);
|
||||||
|
|
||||||
|
let mut choice = Vec::new();
|
||||||
|
write_tlv(TAG_CTX_0, &neg_token_init, &mut choice);
|
||||||
|
|
||||||
|
let mut gss_inner = Vec::new();
|
||||||
|
write_tlv(TAG_OBJECT, OID_SPNEGO, &mut gss_inner);
|
||||||
|
gss_inner.extend_from_slice(&choice);
|
||||||
|
|
||||||
|
let mut blob = Vec::new();
|
||||||
|
write_tlv(TAG_APP_0, &gss_inner, &mut blob);
|
||||||
|
|
||||||
|
let dec = decode_init_token(&blob).unwrap();
|
||||||
|
assert_eq!(dec.mech_types.len(), 1);
|
||||||
|
assert_eq!(dec.mech_types[0], OID_NTLMSSP);
|
||||||
|
assert_eq!(dec.mech_token.as_deref(), Some(&inner_token[..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_non_spnego_oid() {
|
||||||
|
// Build a GSS token with a different OID inside.
|
||||||
|
let bad_oid = [0x2bu8, 0x06, 0x01, 0x01, 0x01, 0x01];
|
||||||
|
let mut gss_inner = Vec::new();
|
||||||
|
write_tlv(TAG_OBJECT, &bad_oid, &mut gss_inner);
|
||||||
|
// Empty [0] payload.
|
||||||
|
let mut empty = Vec::new();
|
||||||
|
write_tlv(TAG_SEQUENCE, &[], &mut empty);
|
||||||
|
let mut choice = Vec::new();
|
||||||
|
write_tlv(TAG_CTX_0, &empty, &mut choice);
|
||||||
|
gss_inner.extend_from_slice(&choice);
|
||||||
|
let mut blob = Vec::new();
|
||||||
|
write_tlv(TAG_APP_0, &gss_inner, &mut blob);
|
||||||
|
|
||||||
|
let err = decode_init_token(&blob).unwrap_err();
|
||||||
|
assert!(matches!(err, ProtoError::Auth(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_truncated_blob() {
|
||||||
|
let err = decode_init_token(&[0x60, 0x05, 0xAA, 0xBB]).unwrap_err();
|
||||||
|
assert!(matches!(err, ProtoError::Auth(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
20
vendor/smb-server/src/proto/crypto.rs
vendored
Normal file
20
vendor/smb-server/src/proto/crypto.rs
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//! SMB signing, key derivation, pre-auth integrity.
|
||||||
|
//!
|
||||||
|
//! Submodules:
|
||||||
|
//! * [`kdf`] — SP 800-108 CTR-mode KDF (`SMB2KDF`) and SMB-specific
|
||||||
|
//! signing/application key helpers (MS-SMB2 §3.1.4.2).
|
||||||
|
//! * [`sign`] — HMAC-SHA-256 (SMB 2.x) and AES-CMAC (SMB 3.x) signing of
|
||||||
|
//! SMB2 messages (MS-SMB2 §3.1.4.1).
|
||||||
|
//! * [`preauth`] — SMB 3.1.1 pre-auth integrity running SHA-512 hash
|
||||||
|
//! (MS-SMB2 §3.1.4.4.1, §3.3.5.4).
|
||||||
|
//!
|
||||||
|
//! Encryption (AES-CCM/AES-GCM) is intentionally out of scope for v1; see the
|
||||||
|
//! design spec.
|
||||||
|
|
||||||
|
pub mod kdf;
|
||||||
|
pub mod preauth;
|
||||||
|
pub mod sign;
|
||||||
|
|
||||||
|
pub use kdf::{signing_key_30, signing_key_311};
|
||||||
|
pub use preauth::PreauthIntegrity;
|
||||||
|
pub use sign::{SigningAlgo, sign, verify};
|
||||||
146
vendor/smb-server/src/proto/crypto/kdf.rs
vendored
Normal file
146
vendor/smb-server/src/proto/crypto/kdf.rs
vendored
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
//! SP 800-108 CTR-mode KDF using HMAC-SHA-256, as required by MS-SMB2 §3.1.4.2.
|
||||||
|
//!
|
||||||
|
//! Fixed input fed to the PRF (HMAC-SHA-256) is:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! i (u32be=1) || Label || 0x00 || Context || L (u32be=128)
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Convention in this crate:
|
||||||
|
//! * Callers pass `label` and `context` *already including* a trailing `\x00`.
|
||||||
|
//! * The KDF then **also** emits a single `0x00` separator between `label`
|
||||||
|
//! and `context`, so the wire-level input has two consecutive NULs at that
|
||||||
|
//! boundary. This matches what real Windows clients require — a single NUL
|
||||||
|
//! produces a different signing key and Windows rejects with
|
||||||
|
//! `STATUS_ACCESS_DENIED` / event 31013 "signing validation failed".
|
||||||
|
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
|
/// SP 800-108 CTR-mode KDF using HMAC-SHA-256.
|
||||||
|
///
|
||||||
|
/// * `key` — the input key (session key, typically 16 bytes).
|
||||||
|
/// * `label` — the label string with trailing NUL (e.g. `b"SMB2AESCMAC\x00"`).
|
||||||
|
/// * `context` — the context string with trailing NUL (e.g. `b"SmbSign\x00"`).
|
||||||
|
///
|
||||||
|
/// Returns the first 16 bytes of `HMAC-SHA-256(key, fixed_input)` where
|
||||||
|
/// `fixed_input = [0,0,0,1] || label || 0x00 || context || [0,0,0,0x80]`.
|
||||||
|
/// The single separator `0x00` between `label` and `context` is required for
|
||||||
|
/// Windows interop; do not remove.
|
||||||
|
pub fn smb2_kdf(key: &[u8], label: &[u8], context: &[u8]) -> [u8; 16] {
|
||||||
|
let mut mac =
|
||||||
|
<HmacSha256 as Mac>::new_from_slice(key).expect("HMAC-SHA-256 accepts keys of any length");
|
||||||
|
|
||||||
|
// i = 1 (big-endian u32)
|
||||||
|
mac.update(&[0x00, 0x00, 0x00, 0x01]);
|
||||||
|
// Label (including trailing NUL provided by caller)
|
||||||
|
mac.update(label);
|
||||||
|
// SP 800-108 separator byte between Label and Context (in addition to any
|
||||||
|
// trailing NUL the caller already included in `label`).
|
||||||
|
mac.update(&[0x00]);
|
||||||
|
// Context (including trailing NUL provided by caller, or for 3.1.1 the
|
||||||
|
// 64-byte preauth hash)
|
||||||
|
mac.update(context);
|
||||||
|
// L = 128 bits (big-endian u32)
|
||||||
|
mac.update(&[0x00, 0x00, 0x00, 0x80]);
|
||||||
|
|
||||||
|
let full = mac.finalize().into_bytes();
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
out.copy_from_slice(&full[..16]);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Convenience helpers ---------------------------------------------------
|
||||||
|
|
||||||
|
/// Signing key for SMB 3.0 / 3.0.2.
|
||||||
|
///
|
||||||
|
/// Label = `"SMB2AESCMAC\x00"`, Context = `"SmbSign\x00"` (MS-SMB2 §3.1.4.2).
|
||||||
|
pub fn signing_key_30(session_key: &[u8]) -> [u8; 16] {
|
||||||
|
smb2_kdf(session_key, b"SMB2AESCMAC\x00", b"SmbSign\x00")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signing key for SMB 3.1.1.
|
||||||
|
///
|
||||||
|
/// Label = `"SMBSigningKey\x00"`, Context = pre-auth integrity hash
|
||||||
|
/// (the SHA-512 snapshot taken at SESSION_SETUP completion).
|
||||||
|
pub fn signing_key_311(session_key: &[u8], preauth_hash: &[u8; 64]) -> [u8; 16] {
|
||||||
|
smb2_kdf(session_key, b"SMBSigningKey\x00", preauth_hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Determinism / shape sanity: the function always produces 16 bytes and
|
||||||
|
/// is reproducible for the same inputs.
|
||||||
|
#[test]
|
||||||
|
fn smb2_kdf_is_deterministic() {
|
||||||
|
let key = [0x11u8; 16];
|
||||||
|
let a = smb2_kdf(&key, b"SMB2AESCMAC\x00", b"SmbSign\x00");
|
||||||
|
let b = smb2_kdf(&key, b"SMB2AESCMAC\x00", b"SmbSign\x00");
|
||||||
|
assert_eq!(a, b);
|
||||||
|
assert_eq!(a.len(), 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Different label or context → different output.
|
||||||
|
#[test]
|
||||||
|
fn smb2_kdf_label_and_context_matter() {
|
||||||
|
let key = [0x42u8; 16];
|
||||||
|
let signing = smb2_kdf(&key, b"SMB2AESCMAC\x00", b"SmbSign\x00");
|
||||||
|
let app = smb2_kdf(&key, b"SMB2APP\x00", b"SmbRpc\x00");
|
||||||
|
assert_ne!(signing, app);
|
||||||
|
|
||||||
|
let other_ctx = smb2_kdf(&key, b"SMB2AESCMAC\x00", b"OtherCtx\x00");
|
||||||
|
assert_ne!(signing, other_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Known-answer test computed directly from the documented fixed-input
|
||||||
|
/// construction. This pins the exact byte layout we feed to HMAC.
|
||||||
|
///
|
||||||
|
/// Reference computation (Python):
|
||||||
|
/// ```text
|
||||||
|
/// import hmac, hashlib
|
||||||
|
/// key = bytes(16) # all zeros
|
||||||
|
/// label = b"SMB2AESCMAC\x00"
|
||||||
|
/// context = b"SmbSign\x00"
|
||||||
|
/// data = b"\x00\x00\x00\x01" + label + b"\x00" + context + b"\x00\x00\x00\x80"
|
||||||
|
/// hmac.new(key, data, hashlib.sha256).hexdigest()[:32]
|
||||||
|
/// # = "9951088b83220f39d99420419d16d393"
|
||||||
|
/// ```
|
||||||
|
#[test]
|
||||||
|
fn smb2_kdf_known_answer_zero_key_signing_30() {
|
||||||
|
let key = [0u8; 16];
|
||||||
|
let out = signing_key_30(&key);
|
||||||
|
let expected = hex::decode("9951088b83220f39d99420419d16d393").unwrap();
|
||||||
|
assert_eq!(out.as_slice(), expected.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 3.1.1 derivation differs from 3.0 (different label, 64-byte context).
|
||||||
|
#[test]
|
||||||
|
fn smb2_kdf_311_differs_from_30() {
|
||||||
|
let key = [0u8; 16];
|
||||||
|
let preauth = [0u8; 64];
|
||||||
|
let k30 = signing_key_30(&key);
|
||||||
|
let k311 = signing_key_311(&key, &preauth);
|
||||||
|
assert_ne!(k30, k311);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Known-answer test for 3.1.1 with zero key and zero pre-auth hash.
|
||||||
|
///
|
||||||
|
/// Reference computation (Python):
|
||||||
|
/// ```text
|
||||||
|
/// data = b"\x00\x00\x00\x01" + b"SMBSigningKey\x00" + b"\x00" + bytes(64) + b"\x00\x00\x00\x80"
|
||||||
|
/// hmac.new(bytes(16), data, hashlib.sha256).hexdigest()[:32]
|
||||||
|
/// # = "a06a153e09bd0f34706a5c671acaa37d"
|
||||||
|
/// ```
|
||||||
|
#[test]
|
||||||
|
fn smb2_kdf_known_answer_zero_key_signing_311() {
|
||||||
|
let key = [0u8; 16];
|
||||||
|
let preauth = [0u8; 64];
|
||||||
|
let out = signing_key_311(&key, &preauth);
|
||||||
|
let expected = hex::decode("a06a153e09bd0f34706a5c671acaa37d").unwrap();
|
||||||
|
assert_eq!(out.as_slice(), expected.as_slice());
|
||||||
|
}
|
||||||
|
}
|
||||||
115
vendor/smb-server/src/proto/crypto/preauth.rs
vendored
Normal file
115
vendor/smb-server/src/proto/crypto/preauth.rs
vendored
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
//! SMB 3.1.1 pre-auth integrity (MS-SMB2 §3.1.4.4.1, §3.3.5.4).
|
||||||
|
//!
|
||||||
|
//! A running SHA-512 hash, initialized to all zeros, that absorbs SMB 3.1.1
|
||||||
|
//! preauth messages (transport prefix excluded). Connection state uses this for
|
||||||
|
//! NEGOTIATE; each SESSION_SETUP exchange forks its own instance. Per spec:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! PreauthIntegrityHashValue =
|
||||||
|
//! SHA-512(PreauthIntegrityHashValue || RequestOrResponse)
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha512};
|
||||||
|
|
||||||
|
/// Running SMB 3.1.1 preauth integrity hash.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PreauthIntegrity {
|
||||||
|
hash: [u8; 64],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PreauthIntegrity {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreauthIntegrity {
|
||||||
|
/// Create a fresh state, hash initialized to all zeros.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { hash: [0u8; 64] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Absorb a frame's bytes (excluding the 4-byte Direct-TCP transport
|
||||||
|
/// prefix). Updates `hash` in place.
|
||||||
|
pub fn update(&mut self, frame: &[u8]) {
|
||||||
|
let mut hasher = Sha512::new();
|
||||||
|
hasher.update(self.hash);
|
||||||
|
hasher.update(frame);
|
||||||
|
let out = hasher.finalize();
|
||||||
|
self.hash.copy_from_slice(&out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take a copy of the current hash. Used as the KDF context for session
|
||||||
|
/// keys at SESSION_SETUP completion.
|
||||||
|
pub fn snapshot(&self) -> [u8; 64] {
|
||||||
|
self.hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use sha2::{Digest, Sha512};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_starts_at_zero() {
|
||||||
|
let p = PreauthIntegrity::new();
|
||||||
|
assert_eq!(p.snapshot(), [0u8; 64]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_starts_at_zero() {
|
||||||
|
let p = PreauthIntegrity::default();
|
||||||
|
assert_eq!(p.snapshot(), [0u8; 64]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two-step chain matches the literal spec formula.
|
||||||
|
#[test]
|
||||||
|
fn chain_two_buffers_matches_precomputed() {
|
||||||
|
let mut p = PreauthIntegrity::new();
|
||||||
|
|
||||||
|
let buf1 = b"NEGOTIATE_REQUEST_FIXTURE";
|
||||||
|
let buf2 = b"NEGOTIATE_RESPONSE_FIXTURE";
|
||||||
|
p.update(buf1);
|
||||||
|
p.update(buf2);
|
||||||
|
|
||||||
|
// Precomputed using Python:
|
||||||
|
// h = bytes(64)
|
||||||
|
// h = sha512(h + buf1).digest()
|
||||||
|
// h = sha512(h + buf2).digest()
|
||||||
|
let expected = hex::decode(
|
||||||
|
"62deb17d9d07d155b7c634dbfec3ac10c32b80981d925333499a6fbd168d0ee3\
|
||||||
|
4d29b093a185529fd927ade8d851c8e8b0d9b55608c7674e4d3e8d438343c95c",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(p.snapshot().as_slice(), expected.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chained call equivalence: explicit SHA-512(prev || frame) on the side
|
||||||
|
/// must match what `update` produces internally.
|
||||||
|
#[test]
|
||||||
|
fn update_equals_manual_sha512() {
|
||||||
|
let buf = b"SOME_FRAME_BYTES_HERE_0123456789";
|
||||||
|
|
||||||
|
let mut p = PreauthIntegrity::new();
|
||||||
|
p.update(buf);
|
||||||
|
|
||||||
|
let mut hasher = Sha512::new();
|
||||||
|
hasher.update([0u8; 64]);
|
||||||
|
hasher.update(buf);
|
||||||
|
let manual = hasher.finalize();
|
||||||
|
|
||||||
|
assert_eq!(p.snapshot().as_slice(), manual.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot must not be aliased — modifying state after snapshot must not
|
||||||
|
/// affect the snapshot already taken.
|
||||||
|
#[test]
|
||||||
|
fn snapshot_is_a_copy() {
|
||||||
|
let mut p = PreauthIntegrity::new();
|
||||||
|
p.update(b"first");
|
||||||
|
let snap = p.snapshot();
|
||||||
|
p.update(b"second");
|
||||||
|
assert_ne!(p.snapshot(), snap);
|
||||||
|
}
|
||||||
|
}
|
||||||
259
vendor/smb-server/src/proto/crypto/sign.rs
vendored
Normal file
259
vendor/smb-server/src/proto/crypto/sign.rs
vendored
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
//! SMB2/3 message signing per MS-SMB2 §3.1.4.1.
|
||||||
|
//!
|
||||||
|
//! Two algorithms are supported:
|
||||||
|
//! 1. **HMAC-SHA-256** for SMB 2.0.2 / 2.1 / 3.0 negotiating without 3.x
|
||||||
|
//! signing.
|
||||||
|
//! 2. **AES-CMAC** for SMB 3.0+.
|
||||||
|
//!
|
||||||
|
//! Both produce a 16-byte signature that lives at bytes 48..64 of the SMB2
|
||||||
|
//! header (the `Signature` field, MS-SMB2 §2.2.1.2).
|
||||||
|
//!
|
||||||
|
//! Algorithm:
|
||||||
|
//! 1. Zero out bytes 48..64 of the message.
|
||||||
|
//! 2. Compute MAC over the **entire** message (header + body).
|
||||||
|
//! 3. Place the first 16 bytes of MAC at bytes 48..64.
|
||||||
|
|
||||||
|
use aes::Aes128;
|
||||||
|
use cmac::Cmac;
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::proto::error::{ProtoError, ProtoResult};
|
||||||
|
|
||||||
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
type CmacAes128 = Cmac<Aes128>;
|
||||||
|
|
||||||
|
/// SMB2 header is 64 bytes; the 16-byte signature field starts at offset 48.
|
||||||
|
const SIG_OFF: usize = 48;
|
||||||
|
const SIG_LEN: usize = 16;
|
||||||
|
const SMB2_HEADER_LEN: usize = 64;
|
||||||
|
|
||||||
|
/// Which signing algorithm to use for a given session/dialect.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SigningAlgo {
|
||||||
|
/// HMAC-SHA-256, used by SMB 2.x.
|
||||||
|
HmacSha256,
|
||||||
|
/// AES-CMAC over AES-128, used by SMB 3.0+.
|
||||||
|
AesCmac,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the 16-byte MAC over `msg` as if the SMB2 signature field were
|
||||||
|
/// zeroed, without copying the whole message.
|
||||||
|
fn compute_mac_zeroed_signature(msg: &[u8], key: &[u8; 16], algo: SigningAlgo) -> [u8; SIG_LEN] {
|
||||||
|
let mut out = [0u8; SIG_LEN];
|
||||||
|
let zero_signature = [0u8; SIG_LEN];
|
||||||
|
let prefix = &msg[..SIG_OFF];
|
||||||
|
let suffix = &msg[SIG_OFF + SIG_LEN..];
|
||||||
|
|
||||||
|
match algo {
|
||||||
|
SigningAlgo::HmacSha256 => {
|
||||||
|
let mut mac = <HmacSha256 as Mac>::new_from_slice(key)
|
||||||
|
.expect("HMAC-SHA-256 accepts keys of any length");
|
||||||
|
mac.update(prefix);
|
||||||
|
mac.update(&zero_signature);
|
||||||
|
mac.update(suffix);
|
||||||
|
let full = mac.finalize().into_bytes();
|
||||||
|
out.copy_from_slice(&full[..SIG_LEN]);
|
||||||
|
}
|
||||||
|
SigningAlgo::AesCmac => {
|
||||||
|
let mut mac = <CmacAes128 as Mac>::new_from_slice(key)
|
||||||
|
.expect("AES-128-CMAC requires a 16-byte key, which we have");
|
||||||
|
mac.update(prefix);
|
||||||
|
mac.update(&zero_signature);
|
||||||
|
mac.update(suffix);
|
||||||
|
let full = mac.finalize().into_bytes();
|
||||||
|
out.copy_from_slice(&full[..SIG_LEN]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute and embed a signature in `msg`. Mutates `msg` in place.
|
||||||
|
///
|
||||||
|
/// The caller is responsible for setting the SMB2 SIGNED flag (`0x00000008`)
|
||||||
|
/// on the header *before* calling — it is part of the bytes that get MAC'd.
|
||||||
|
///
|
||||||
|
/// Errors if `msg` is too short to contain an SMB2 header (< 64 bytes).
|
||||||
|
pub fn sign(msg: &mut [u8], key: &[u8; 16], algo: SigningAlgo) -> ProtoResult<()> {
|
||||||
|
if msg.len() < SMB2_HEADER_LEN {
|
||||||
|
return Err(ProtoError::Crypto("message too short to sign"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute MAC over the whole message with the signature field treated as
|
||||||
|
// zero, then place the MAC into the signature field.
|
||||||
|
let mac = compute_mac_zeroed_signature(msg, key, algo);
|
||||||
|
msg[SIG_OFF..SIG_OFF + SIG_LEN].copy_from_slice(&mac);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the signature in `msg`. Does **not** modify `msg`.
|
||||||
|
///
|
||||||
|
/// Uses constant-time comparison. Returns `Ok(())` if the embedded signature
|
||||||
|
/// matches the freshly computed MAC.
|
||||||
|
pub fn verify(msg: &[u8], key: &[u8; 16], algo: SigningAlgo) -> ProtoResult<()> {
|
||||||
|
if msg.len() < SMB2_HEADER_LEN {
|
||||||
|
return Err(ProtoError::Crypto("message too short to verify"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture the embedded signature.
|
||||||
|
let mut embedded = [0u8; SIG_LEN];
|
||||||
|
embedded.copy_from_slice(&msg[SIG_OFF..SIG_OFF + SIG_LEN]);
|
||||||
|
|
||||||
|
let computed = compute_mac_zeroed_signature(msg, key, algo);
|
||||||
|
|
||||||
|
if constant_time_eq(&embedded, &computed) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ProtoError::Crypto("signature mismatch"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constant-time comparison of two 16-byte arrays.
|
||||||
|
#[inline]
|
||||||
|
fn constant_time_eq(a: &[u8; SIG_LEN], b: &[u8; SIG_LEN]) -> bool {
|
||||||
|
let mut diff: u8 = 0;
|
||||||
|
for i in 0..SIG_LEN {
|
||||||
|
diff |= a[i] ^ b[i];
|
||||||
|
}
|
||||||
|
diff == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Build a 100-byte message: a plausible 64-byte SMB2 header followed by
|
||||||
|
/// 36 bytes of body. The signature region (bytes 48..64) is left zero;
|
||||||
|
/// `sign` will overwrite it.
|
||||||
|
fn fixture_message() -> Vec<u8> {
|
||||||
|
let mut msg = vec![0u8; 100];
|
||||||
|
// Magic: 0xFE 'S' 'M' 'B'
|
||||||
|
msg[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
|
||||||
|
// StructureSize = 64
|
||||||
|
msg[4..6].copy_from_slice(&64u16.to_le_bytes());
|
||||||
|
// Pretend ChannelSequence = 0
|
||||||
|
msg[6..8].copy_from_slice(&0u16.to_le_bytes());
|
||||||
|
// Command = NEGOTIATE (0)
|
||||||
|
msg[12..14].copy_from_slice(&0u16.to_le_bytes());
|
||||||
|
// Flags: SIGNED (0x00000008)
|
||||||
|
msg[16..20].copy_from_slice(&0x0000_0008u32.to_le_bytes());
|
||||||
|
// Body filler
|
||||||
|
for (i, b) in msg[64..].iter_mut().enumerate() {
|
||||||
|
*b = (i as u8).wrapping_mul(7);
|
||||||
|
}
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sign_and_verify_hmac_sha256() {
|
||||||
|
let key = [0xAAu8; 16];
|
||||||
|
let mut msg = fixture_message();
|
||||||
|
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||||
|
|
||||||
|
// Signature should now be non-zero (overwhelmingly likely).
|
||||||
|
assert_ne!(&msg[SIG_OFF..SIG_OFF + SIG_LEN], &[0u8; 16]);
|
||||||
|
|
||||||
|
verify(&msg, &key, SigningAlgo::HmacSha256).expect("verify ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sign_and_verify_aes_cmac() {
|
||||||
|
let key = [0x55u8; 16];
|
||||||
|
let mut msg = fixture_message();
|
||||||
|
sign(&mut msg, &key, SigningAlgo::AesCmac).expect("sign ok");
|
||||||
|
assert_ne!(&msg[SIG_OFF..SIG_OFF + SIG_LEN], &[0u8; 16]);
|
||||||
|
verify(&msg, &key, SigningAlgo::AesCmac).expect("verify ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tamper_outside_sig_fails_verify_hmac() {
|
||||||
|
let key = [0xAAu8; 16];
|
||||||
|
let mut msg = fixture_message();
|
||||||
|
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||||
|
|
||||||
|
// Flip one body byte.
|
||||||
|
msg[80] ^= 0x01;
|
||||||
|
let res = verify(&msg, &key, SigningAlgo::HmacSha256);
|
||||||
|
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tamper_outside_sig_fails_verify_cmac() {
|
||||||
|
let key = [0x55u8; 16];
|
||||||
|
let mut msg = fixture_message();
|
||||||
|
sign(&mut msg, &key, SigningAlgo::AesCmac).expect("sign ok");
|
||||||
|
|
||||||
|
// Flip a header byte (not in the sig region).
|
||||||
|
msg[10] ^= 0xFF;
|
||||||
|
let res = verify(&msg, &key, SigningAlgo::AesCmac);
|
||||||
|
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tamper_signature_fails_verify() {
|
||||||
|
let key = [0xAAu8; 16];
|
||||||
|
let mut msg = fixture_message();
|
||||||
|
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||||
|
msg[SIG_OFF] ^= 0x01;
|
||||||
|
let res = verify(&msg, &key, SigningAlgo::HmacSha256);
|
||||||
|
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrong_key_fails_verify() {
|
||||||
|
let key = [0xAAu8; 16];
|
||||||
|
let bad_key = [0xBBu8; 16];
|
||||||
|
let mut msg = fixture_message();
|
||||||
|
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||||
|
let res = verify(&msg, &bad_key, SigningAlgo::HmacSha256);
|
||||||
|
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn too_short_message_errors() {
|
||||||
|
let mut tiny = [0u8; 10];
|
||||||
|
let key = [0u8; 16];
|
||||||
|
let res = sign(&mut tiny, &key, SigningAlgo::HmacSha256);
|
||||||
|
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||||
|
let res = verify(&tiny, &key, SigningAlgo::HmacSha256);
|
||||||
|
assert!(matches!(res, Err(ProtoError::Crypto(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_does_not_mutate_message_hmac_sha256() {
|
||||||
|
let key = [0xAAu8; 16];
|
||||||
|
let mut msg = fixture_message();
|
||||||
|
sign(&mut msg, &key, SigningAlgo::HmacSha256).expect("sign ok");
|
||||||
|
let snapshot = msg.clone();
|
||||||
|
verify(&msg, &key, SigningAlgo::HmacSha256).expect("verify ok");
|
||||||
|
assert_eq!(msg, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_does_not_mutate_message_aes_cmac() {
|
||||||
|
let key = [0x55u8; 16];
|
||||||
|
let mut msg = fixture_message();
|
||||||
|
sign(&mut msg, &key, SigningAlgo::AesCmac).expect("sign ok");
|
||||||
|
let snapshot = msg.clone();
|
||||||
|
verify(&msg, &key, SigningAlgo::AesCmac).expect("verify ok");
|
||||||
|
assert_eq!(msg, snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sign_ignores_existing_signature_bytes() {
|
||||||
|
let key = [0xAAu8; 16];
|
||||||
|
let mut clean = fixture_message();
|
||||||
|
let mut dirty = fixture_message();
|
||||||
|
dirty[SIG_OFF..SIG_OFF + SIG_LEN].fill(0xCC);
|
||||||
|
|
||||||
|
sign(&mut clean, &key, SigningAlgo::HmacSha256).expect("sign clean");
|
||||||
|
sign(&mut dirty, &key, SigningAlgo::HmacSha256).expect("sign dirty");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
&clean[SIG_OFF..SIG_OFF + SIG_LEN],
|
||||||
|
&dirty[SIG_OFF..SIG_OFF + SIG_LEN]
|
||||||
|
);
|
||||||
|
verify(&dirty, &key, SigningAlgo::HmacSha256).expect("verify dirty");
|
||||||
|
}
|
||||||
|
}
|
||||||
26
vendor/smb-server/src/proto/error.rs
vendored
Normal file
26
vendor/smb-server/src/proto/error.rs
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//! Crate-wide error type for the internal SMB protocol layer.
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub type ProtoResult<T> = Result<T, ProtoError>;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ProtoError {
|
||||||
|
#[error("malformed wire frame: {0}")]
|
||||||
|
Malformed(&'static str),
|
||||||
|
|
||||||
|
#[error("unsupported dialect: 0x{0:04x}")]
|
||||||
|
UnsupportedDialect(u16),
|
||||||
|
|
||||||
|
#[error("auth failure: {0}")]
|
||||||
|
Auth(&'static str),
|
||||||
|
|
||||||
|
#[error("crypto failure: {0}")]
|
||||||
|
Crypto(&'static str),
|
||||||
|
|
||||||
|
#[error("io error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("binrw error: {0}")]
|
||||||
|
Binrw(#[from] binrw::Error),
|
||||||
|
}
|
||||||
155
vendor/smb-server/src/proto/framing.rs
vendored
Normal file
155
vendor/smb-server/src/proto/framing.rs
vendored
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
//! Direct-TCP / NetBIOS-over-TCP framing for SMB2/3.
|
||||||
|
//!
|
||||||
|
//! MS-SMB2 §2.1 requires a 4-byte big-endian length prefix on every TCP frame:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! +-------+--------------------------------+
|
||||||
|
//! | 0x00 | 24-bit big-endian payload len |
|
||||||
|
//! +-------+--------------------------------+
|
||||||
|
//! | SMB2 packet ... |
|
||||||
|
//! +----------------------------------------+
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The top byte is reserved (must be zero in Direct-TCP transport — it is the
|
||||||
|
//! NetBIOS session-message-type byte from RFC 1002 §4.3.1). The remaining 24
|
||||||
|
//! bits encode the payload length, so the absolute maximum on the wire is
|
||||||
|
//! `2^24 - 1 = 16_777_215` bytes (16 MiB - 1). We enforce that as the cap.
|
||||||
|
//!
|
||||||
|
//! This module is async-runtime-agnostic. Only sync helpers operating on byte
|
||||||
|
//! slices and `Vec<u8>` live here; the server crate wraps these with tokio
|
||||||
|
//! I/O.
|
||||||
|
|
||||||
|
use crate::proto::error::{ProtoError, ProtoResult};
|
||||||
|
|
||||||
|
/// Length of the Direct-TCP frame header (4 bytes).
|
||||||
|
pub const FRAME_HEADER_LEN: usize = 4;
|
||||||
|
|
||||||
|
/// Maximum payload size representable by the 3-byte length field.
|
||||||
|
///
|
||||||
|
/// MS-SMB2 §2.1 — `2^24 - 1 = 16_777_215` bytes.
|
||||||
|
pub const MAX_FRAME_PAYLOAD: u32 = 0x00FF_FFFF;
|
||||||
|
|
||||||
|
/// Encode a single Direct-TCP frame: 4-byte header + payload.
|
||||||
|
///
|
||||||
|
/// Panics in debug if the payload exceeds [`MAX_FRAME_PAYLOAD`]; release builds
|
||||||
|
/// silently truncate the high byte.
|
||||||
|
pub fn encode_frame(payload: &[u8], out: &mut Vec<u8>) {
|
||||||
|
debug_assert!(
|
||||||
|
payload.len() as u64 <= MAX_FRAME_PAYLOAD as u64,
|
||||||
|
"frame payload exceeds 16 MiB - 1"
|
||||||
|
);
|
||||||
|
let len = payload.len() as u32;
|
||||||
|
// Top byte is the NetBIOS session-message type (0x00 for Direct-TCP).
|
||||||
|
// Lower 3 bytes are payload length, big-endian.
|
||||||
|
out.reserve(FRAME_HEADER_LEN + payload.len());
|
||||||
|
out.push(0x00);
|
||||||
|
out.push(((len >> 16) & 0xFF) as u8);
|
||||||
|
out.push(((len >> 8) & 0xFF) as u8);
|
||||||
|
out.push((len & 0xFF) as u8);
|
||||||
|
out.extend_from_slice(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode the 4-byte frame header, returning the payload length.
|
||||||
|
///
|
||||||
|
/// Returns [`ProtoError::Malformed`] if the top byte is non-zero (NetBIOS
|
||||||
|
/// session-message type other than `SESSION MESSAGE` is not supported in
|
||||||
|
/// Direct-TCP transport).
|
||||||
|
pub fn decode_frame_header(bytes: &[u8; FRAME_HEADER_LEN]) -> ProtoResult<u32> {
|
||||||
|
if bytes[0] != 0x00 {
|
||||||
|
return Err(ProtoError::Malformed(
|
||||||
|
"NetBIOS session-message type byte must be 0x00 for Direct-TCP",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let len = (u32::from(bytes[1]) << 16) | (u32::from(bytes[2]) << 8) | u32::from(bytes[3]);
|
||||||
|
Ok(len)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: read one full frame from a contiguous byte slice.
|
||||||
|
///
|
||||||
|
/// Returns the payload slice and the remaining bytes after the frame.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn decode_frame(buf: &[u8]) -> ProtoResult<(&[u8], &[u8])> {
|
||||||
|
if buf.len() < FRAME_HEADER_LEN {
|
||||||
|
return Err(ProtoError::Malformed("short frame header"));
|
||||||
|
}
|
||||||
|
let mut hdr = [0u8; FRAME_HEADER_LEN];
|
||||||
|
hdr.copy_from_slice(&buf[..FRAME_HEADER_LEN]);
|
||||||
|
let len = decode_frame_header(&hdr)? as usize;
|
||||||
|
let total = FRAME_HEADER_LEN + len;
|
||||||
|
if buf.len() < total {
|
||||||
|
return Err(ProtoError::Malformed("truncated frame body"));
|
||||||
|
}
|
||||||
|
Ok((&buf[FRAME_HEADER_LEN..total], &buf[total..]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encodes_empty_frame() {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
encode_frame(&[], &mut out);
|
||||||
|
assert_eq!(out, [0x00, 0x00, 0x00, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encodes_simple_frame() {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
encode_frame(&[0xAA, 0xBB, 0xCC], &mut out);
|
||||||
|
assert_eq!(out, [0x00, 0x00, 0x00, 0x03, 0xAA, 0xBB, 0xCC]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips_random_payload() {
|
||||||
|
let payload: Vec<u8> = (0u8..=200).collect();
|
||||||
|
let mut wire = Vec::new();
|
||||||
|
encode_frame(&payload, &mut wire);
|
||||||
|
|
||||||
|
let (decoded, rest) = decode_frame(&wire).unwrap();
|
||||||
|
assert_eq!(decoded, payload.as_slice());
|
||||||
|
assert!(rest.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decodes_header_three_byte_length() {
|
||||||
|
// 0x00_12_34_56 -> length 0x123456
|
||||||
|
let len = decode_frame_header(&[0x00, 0x12, 0x34, 0x56]).unwrap();
|
||||||
|
assert_eq!(len, 0x0012_3456);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decodes_header_max_length() {
|
||||||
|
let len = decode_frame_header(&[0x00, 0xFF, 0xFF, 0xFF]).unwrap();
|
||||||
|
assert_eq!(len, MAX_FRAME_PAYLOAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_nonzero_top_byte() {
|
||||||
|
let err = decode_frame_header(&[0x81, 0x00, 0x00, 0x00]).unwrap_err();
|
||||||
|
assert!(matches!(err, ProtoError::Malformed(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_frame_handles_trailing_data() {
|
||||||
|
let mut wire = Vec::new();
|
||||||
|
encode_frame(&[1, 2, 3], &mut wire);
|
||||||
|
wire.extend_from_slice(&[9, 9, 9]); // simulate a partial second frame
|
||||||
|
|
||||||
|
let (payload, rest) = decode_frame(&wire).unwrap();
|
||||||
|
assert_eq!(payload, &[1, 2, 3]);
|
||||||
|
assert_eq!(rest, &[9, 9, 9]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_frame_short_header() {
|
||||||
|
let err = decode_frame(&[0x00, 0x00]).unwrap_err();
|
||||||
|
assert!(matches!(err, ProtoError::Malformed(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_frame_truncated_body() {
|
||||||
|
let err = decode_frame(&[0x00, 0x00, 0x00, 0x05, 0xAA]).unwrap_err();
|
||||||
|
assert!(matches!(err, ProtoError::Malformed(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
471
vendor/smb-server/src/proto/header.rs
vendored
Normal file
471
vendor/smb-server/src/proto/header.rs
vendored
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
//! SMB2 fixed 64-byte packet header (sync + async forms).
|
||||||
|
//!
|
||||||
|
//! References:
|
||||||
|
//! * MS-SMB2 §2.2.1 — Common header preamble.
|
||||||
|
//! * MS-SMB2 §2.2.1.1 — Async form (`Flags & SMB2_FLAGS_ASYNC_COMMAND`).
|
||||||
|
//! * MS-SMB2 §2.2.1.2 — Sync form.
|
||||||
|
//!
|
||||||
|
//! ## Encoding choice
|
||||||
|
//!
|
||||||
|
//! The two forms differ only in the 12-byte block at offset 0x18..0x24:
|
||||||
|
//!
|
||||||
|
//! * **Sync**: `ChannelSequence` (u16) + `Reserved` (u16) + `Reserved2` (u32) + `TreeId` (u32)
|
||||||
|
//! wait — actually the sync form is: `Reserved` (u32) + `TreeId` (u32) (bytes 0x20..0x28).
|
||||||
|
//! * **Async**: `AsyncId` (u64) at bytes 0x20..0x28.
|
||||||
|
//!
|
||||||
|
//! In *both* forms, bytes 0x10..0x14 are `Status` (or `ChannelSequence + Reserved` on
|
||||||
|
//! 3.x channel-sequence-aware requests; we treat them as a single u32 named
|
||||||
|
//! `channel_sequence_status`). Bytes 0x14..0x18 are `Command + CreditReqResp`,
|
||||||
|
//! 0x18..0x1C are `Flags`, 0x1C..0x20 are `NextCommand`, 0x20..0x28 are `MessageId`.
|
||||||
|
//! The discriminated 8-byte block lives at 0x28..0x30, followed by the 16-byte
|
||||||
|
//! `Signature` at 0x30..0x40 — totalling 64 bytes.
|
||||||
|
//!
|
||||||
|
//! We model this as a single `Smb2Header` struct with a `tail: HeaderTail` enum
|
||||||
|
//! that is `Sync { reserved: u32, tree_id: u32 }` or `Async { async_id: u64 }`,
|
||||||
|
//! discriminated by `Flags & SMB2_FLAGS_ASYNC_COMMAND`. This is the cleanest
|
||||||
|
//! mapping to the spec — every other field is shared.
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::{ProtoError, ProtoResult};
|
||||||
|
|
||||||
|
/// SMB2 protocol identifier ("\xfeSMB").
|
||||||
|
pub const SMB2_MAGIC: [u8; 4] = [0xFE, b'S', b'M', b'B'];
|
||||||
|
|
||||||
|
/// Fixed `StructureSize` of the SMB2 header (MS-SMB2 §2.2.1.1/§2.2.1.2).
|
||||||
|
pub const SMB2_HEADER_STRUCTURE_SIZE: u16 = 64;
|
||||||
|
|
||||||
|
/// Total wire size of the SMB2 header.
|
||||||
|
pub const SMB2_HEADER_LEN: usize = 64;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Flags (MS-SMB2 §2.2.1.2 Flags field)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// `SMB2_FLAGS_SERVER_TO_REDIR` — set on responses.
|
||||||
|
pub const SMB2_FLAGS_SERVER_TO_REDIR: u32 = 0x0000_0001;
|
||||||
|
/// `SMB2_FLAGS_ASYNC_COMMAND` — selects the async header form.
|
||||||
|
pub const SMB2_FLAGS_ASYNC_COMMAND: u32 = 0x0000_0002;
|
||||||
|
/// `SMB2_FLAGS_RELATED_OPERATIONS` — compound chain marker.
|
||||||
|
pub const SMB2_FLAGS_RELATED_OPERATIONS: u32 = 0x0000_0004;
|
||||||
|
/// `SMB2_FLAGS_SIGNED` — message is signed.
|
||||||
|
pub const SMB2_FLAGS_SIGNED: u32 = 0x0000_0008;
|
||||||
|
/// `SMB2_FLAGS_PRIORITY_MASK` — bits 4..6 hold priority (3.1.1+).
|
||||||
|
pub const SMB2_FLAGS_PRIORITY_MASK: u32 = 0x0000_0070;
|
||||||
|
/// `SMB2_FLAGS_DFS_OPERATIONS`.
|
||||||
|
pub const SMB2_FLAGS_DFS_OPERATIONS: u32 = 0x1000_0000;
|
||||||
|
/// `SMB2_FLAGS_REPLAY_OPERATION`.
|
||||||
|
pub const SMB2_FLAGS_REPLAY_OPERATION: u32 = 0x2000_0000;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Command opcodes (MS-SMB2 §2.2.1.2 Command field)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// SMB2 command opcodes (the 19 commands in v1).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little, repr = u16)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Command {
|
||||||
|
Negotiate = 0x0000,
|
||||||
|
SessionSetup = 0x0001,
|
||||||
|
Logoff = 0x0002,
|
||||||
|
TreeConnect = 0x0003,
|
||||||
|
TreeDisconnect = 0x0004,
|
||||||
|
Create = 0x0005,
|
||||||
|
Close = 0x0006,
|
||||||
|
Flush = 0x0007,
|
||||||
|
Read = 0x0008,
|
||||||
|
Write = 0x0009,
|
||||||
|
Lock = 0x000A,
|
||||||
|
Ioctl = 0x000B,
|
||||||
|
Cancel = 0x000C,
|
||||||
|
Echo = 0x000D,
|
||||||
|
QueryDirectory = 0x000E,
|
||||||
|
ChangeNotify = 0x000F,
|
||||||
|
QueryInfo = 0x0010,
|
||||||
|
SetInfo = 0x0011,
|
||||||
|
OplockBreak = 0x0012,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
/// Raw opcode for diagnostics.
|
||||||
|
pub const fn as_u16(self) -> u16 {
|
||||||
|
self as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Header struct
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// The 12-byte tail of the header that differs between sync and async forms.
|
||||||
|
///
|
||||||
|
/// The discriminant is `flags & SMB2_FLAGS_ASYNC_COMMAND`. We can't easily use
|
||||||
|
/// binrw's args+if without making the parent struct generic over the runtime
|
||||||
|
/// flag value, so the parent reads/writes this manually via `parse` / `write`
|
||||||
|
/// helpers and we expose a regular Rust enum here.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum HeaderTail {
|
||||||
|
/// Sync form: `Reserved (u32)` + `TreeId (u32)` at bytes 0x24..0x2C.
|
||||||
|
/// (See note in module docs about offsets.)
|
||||||
|
Sync { reserved: u32, tree_id: u32 },
|
||||||
|
/// Async form: `AsyncId (u64)` at bytes 0x24..0x2C.
|
||||||
|
Async { async_id: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeaderTail {
|
||||||
|
/// Default sync tail with `TreeId = 0`.
|
||||||
|
pub const fn sync(tree_id: u32) -> Self {
|
||||||
|
HeaderTail::Sync {
|
||||||
|
reserved: 0,
|
||||||
|
tree_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default async tail.
|
||||||
|
pub const fn async_(async_id: u64) -> Self {
|
||||||
|
HeaderTail::Async { async_id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2 fixed 64-byte header.
|
||||||
|
///
|
||||||
|
/// On the wire the layout is (offsets in decimal — total 64 bytes):
|
||||||
|
///
|
||||||
|
/// | Offset | Size | Field |
|
||||||
|
/// |-------:|-----:|-------|
|
||||||
|
/// | 0 | 4 | ProtocolId (`0xFE 'S' 'M' 'B'`) |
|
||||||
|
/// | 4 | 2 | StructureSize (always 64) |
|
||||||
|
/// | 6 | 2 | CreditCharge |
|
||||||
|
/// | 8 | 4 | (Channel)Status |
|
||||||
|
/// | 12 | 2 | Command |
|
||||||
|
/// | 14 | 2 | CreditRequest/CreditResponse |
|
||||||
|
/// | 16 | 4 | Flags |
|
||||||
|
/// | 20 | 4 | NextCommand |
|
||||||
|
/// | 24 | 8 | MessageId |
|
||||||
|
/// | 32 | 8 | Reserved/TreeId (sync) **or** AsyncId (async) |
|
||||||
|
/// | 40 | 8 | SessionId |
|
||||||
|
/// | 48 | 16 | Signature |
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Smb2Header {
|
||||||
|
pub credit_charge: u16,
|
||||||
|
/// Bytes 8..12: in client→server requests on 3.x this can split into
|
||||||
|
/// `ChannelSequence(u16)` + `Reserved(u16)`; in server→client responses
|
||||||
|
/// it carries `Status` (NTSTATUS). We expose the raw u32 — handlers/
|
||||||
|
/// signing code interpret it.
|
||||||
|
pub channel_sequence_status: u32,
|
||||||
|
pub command: Command,
|
||||||
|
/// On requests this is `CreditRequest`; on responses, `CreditResponse`.
|
||||||
|
pub credit_request_response: u16,
|
||||||
|
pub flags: u32,
|
||||||
|
/// Offset to the next header in a compound chain, or 0 for the last.
|
||||||
|
pub next_command: u32,
|
||||||
|
pub message_id: u64,
|
||||||
|
/// Sync: `(reserved, tree_id)`. Async: `async_id`. Discriminated by
|
||||||
|
/// `flags & SMB2_FLAGS_ASYNC_COMMAND`.
|
||||||
|
pub tail: HeaderTail,
|
||||||
|
pub session_id: u64,
|
||||||
|
/// 16-byte signature; zeroed on unsigned messages.
|
||||||
|
pub signature: [u8; 16],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Smb2Header {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
credit_charge: 0,
|
||||||
|
channel_sequence_status: 0,
|
||||||
|
command: Command::Negotiate,
|
||||||
|
credit_request_response: 0,
|
||||||
|
flags: 0,
|
||||||
|
next_command: 0,
|
||||||
|
message_id: 0,
|
||||||
|
tail: HeaderTail::sync(0),
|
||||||
|
session_id: 0,
|
||||||
|
signature: [0u8; 16],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Smb2Header {
|
||||||
|
/// Convenience: is this an async-form header?
|
||||||
|
pub fn is_async(&self) -> bool {
|
||||||
|
self.flags & SMB2_FLAGS_ASYNC_COMMAND != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: is this a server→client response?
|
||||||
|
pub fn is_response(&self) -> bool {
|
||||||
|
self.flags & SMB2_FLAGS_SERVER_TO_REDIR != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: tree_id from a sync header (panics if async).
|
||||||
|
pub fn tree_id(&self) -> Option<u32> {
|
||||||
|
match self.tail {
|
||||||
|
HeaderTail::Sync { tree_id, .. } => Some(tree_id),
|
||||||
|
HeaderTail::Async { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: async_id from an async header.
|
||||||
|
pub fn async_id(&self) -> Option<u64> {
|
||||||
|
match self.tail {
|
||||||
|
HeaderTail::Async { async_id } => Some(async_id),
|
||||||
|
HeaderTail::Sync { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse from a byte slice. Returns the header and the remaining bytes.
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<(Self, &[u8])> {
|
||||||
|
if buf.len() < SMB2_HEADER_LEN {
|
||||||
|
return Err(ProtoError::Malformed("short SMB2 header"));
|
||||||
|
}
|
||||||
|
let mut cursor = Cursor::new(&buf[..SMB2_HEADER_LEN]);
|
||||||
|
let raw = RawHeader::read(&mut cursor)?;
|
||||||
|
if raw.protocol_id != SMB2_MAGIC {
|
||||||
|
return Err(ProtoError::Malformed("bad SMB2 magic"));
|
||||||
|
}
|
||||||
|
if raw.structure_size != SMB2_HEADER_STRUCTURE_SIZE {
|
||||||
|
return Err(ProtoError::Malformed("SMB2 header structure_size != 64"));
|
||||||
|
}
|
||||||
|
let command = match Command::read_le(&mut Cursor::new(raw.command_raw.to_le_bytes())) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(ProtoError::Malformed("unknown SMB2 command opcode"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let tail = if raw.flags & SMB2_FLAGS_ASYNC_COMMAND != 0 {
|
||||||
|
HeaderTail::Async {
|
||||||
|
async_id: u64::from_le_bytes(raw.tail_bytes),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let reserved = u32::from_le_bytes([
|
||||||
|
raw.tail_bytes[0],
|
||||||
|
raw.tail_bytes[1],
|
||||||
|
raw.tail_bytes[2],
|
||||||
|
raw.tail_bytes[3],
|
||||||
|
]);
|
||||||
|
let tree_id = u32::from_le_bytes([
|
||||||
|
raw.tail_bytes[4],
|
||||||
|
raw.tail_bytes[5],
|
||||||
|
raw.tail_bytes[6],
|
||||||
|
raw.tail_bytes[7],
|
||||||
|
]);
|
||||||
|
HeaderTail::Sync { reserved, tree_id }
|
||||||
|
};
|
||||||
|
Ok((
|
||||||
|
Smb2Header {
|
||||||
|
credit_charge: raw.credit_charge,
|
||||||
|
channel_sequence_status: raw.channel_sequence_status,
|
||||||
|
command,
|
||||||
|
credit_request_response: raw.credit_request_response,
|
||||||
|
flags: raw.flags,
|
||||||
|
next_command: raw.next_command,
|
||||||
|
message_id: raw.message_id,
|
||||||
|
tail,
|
||||||
|
session_id: raw.session_id,
|
||||||
|
signature: raw.signature,
|
||||||
|
},
|
||||||
|
&buf[SMB2_HEADER_LEN..],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize the 64-byte header into `out`.
|
||||||
|
pub fn write(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let tail_bytes = match self.tail {
|
||||||
|
HeaderTail::Sync { reserved, tree_id } => {
|
||||||
|
let mut b = [0u8; 8];
|
||||||
|
b[..4].copy_from_slice(&reserved.to_le_bytes());
|
||||||
|
b[4..].copy_from_slice(&tree_id.to_le_bytes());
|
||||||
|
b
|
||||||
|
}
|
||||||
|
HeaderTail::Async { async_id } => async_id.to_le_bytes(),
|
||||||
|
};
|
||||||
|
let raw = RawHeader {
|
||||||
|
protocol_id: SMB2_MAGIC,
|
||||||
|
structure_size: SMB2_HEADER_STRUCTURE_SIZE,
|
||||||
|
credit_charge: self.credit_charge,
|
||||||
|
channel_sequence_status: self.channel_sequence_status,
|
||||||
|
command_raw: self.command.as_u16(),
|
||||||
|
credit_request_response: self.credit_request_response,
|
||||||
|
flags: self.flags,
|
||||||
|
next_command: self.next_command,
|
||||||
|
message_id: self.message_id,
|
||||||
|
tail_bytes,
|
||||||
|
session_id: self.session_id,
|
||||||
|
signature: self.signature,
|
||||||
|
};
|
||||||
|
let start = out.len();
|
||||||
|
let mut cursor = Cursor::new(Vec::with_capacity(SMB2_HEADER_LEN));
|
||||||
|
raw.write(&mut cursor)?;
|
||||||
|
out.extend_from_slice(&cursor.into_inner());
|
||||||
|
debug_assert_eq!(out.len() - start, SMB2_HEADER_LEN);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal raw header for binrw plumbing.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct RawHeader {
|
||||||
|
protocol_id: [u8; 4],
|
||||||
|
structure_size: u16,
|
||||||
|
credit_charge: u16,
|
||||||
|
channel_sequence_status: u32,
|
||||||
|
command_raw: u16,
|
||||||
|
credit_request_response: u16,
|
||||||
|
flags: u32,
|
||||||
|
next_command: u32,
|
||||||
|
message_id: u64,
|
||||||
|
tail_bytes: [u8; 8],
|
||||||
|
session_id: u64,
|
||||||
|
signature: [u8; 16],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn sample_sync() -> Smb2Header {
|
||||||
|
Smb2Header {
|
||||||
|
credit_charge: 1,
|
||||||
|
channel_sequence_status: 0,
|
||||||
|
command: Command::Negotiate,
|
||||||
|
credit_request_response: 1,
|
||||||
|
flags: 0,
|
||||||
|
next_command: 0,
|
||||||
|
message_id: 0,
|
||||||
|
tail: HeaderTail::Sync {
|
||||||
|
reserved: 0,
|
||||||
|
tree_id: 0,
|
||||||
|
},
|
||||||
|
session_id: 0,
|
||||||
|
signature: [0u8; 16],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_async() -> Smb2Header {
|
||||||
|
Smb2Header {
|
||||||
|
credit_charge: 4,
|
||||||
|
channel_sequence_status: 0,
|
||||||
|
command: Command::Read,
|
||||||
|
credit_request_response: 1,
|
||||||
|
flags: SMB2_FLAGS_ASYNC_COMMAND | SMB2_FLAGS_SERVER_TO_REDIR,
|
||||||
|
next_command: 0,
|
||||||
|
message_id: 42,
|
||||||
|
tail: HeaderTail::Async {
|
||||||
|
async_id: 0xDEAD_BEEF_CAFE_F00D,
|
||||||
|
},
|
||||||
|
session_id: 0x1122_3344_5566_7788,
|
||||||
|
signature: [0xAA; 16],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_round_trips() {
|
||||||
|
let hdr = sample_sync();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
hdr.write(&mut buf).unwrap();
|
||||||
|
assert_eq!(buf.len(), SMB2_HEADER_LEN);
|
||||||
|
// First 4 bytes must be the magic.
|
||||||
|
assert_eq!(&buf[..4], &SMB2_MAGIC);
|
||||||
|
// StructureSize at offset 4 == 64
|
||||||
|
assert_eq!(u16::from_le_bytes([buf[4], buf[5]]), 64);
|
||||||
|
|
||||||
|
let (decoded, rest) = Smb2Header::parse(&buf).unwrap();
|
||||||
|
assert!(rest.is_empty());
|
||||||
|
assert_eq!(decoded, hdr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn async_round_trips() {
|
||||||
|
let hdr = sample_async();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
hdr.write(&mut buf).unwrap();
|
||||||
|
assert_eq!(buf.len(), SMB2_HEADER_LEN);
|
||||||
|
|
||||||
|
let (decoded, _rest) = Smb2Header::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, hdr);
|
||||||
|
assert!(decoded.is_async());
|
||||||
|
assert!(decoded.is_response());
|
||||||
|
assert_eq!(decoded.async_id(), Some(0xDEAD_BEEF_CAFE_F00D));
|
||||||
|
assert_eq!(decoded.tree_id(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_bad_magic() {
|
||||||
|
let hdr = sample_sync();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
hdr.write(&mut buf).unwrap();
|
||||||
|
buf[0] = 0xFF;
|
||||||
|
let err = Smb2Header::parse(&buf).unwrap_err();
|
||||||
|
assert!(matches!(err, ProtoError::Malformed(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_bad_structure_size() {
|
||||||
|
let hdr = sample_sync();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
hdr.write(&mut buf).unwrap();
|
||||||
|
buf[4] = 0; // wreck the structure_size LE bytes
|
||||||
|
buf[5] = 0;
|
||||||
|
let err = Smb2Header::parse(&buf).unwrap_err();
|
||||||
|
assert!(matches!(err, ProtoError::Malformed(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_short_buffer() {
|
||||||
|
let err = Smb2Header::parse(&[0u8; 32]).unwrap_err();
|
||||||
|
assert!(matches!(err, ProtoError::Malformed(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handcrafted_sync_negotiate_request() {
|
||||||
|
// Hand-built Sync NEGOTIATE request header: magic, size=64, no flags,
|
||||||
|
// command=0, mid=0, tree_id=0, sid=0, no signature.
|
||||||
|
let mut buf = vec![0u8; 64];
|
||||||
|
buf[..4].copy_from_slice(&SMB2_MAGIC);
|
||||||
|
buf[4..6].copy_from_slice(&64u16.to_le_bytes());
|
||||||
|
// command at offset 12 = 0 (NEGOTIATE), already zero
|
||||||
|
// everything else zero
|
||||||
|
let (hdr, _) = Smb2Header::parse(&buf).unwrap();
|
||||||
|
assert_eq!(hdr.command, Command::Negotiate);
|
||||||
|
assert!(!hdr.is_async());
|
||||||
|
assert_eq!(hdr.tree_id(), Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_round_trips_via_binrw() {
|
||||||
|
for cmd in [
|
||||||
|
Command::Negotiate,
|
||||||
|
Command::SessionSetup,
|
||||||
|
Command::Logoff,
|
||||||
|
Command::TreeConnect,
|
||||||
|
Command::TreeDisconnect,
|
||||||
|
Command::Create,
|
||||||
|
Command::Close,
|
||||||
|
Command::Flush,
|
||||||
|
Command::Read,
|
||||||
|
Command::Write,
|
||||||
|
Command::Lock,
|
||||||
|
Command::Ioctl,
|
||||||
|
Command::Cancel,
|
||||||
|
Command::Echo,
|
||||||
|
Command::QueryDirectory,
|
||||||
|
Command::ChangeNotify,
|
||||||
|
Command::QueryInfo,
|
||||||
|
Command::SetInfo,
|
||||||
|
Command::OplockBreak,
|
||||||
|
] {
|
||||||
|
let mut hdr = sample_sync();
|
||||||
|
hdr.command = cmd;
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
hdr.write(&mut buf).unwrap();
|
||||||
|
let (decoded, _) = Smb2Header::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded.command, cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
vendor/smb-server/src/proto/messages/cancel.rs
vendored
Normal file
49
vendor/smb-server/src/proto/messages/cancel.rs
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//! CANCEL Request (MS-SMB2 §2.2.30). No response — server cancels in place.
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CancelRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CancelRequest {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 4,
|
||||||
|
reserved: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CancelRequest {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips() {
|
||||||
|
let r = CancelRequest::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(buf.len(), 4);
|
||||||
|
assert_eq!(CancelRequest::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
vendor/smb-server/src/proto/messages/change_notify.rs
vendored
Normal file
93
vendor/smb-server/src/proto/messages/change_notify.rs
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
//! CHANGE_NOTIFY Request/Response (MS-SMB2 §2.2.35 / §2.2.36).
|
||||||
|
//!
|
||||||
|
//! V1 returns `STATUS_NOT_SUPPORTED`, but we still parse/encode the wire
|
||||||
|
//! form so the dispatcher can recognize it.
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ChangeNotifyRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub flags: u16,
|
||||||
|
pub output_buffer_length: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub completion_filter: u32,
|
||||||
|
pub reserved: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangeNotifyRequest {
|
||||||
|
/// Flag: SMB2_WATCH_TREE.
|
||||||
|
pub const FLAG_WATCH_TREE: u16 = 0x0001;
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ChangeNotifyResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub output_buffer_offset: u16,
|
||||||
|
pub output_buffer_length: u32,
|
||||||
|
#[br(count = output_buffer_length as usize)]
|
||||||
|
pub buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChangeNotifyResponse {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let r = ChangeNotifyRequest {
|
||||||
|
structure_size: 32,
|
||||||
|
flags: ChangeNotifyRequest::FLAG_WATCH_TREE,
|
||||||
|
output_buffer_length: 0x1000,
|
||||||
|
file_id: FileId::new(1, 2),
|
||||||
|
completion_filter: 0xFF,
|
||||||
|
reserved: 0,
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(ChangeNotifyRequest::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = ChangeNotifyResponse {
|
||||||
|
structure_size: 9,
|
||||||
|
output_buffer_offset: 0x48,
|
||||||
|
output_buffer_length: 0,
|
||||||
|
buffer: vec![],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(ChangeNotifyResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
vendor/smb-server/src/proto/messages/close.rs
vendored
Normal file
93
vendor/smb-server/src/proto/messages/close.rs
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
//! CLOSE Request/Response (MS-SMB2 §2.2.15 / §2.2.16).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CloseRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub flags: u16,
|
||||||
|
pub reserved: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloseRequest {
|
||||||
|
/// Flag: SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB.
|
||||||
|
pub const FLAG_POSTQUERY_ATTRIB: u16 = 0x0001;
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct CloseResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub flags: u16,
|
||||||
|
pub reserved: u32,
|
||||||
|
pub creation_time: u64,
|
||||||
|
pub last_access_time: u64,
|
||||||
|
pub last_write_time: u64,
|
||||||
|
pub change_time: u64,
|
||||||
|
pub allocation_size: u64,
|
||||||
|
pub end_of_file: u64,
|
||||||
|
pub file_attributes: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloseResponse {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 60,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips() {
|
||||||
|
let r = CloseRequest {
|
||||||
|
structure_size: 24,
|
||||||
|
flags: CloseRequest::FLAG_POSTQUERY_ATTRIB,
|
||||||
|
reserved: 0,
|
||||||
|
file_id: FileId::new(0x1, 0x2),
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(CloseRequest::parse(&buf).unwrap(), r);
|
||||||
|
|
||||||
|
let r = CloseResponse {
|
||||||
|
structure_size: 60,
|
||||||
|
..CloseResponse::new()
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(CloseResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
437
vendor/smb-server/src/proto/messages/create.rs
vendored
Normal file
437
vendor/smb-server/src/proto/messages/create.rs
vendored
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
//! CREATE Request/Response (MS-SMB2 §2.2.13 / §2.2.14).
|
||||||
|
//!
|
||||||
|
//! `create_contexts` is a chained sequence of `SMB2_CREATE_CONTEXT` records
|
||||||
|
//! (MS-SMB2 §2.2.13.2). Each record has `Next` (offset to the next entry,
|
||||||
|
//! relative to the start of *this* entry; 0 marks the last), a name + data
|
||||||
|
//! pair, and 8-byte alignment.
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::{ProtoError, ProtoResult};
|
||||||
|
|
||||||
|
/// SMB2 FileId — opaque 16 bytes (volatile + persistent).
|
||||||
|
///
|
||||||
|
/// MS-SMB2 §2.2.14.1. We expose both halves; the server uses identical values
|
||||||
|
/// for both since durable handles are out of scope (spec §2 in the v1 design).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct FileId {
|
||||||
|
pub persistent: u64,
|
||||||
|
pub volatile: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileId {
|
||||||
|
pub const fn new(persistent: u64, volatile: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
persistent,
|
||||||
|
volatile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MS-SMB2: the "any" FileId is `0xFFFF…FFFF`.
|
||||||
|
pub const fn any() -> Self {
|
||||||
|
Self {
|
||||||
|
persistent: u64::MAX,
|
||||||
|
volatile: u64::MAX,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.13 CREATE Request — fixed prefix.
|
||||||
|
///
|
||||||
|
/// Variable-length tail: the file `name` (UTF-16LE) and `create_contexts`
|
||||||
|
/// blob, each at absolute offsets from the start of the SMB2 header. We hold
|
||||||
|
/// them as length-counted byte buffers immediately following the fixed
|
||||||
|
/// portion. The server crate parses contexts with [`CreateContext::parse_chain`].
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CreateRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub security_flags: u8,
|
||||||
|
pub requested_oplock_level: u8,
|
||||||
|
pub impersonation_level: u32,
|
||||||
|
pub smb_create_flags: u64,
|
||||||
|
pub reserved: u64,
|
||||||
|
pub desired_access: u32,
|
||||||
|
pub file_attributes: u32,
|
||||||
|
pub share_access: u32,
|
||||||
|
pub create_disposition: u32,
|
||||||
|
pub create_options: u32,
|
||||||
|
pub name_offset: u16,
|
||||||
|
pub name_length: u16,
|
||||||
|
pub create_contexts_offset: u32,
|
||||||
|
pub create_contexts_length: u32,
|
||||||
|
/// UTF-16LE filename.
|
||||||
|
#[br(count = name_length as usize)]
|
||||||
|
pub name: Vec<u8>,
|
||||||
|
/// Raw create-contexts chain bytes; parse with
|
||||||
|
/// [`CreateContext::parse_chain`].
|
||||||
|
#[br(count = create_contexts_length as usize)]
|
||||||
|
pub create_contexts: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateRequest {
|
||||||
|
/// Decode the UTF-16LE filename.
|
||||||
|
pub fn name_str(&self) -> Option<String> {
|
||||||
|
if !self.name.len().is_multiple_of(2) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let units: Vec<u16> = self
|
||||||
|
.name
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||||
|
.collect();
|
||||||
|
Some(String::from_utf16_lossy(&units))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.14 CREATE Response.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CreateResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub oplock_level: u8,
|
||||||
|
pub flags: u8,
|
||||||
|
pub create_action: u32,
|
||||||
|
pub creation_time: u64,
|
||||||
|
pub last_access_time: u64,
|
||||||
|
pub last_write_time: u64,
|
||||||
|
pub change_time: u64,
|
||||||
|
pub allocation_size: u64,
|
||||||
|
pub end_of_file: u64,
|
||||||
|
pub file_attributes: u32,
|
||||||
|
pub reserved2: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub create_contexts_offset: u32,
|
||||||
|
pub create_contexts_length: u32,
|
||||||
|
#[br(count = create_contexts_length as usize)]
|
||||||
|
pub create_contexts: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateResponse {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Create contexts (MS-SMB2 §2.2.13.2)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Generic SMB2_CREATE_CONTEXT envelope.
|
||||||
|
///
|
||||||
|
/// Per MS-SMB2 §2.2.13.2 each entry has:
|
||||||
|
/// * `Next` — offset (bytes) from the start of *this* entry to the start of
|
||||||
|
/// the next entry in the chain, or 0 for the last entry.
|
||||||
|
/// * `NameOffset`/`NameLength` — name (typically a 4-byte ASCII tag) at an
|
||||||
|
/// offset relative to the entry start.
|
||||||
|
/// * `Reserved` — 2 bytes.
|
||||||
|
/// * `DataOffset`/`DataLength` — payload at an offset relative to the entry
|
||||||
|
/// start.
|
||||||
|
///
|
||||||
|
/// We model the entry as `name` + `data` byte vectors plus the raw flags. The
|
||||||
|
/// chain reader / writer below handles `Next` and 8-byte alignment between
|
||||||
|
/// entries.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct CreateContext {
|
||||||
|
pub name: Vec<u8>,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateContext {
|
||||||
|
// Well-known names (MS-SMB2 §2.2.13.2 table). 4-byte ASCII tags.
|
||||||
|
pub const NAME_EXTA: &'static [u8; 4] = b"ExtA"; // SMB2_CREATE_EA_BUFFER
|
||||||
|
pub const NAME_SECD: &'static [u8; 4] = b"SecD"; // SMB2_CREATE_SD_BUFFER
|
||||||
|
pub const NAME_DHNQ: &'static [u8; 4] = b"DHnQ"; // DURABLE_HANDLE_REQUEST
|
||||||
|
pub const NAME_DHNC: &'static [u8; 4] = b"DHnC"; // DURABLE_HANDLE_RECONNECT
|
||||||
|
pub const NAME_ALSI: &'static [u8; 4] = b"AlSi"; // ALLOCATION_SIZE
|
||||||
|
pub const NAME_MXAC: &'static [u8; 4] = b"MxAc"; // QUERY_MAXIMAL_ACCESS
|
||||||
|
pub const NAME_TWRP: &'static [u8; 4] = b"TWrp"; // TIMEWARP_TOKEN
|
||||||
|
pub const NAME_QFID: &'static [u8; 4] = b"QFid"; // QUERY_ON_DISK_ID
|
||||||
|
pub const NAME_RQLS: &'static [u8; 4] = b"RqLs"; // REQUEST_LEASE
|
||||||
|
pub const NAME_DH2Q: &'static [u8; 4] = b"DH2Q"; // DURABLE_HANDLE_REQUEST_V2
|
||||||
|
pub const NAME_DH2C: &'static [u8; 4] = b"DH2C"; // DURABLE_HANDLE_RECONNECT_V2
|
||||||
|
|
||||||
|
/// Parse a chain of create-contexts from the raw chain bytes.
|
||||||
|
///
|
||||||
|
/// The chain is empty if `chain.is_empty()`. Otherwise we walk `Next`
|
||||||
|
/// offsets until we hit a zero terminator, validating bounds at each step.
|
||||||
|
pub fn parse_chain(chain: &[u8]) -> ProtoResult<Vec<CreateContext>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if chain.is_empty() {
|
||||||
|
return Ok(out);
|
||||||
|
}
|
||||||
|
let mut cursor_off = 0usize;
|
||||||
|
loop {
|
||||||
|
let entry = &chain
|
||||||
|
.get(cursor_off..)
|
||||||
|
.ok_or(ProtoError::Malformed("create context out of range"))?;
|
||||||
|
if entry.len() < 16 {
|
||||||
|
return Err(ProtoError::Malformed("create context too short"));
|
||||||
|
}
|
||||||
|
let next = u32::from_le_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
|
||||||
|
let name_offset = u16::from_le_bytes([entry[4], entry[5]]) as usize;
|
||||||
|
let name_length = u16::from_le_bytes([entry[6], entry[7]]) as usize;
|
||||||
|
// entry[8..10] = reserved
|
||||||
|
let data_offset = u16::from_le_bytes([entry[10], entry[11]]) as usize;
|
||||||
|
let data_length =
|
||||||
|
u32::from_le_bytes([entry[12], entry[13], entry[14], entry[15]]) as usize;
|
||||||
|
|
||||||
|
let name = entry
|
||||||
|
.get(name_offset..name_offset + name_length)
|
||||||
|
.ok_or(ProtoError::Malformed("create context name out of range"))?
|
||||||
|
.to_vec();
|
||||||
|
let data = if data_length == 0 {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
entry
|
||||||
|
.get(data_offset..data_offset + data_length)
|
||||||
|
.ok_or(ProtoError::Malformed("create context data out of range"))?
|
||||||
|
.to_vec()
|
||||||
|
};
|
||||||
|
out.push(CreateContext { name, data });
|
||||||
|
|
||||||
|
if next == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor_off = cursor_off
|
||||||
|
.checked_add(next)
|
||||||
|
.ok_or(ProtoError::Malformed("create context next overflow"))?;
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a chain of create-contexts into `out`. Inserts `Next` offsets
|
||||||
|
/// and 8-byte alignment padding between entries.
|
||||||
|
pub fn encode_chain(list: &[CreateContext], out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
if list.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
// We build the chain in a scratch buffer, then copy. Each entry is:
|
||||||
|
// 16-byte header + name + (pad to 8) + data + (pad to 8 if not last)
|
||||||
|
// The `Next` of every entry except the last is the size from this
|
||||||
|
// entry's start to the next entry's start.
|
||||||
|
let mut scratch: Vec<u8> = Vec::new();
|
||||||
|
let mut entry_starts: Vec<usize> = Vec::with_capacity(list.len());
|
||||||
|
|
||||||
|
for (i, ctx) in list.iter().enumerate() {
|
||||||
|
// Pad to 8-byte boundary before each entry (except possibly first
|
||||||
|
// — but contexts must be 8-byte aligned, and the chain itself is
|
||||||
|
// anchored at an 8-aligned offset by the server).
|
||||||
|
while !scratch.len().is_multiple_of(8) {
|
||||||
|
scratch.push(0);
|
||||||
|
}
|
||||||
|
entry_starts.push(scratch.len());
|
||||||
|
|
||||||
|
// Reserve 16 bytes for the header; will fill in once we know
|
||||||
|
// the actual offsets.
|
||||||
|
let header_pos = scratch.len();
|
||||||
|
scratch.extend_from_slice(&[0u8; 16]);
|
||||||
|
|
||||||
|
// Name immediately follows the header.
|
||||||
|
let name_offset_rel = (scratch.len() - header_pos) as u16;
|
||||||
|
scratch.extend_from_slice(&ctx.name);
|
||||||
|
// Pad to 8 before data.
|
||||||
|
while !(scratch.len() - header_pos).is_multiple_of(8) {
|
||||||
|
scratch.push(0);
|
||||||
|
}
|
||||||
|
let data_offset_rel = (scratch.len() - header_pos) as u16;
|
||||||
|
scratch.extend_from_slice(&ctx.data);
|
||||||
|
|
||||||
|
// Now backfill the header bytes (Next is patched after the loop).
|
||||||
|
let hdr = &mut scratch[header_pos..header_pos + 16];
|
||||||
|
hdr[0..4].copy_from_slice(&0u32.to_le_bytes()); // Next, fixed up below
|
||||||
|
hdr[4..6].copy_from_slice(&name_offset_rel.to_le_bytes());
|
||||||
|
hdr[6..8].copy_from_slice(&(ctx.name.len() as u16).to_le_bytes());
|
||||||
|
hdr[8..10].copy_from_slice(&0u16.to_le_bytes()); // Reserved
|
||||||
|
hdr[10..12].copy_from_slice(&data_offset_rel.to_le_bytes());
|
||||||
|
hdr[12..16].copy_from_slice(&(ctx.data.len() as u32).to_le_bytes());
|
||||||
|
|
||||||
|
// For non-last, pad the trailing data area to 8 so the next
|
||||||
|
// entry starts aligned.
|
||||||
|
if i + 1 < list.len() {
|
||||||
|
while !scratch.len().is_multiple_of(8) {
|
||||||
|
scratch.push(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch `Next` offsets.
|
||||||
|
for i in 0..(entry_starts.len() - 1) {
|
||||||
|
let this = entry_starts[i];
|
||||||
|
let next = entry_starts[i + 1];
|
||||||
|
let delta = (next - this) as u32;
|
||||||
|
scratch[this..this + 4].copy_from_slice(&delta.to_le_bytes());
|
||||||
|
}
|
||||||
|
// Last entry's Next stays 0.
|
||||||
|
|
||||||
|
out.extend_from_slice(&scratch);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper enums (oplock level, impersonation level)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.13 RequestedOplockLevel / §2.2.14 OplockLevel.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum OplockLevel {
|
||||||
|
None = 0x00,
|
||||||
|
Ii = 0x01,
|
||||||
|
Exclusive = 0x08,
|
||||||
|
Batch = 0x09,
|
||||||
|
Lease = 0xFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OplockLevel {
|
||||||
|
pub fn from_u8(v: u8) -> Option<Self> {
|
||||||
|
Some(match v {
|
||||||
|
0x00 => Self::None,
|
||||||
|
0x01 => Self::Ii,
|
||||||
|
0x08 => Self::Exclusive,
|
||||||
|
0x09 => Self::Batch,
|
||||||
|
0xFF => Self::Lease,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.13 ImpersonationLevel.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum ImpersonationLevel {
|
||||||
|
Anonymous = 0x0000_0000,
|
||||||
|
Identification = 0x0000_0001,
|
||||||
|
Impersonation = 0x0000_0002,
|
||||||
|
Delegate = 0x0000_0003,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn utf16le(s: &str) -> Vec<u8> {
|
||||||
|
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let name = utf16le("dir\\file.txt");
|
||||||
|
let r = CreateRequest {
|
||||||
|
structure_size: 57,
|
||||||
|
security_flags: 0,
|
||||||
|
requested_oplock_level: 0,
|
||||||
|
impersonation_level: ImpersonationLevel::Impersonation as u32,
|
||||||
|
smb_create_flags: 0,
|
||||||
|
reserved: 0,
|
||||||
|
desired_access: 0x0012_0089,
|
||||||
|
file_attributes: 0,
|
||||||
|
share_access: 0x0000_0007,
|
||||||
|
create_disposition: 1,
|
||||||
|
create_options: 0x0000_0040,
|
||||||
|
name_offset: 0x78,
|
||||||
|
name_length: name.len() as u16,
|
||||||
|
create_contexts_offset: 0,
|
||||||
|
create_contexts_length: 0,
|
||||||
|
name,
|
||||||
|
create_contexts: vec![],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
let decoded = CreateRequest::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, r);
|
||||||
|
assert_eq!(decoded.name_str().unwrap(), "dir\\file.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = CreateResponse {
|
||||||
|
structure_size: 89,
|
||||||
|
oplock_level: 0,
|
||||||
|
flags: 0,
|
||||||
|
create_action: 1,
|
||||||
|
creation_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_access_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_write_time: 0x01D9_0000_0000_0000,
|
||||||
|
change_time: 0x01D9_0000_0000_0000,
|
||||||
|
allocation_size: 0x1000,
|
||||||
|
end_of_file: 0x800,
|
||||||
|
file_attributes: 0x0020,
|
||||||
|
reserved2: 0,
|
||||||
|
file_id: FileId::new(0x1234, 0x5678),
|
||||||
|
create_contexts_offset: 0,
|
||||||
|
create_contexts_length: 0,
|
||||||
|
create_contexts: vec![],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
let decoded = CreateResponse::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_context_chain_round_trips_single() {
|
||||||
|
let ctxs = vec![CreateContext {
|
||||||
|
name: b"MxAc".to_vec(),
|
||||||
|
data: vec![],
|
||||||
|
}];
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
CreateContext::encode_chain(&ctxs, &mut buf).unwrap();
|
||||||
|
let decoded = CreateContext::parse_chain(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, ctxs);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_context_chain_round_trips_multi() {
|
||||||
|
let ctxs = vec![
|
||||||
|
CreateContext {
|
||||||
|
name: b"DHnQ".to_vec(),
|
||||||
|
data: vec![0u8; 16],
|
||||||
|
},
|
||||||
|
CreateContext {
|
||||||
|
name: b"MxAc".to_vec(),
|
||||||
|
data: vec![],
|
||||||
|
},
|
||||||
|
CreateContext {
|
||||||
|
name: b"QFid".to_vec(),
|
||||||
|
data: vec![0xAA; 32],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
CreateContext::encode_chain(&ctxs, &mut buf).unwrap();
|
||||||
|
let decoded = CreateContext::parse_chain(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, ctxs);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_chain_round_trips() {
|
||||||
|
let ctxs: Vec<CreateContext> = vec![];
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
CreateContext::encode_chain(&ctxs, &mut buf).unwrap();
|
||||||
|
assert!(buf.is_empty());
|
||||||
|
let decoded = CreateContext::parse_chain(&buf).unwrap();
|
||||||
|
assert!(decoded.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
83
vendor/smb-server/src/proto/messages/echo.rs
vendored
Normal file
83
vendor/smb-server/src/proto/messages/echo.rs
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
//! ECHO Request/Response (MS-SMB2 §2.2.28).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct EchoRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EchoRequest {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 4,
|
||||||
|
reserved: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct EchoResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EchoResponse {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 4,
|
||||||
|
reserved: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EchoRequest {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EchoResponse {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips() {
|
||||||
|
let req = EchoRequest::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
req.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(buf.len(), 4);
|
||||||
|
assert_eq!(EchoRequest::parse(&buf).unwrap(), req);
|
||||||
|
|
||||||
|
let resp = EchoResponse::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(EchoResponse::parse(&buf).unwrap(), resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
vendor/smb-server/src/proto/messages/error_response.rs
vendored
Normal file
84
vendor/smb-server/src/proto/messages/error_response.rs
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
//! SMB2 ERROR Response (MS-SMB2 §2.2.2).
|
||||||
|
//!
|
||||||
|
//! Sent in place of any normal response when the server returns a non-zero
|
||||||
|
//! NTSTATUS. The SMB2 header carries the NTSTATUS in `channel_sequence_status`;
|
||||||
|
//! this body provides extended error context if any.
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.2 ERROR Response.
|
||||||
|
///
|
||||||
|
/// `structure_size` is always 9; `byte_count` is the length of `error_data`
|
||||||
|
/// when there is no structured error context (the common case). When
|
||||||
|
/// `error_context_count > 0`, `error_data` holds a sequence of
|
||||||
|
/// [`ErrorContext`] entries (SMB 3.1.1+).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub error_context_count: u8,
|
||||||
|
pub reserved: u8,
|
||||||
|
pub byte_count: u32,
|
||||||
|
#[br(count = if byte_count == 0 { 1 } else { byte_count as usize })]
|
||||||
|
pub error_data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorResponse {
|
||||||
|
/// Build a minimal ERROR response body for the given NTSTATUS.
|
||||||
|
///
|
||||||
|
/// Per MS-SMB2 §2.2.2 a zero-`byte_count` ERROR response still emits a
|
||||||
|
/// single byte of `error_data` (the field is mandatory, length 1 when
|
||||||
|
/// there is no payload).
|
||||||
|
pub fn status(_ntstatus: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 9,
|
||||||
|
error_context_count: 0,
|
||||||
|
reserved: 0,
|
||||||
|
byte_count: 0,
|
||||||
|
error_data: vec![0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
let mut c = Cursor::new(buf);
|
||||||
|
Ok(Self::read(&mut c)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.2.1 ERROR Context Response (3.1.1+).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ErrorContext {
|
||||||
|
pub error_data_length: u32,
|
||||||
|
pub error_id: u32,
|
||||||
|
#[br(count = error_data_length as usize)]
|
||||||
|
pub error_context_data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips_status_helper() {
|
||||||
|
let r = ErrorResponse::status(0xC000_0022 /* STATUS_ACCESS_DENIED */);
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
let decoded = ErrorResponse::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, r);
|
||||||
|
// structure_size, contexts, reserved, bytecount, 1 byte payload = 9 bytes
|
||||||
|
assert_eq!(buf.len(), 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
vendor/smb-server/src/proto/messages/flush.rs
vendored
Normal file
86
vendor/smb-server/src/proto/messages/flush.rs
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
//! FLUSH Request/Response (MS-SMB2 §2.2.17 / §2.2.18).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FlushRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved1: u16,
|
||||||
|
pub reserved2: u32,
|
||||||
|
/// Volatile portion of the FileId.
|
||||||
|
pub file_id_persistent: u64,
|
||||||
|
/// Persistent portion of the FileId.
|
||||||
|
pub file_id_volatile: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlushRequest {
|
||||||
|
pub fn new(persistent: u64, volatile: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 24,
|
||||||
|
reserved1: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
file_id_persistent: persistent,
|
||||||
|
file_id_volatile: volatile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FlushResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FlushResponse {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 4,
|
||||||
|
reserved: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_codec {
|
||||||
|
($t:ty) => {
|
||||||
|
impl $t {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(<Self as BinRead>::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_codec!(FlushRequest);
|
||||||
|
impl_codec!(FlushResponse);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips() {
|
||||||
|
let r = FlushRequest::new(0x1122_3344_5566_7788, 0xAABB_CCDD_EEFF_0011);
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(buf.len(), 24);
|
||||||
|
assert_eq!(FlushRequest::parse(&buf).unwrap(), r);
|
||||||
|
|
||||||
|
let r = FlushResponse::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(FlushResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
206
vendor/smb-server/src/proto/messages/ioctl.rs
vendored
Normal file
206
vendor/smb-server/src/proto/messages/ioctl.rs
vendored
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
//! IOCTL Request/Response (MS-SMB2 §2.2.31 / §2.2.32).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// File-system control codes we recognize at the wire layer.
|
||||||
|
///
|
||||||
|
/// MS-FSCC catalogues the FSCTL codes; we only enumerate the ones referenced
|
||||||
|
/// in the spec for v1. Unknown codes round-trip via [`Fsctl::Other`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Fsctl {
|
||||||
|
/// `FSCTL_VALIDATE_NEGOTIATE_INFO` — required handler in v1.
|
||||||
|
ValidateNegotiateInfo,
|
||||||
|
/// `FSCTL_DFS_GET_REFERRALS`.
|
||||||
|
DfsGetReferrals,
|
||||||
|
/// `FSCTL_DFS_GET_REFERRALS_EX`.
|
||||||
|
DfsGetReferralsEx,
|
||||||
|
/// `FSCTL_PIPE_TRANSCEIVE`.
|
||||||
|
PipeTranscede,
|
||||||
|
/// `FSCTL_PIPE_PEEK`.
|
||||||
|
PipePeek,
|
||||||
|
/// `FSCTL_PIPE_WAIT`.
|
||||||
|
PipeWait,
|
||||||
|
/// `FSCTL_LMR_REQUEST_RESILIENCY`.
|
||||||
|
LmrRequestResiliency,
|
||||||
|
/// `FSCTL_QUERY_NETWORK_INTERFACE_INFO`.
|
||||||
|
QueryNetworkInterfaceInfo,
|
||||||
|
/// Anything else.
|
||||||
|
Other(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fsctl {
|
||||||
|
pub const VALIDATE_NEGOTIATE_INFO: u32 = 0x0014_0204;
|
||||||
|
pub const DFS_GET_REFERRALS: u32 = 0x0006_0194;
|
||||||
|
pub const DFS_GET_REFERRALS_EX: u32 = 0x0006_0198;
|
||||||
|
pub const PIPE_TRANSCEIVE: u32 = 0x0011_C017;
|
||||||
|
pub const PIPE_PEEK: u32 = 0x0011_400C;
|
||||||
|
pub const PIPE_WAIT: u32 = 0x0011_C018;
|
||||||
|
pub const LMR_REQUEST_RESILIENCY: u32 = 0x001C_0017;
|
||||||
|
pub const QUERY_NETWORK_INTERFACE_INFO: u32 = 0x001F_C017;
|
||||||
|
|
||||||
|
pub fn from_u32(code: u32) -> Self {
|
||||||
|
match code {
|
||||||
|
Self::VALIDATE_NEGOTIATE_INFO => Self::ValidateNegotiateInfo,
|
||||||
|
Self::DFS_GET_REFERRALS => Self::DfsGetReferrals,
|
||||||
|
Self::DFS_GET_REFERRALS_EX => Self::DfsGetReferralsEx,
|
||||||
|
Self::PIPE_TRANSCEIVE => Self::PipeTranscede,
|
||||||
|
Self::PIPE_PEEK => Self::PipePeek,
|
||||||
|
Self::PIPE_WAIT => Self::PipeWait,
|
||||||
|
Self::LMR_REQUEST_RESILIENCY => Self::LmrRequestResiliency,
|
||||||
|
Self::QUERY_NETWORK_INTERFACE_INFO => Self::QueryNetworkInterfaceInfo,
|
||||||
|
other => Self::Other(other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_u32(self) -> u32 {
|
||||||
|
match self {
|
||||||
|
Self::ValidateNegotiateInfo => Self::VALIDATE_NEGOTIATE_INFO,
|
||||||
|
Self::DfsGetReferrals => Self::DFS_GET_REFERRALS,
|
||||||
|
Self::DfsGetReferralsEx => Self::DFS_GET_REFERRALS_EX,
|
||||||
|
Self::PipeTranscede => Self::PIPE_TRANSCEIVE,
|
||||||
|
Self::PipePeek => Self::PIPE_PEEK,
|
||||||
|
Self::PipeWait => Self::PIPE_WAIT,
|
||||||
|
Self::LmrRequestResiliency => Self::LMR_REQUEST_RESILIENCY,
|
||||||
|
Self::QueryNetworkInterfaceInfo => Self::QUERY_NETWORK_INTERFACE_INFO,
|
||||||
|
Self::Other(c) => c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_IOCTL_REQUEST (MS-SMB2 §2.2.31).
|
||||||
|
///
|
||||||
|
/// `input_offset` and `output_offset` are absolute (from the start of the
|
||||||
|
/// SMB2 header). We model the input buffer immediately following the fixed
|
||||||
|
/// prefix; the output buffer area is unused on requests but kept for round
|
||||||
|
/// tripping and extension scenarios.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct IoctlRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
pub ctl_code: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub input_offset: u32,
|
||||||
|
pub input_count: u32,
|
||||||
|
pub max_input_response: u32,
|
||||||
|
pub output_offset: u32,
|
||||||
|
pub output_count: u32,
|
||||||
|
pub max_output_response: u32,
|
||||||
|
pub flags: u32,
|
||||||
|
pub reserved2: u32,
|
||||||
|
#[br(count = input_count as usize)]
|
||||||
|
pub input: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IoctlRequest {
|
||||||
|
/// Flag: SMB2_0_IOCTL_IS_FSCTL.
|
||||||
|
pub const FLAG_IS_FSCTL: u32 = 0x0000_0001;
|
||||||
|
|
||||||
|
pub fn fsctl(&self) -> Fsctl {
|
||||||
|
Fsctl::from_u32(self.ctl_code)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_IOCTL_RESPONSE (MS-SMB2 §2.2.32).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct IoctlResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
pub ctl_code: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub input_offset: u32,
|
||||||
|
pub input_count: u32,
|
||||||
|
pub output_offset: u32,
|
||||||
|
pub output_count: u32,
|
||||||
|
pub flags: u32,
|
||||||
|
pub reserved2: u32,
|
||||||
|
/// Output buffer immediately following the fixed prefix.
|
||||||
|
#[br(count = output_count as usize)]
|
||||||
|
pub output: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IoctlResponse {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fsctl_decode_known() {
|
||||||
|
assert_eq!(Fsctl::from_u32(0x0014_0204), Fsctl::ValidateNegotiateInfo);
|
||||||
|
assert_eq!(Fsctl::from_u32(0xDEAD_BEEF), Fsctl::Other(0xDEAD_BEEF));
|
||||||
|
assert_eq!(Fsctl::ValidateNegotiateInfo.as_u32(), 0x0014_0204);
|
||||||
|
assert_eq!(Fsctl::Other(0xDEAD_BEEF).as_u32(), 0xDEAD_BEEF);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let r = IoctlRequest {
|
||||||
|
structure_size: 57,
|
||||||
|
reserved: 0,
|
||||||
|
ctl_code: Fsctl::VALIDATE_NEGOTIATE_INFO,
|
||||||
|
file_id: FileId::any(),
|
||||||
|
input_offset: 0x78,
|
||||||
|
input_count: 4,
|
||||||
|
max_input_response: 0,
|
||||||
|
output_offset: 0,
|
||||||
|
output_count: 0,
|
||||||
|
max_output_response: 0x1000,
|
||||||
|
flags: IoctlRequest::FLAG_IS_FSCTL,
|
||||||
|
reserved2: 0,
|
||||||
|
input: vec![0xCA, 0xFE, 0xBA, 0xBE],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
let decoded = IoctlRequest::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, r);
|
||||||
|
assert_eq!(decoded.fsctl(), Fsctl::ValidateNegotiateInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = IoctlResponse {
|
||||||
|
structure_size: 49,
|
||||||
|
reserved: 0,
|
||||||
|
ctl_code: Fsctl::VALIDATE_NEGOTIATE_INFO,
|
||||||
|
file_id: FileId::any(),
|
||||||
|
input_offset: 0,
|
||||||
|
input_count: 0,
|
||||||
|
output_offset: 0x70,
|
||||||
|
output_count: 4,
|
||||||
|
flags: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
output: vec![1, 2, 3, 4],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(IoctlResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
vendor/smb-server/src/proto/messages/lock.rs
vendored
Normal file
118
vendor/smb-server/src/proto/messages/lock.rs
vendored
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
//! LOCK Request/Response (MS-SMB2 §2.2.26 / §2.2.27).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// SMB2_LOCK_ELEMENT (MS-SMB2 §2.2.26.1) — exactly 24 bytes.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LockElement {
|
||||||
|
pub offset: u64,
|
||||||
|
pub length: u64,
|
||||||
|
pub flags: u32,
|
||||||
|
pub reserved: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockElement {
|
||||||
|
pub const FLAG_SHARED_LOCK: u32 = 0x0000_0001;
|
||||||
|
pub const FLAG_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
|
||||||
|
pub const FLAG_UNLOCK: u32 = 0x0000_0004;
|
||||||
|
pub const FLAG_FAIL_IMMEDIATELY: u32 = 0x0000_0010;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LockRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub lock_count: u16,
|
||||||
|
pub lock_sequence: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
#[br(count = lock_count as usize)]
|
||||||
|
pub locks: Vec<LockElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockRequest {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LockResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LockResponse {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 4,
|
||||||
|
reserved: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockResponse {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let r = LockRequest {
|
||||||
|
structure_size: 48,
|
||||||
|
lock_count: 2,
|
||||||
|
lock_sequence: 0,
|
||||||
|
file_id: FileId::new(1, 2),
|
||||||
|
locks: vec![
|
||||||
|
LockElement {
|
||||||
|
offset: 0,
|
||||||
|
length: 16,
|
||||||
|
flags: LockElement::FLAG_EXCLUSIVE_LOCK,
|
||||||
|
reserved: 0,
|
||||||
|
},
|
||||||
|
LockElement {
|
||||||
|
offset: 0,
|
||||||
|
length: 16,
|
||||||
|
flags: LockElement::FLAG_UNLOCK,
|
||||||
|
reserved: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(LockRequest::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = LockResponse::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(LockResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
vendor/smb-server/src/proto/messages/logoff.rs
vendored
Normal file
77
vendor/smb-server/src/proto/messages/logoff.rs
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//! LOGOFF Request/Response (MS-SMB2 §2.2.7 / §2.2.8).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LogoffRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogoffRequest {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 4,
|
||||||
|
reserved: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LogoffResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogoffResponse {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 4,
|
||||||
|
reserved: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_codec {
|
||||||
|
($t:ty) => {
|
||||||
|
impl $t {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(<Self as BinRead>::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_codec!(LogoffRequest);
|
||||||
|
impl_codec!(LogoffResponse);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips() {
|
||||||
|
let r = LogoffRequest::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(LogoffRequest::parse(&buf).unwrap(), r);
|
||||||
|
|
||||||
|
let r = LogoffResponse::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(LogoffResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
vendor/smb-server/src/proto/messages/mod.rs
vendored
Normal file
55
vendor/smb-server/src/proto/messages/mod.rs
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//! Per-command request/response wire structs.
|
||||||
|
//!
|
||||||
|
//! Each SMB2 command (MS-SMB2 §2.2.3 — §2.2.18, §2.2.31, §2.2.37, §2.2.39)
|
||||||
|
//! gets its own submodule with a `…Request` and `…Response` struct, both
|
||||||
|
//! `binrw`-driven and round-trip safe.
|
||||||
|
//!
|
||||||
|
//! The crate does **not** implement command behavior — it only encodes/decodes
|
||||||
|
//! the wire bytes. The server crate owns dispatch and state.
|
||||||
|
|
||||||
|
pub mod cancel;
|
||||||
|
pub mod change_notify;
|
||||||
|
pub mod close;
|
||||||
|
pub mod create;
|
||||||
|
pub mod echo;
|
||||||
|
pub mod error_response;
|
||||||
|
pub mod flush;
|
||||||
|
pub mod ioctl;
|
||||||
|
pub mod lock;
|
||||||
|
pub mod logoff;
|
||||||
|
pub mod negotiate;
|
||||||
|
pub mod oplock_break;
|
||||||
|
pub mod query_directory;
|
||||||
|
pub mod query_info;
|
||||||
|
pub mod read;
|
||||||
|
pub mod session_setup;
|
||||||
|
pub mod set_info;
|
||||||
|
pub mod tree_connect;
|
||||||
|
pub mod tree_disconnect;
|
||||||
|
pub mod write;
|
||||||
|
|
||||||
|
pub use cancel::CancelRequest;
|
||||||
|
pub use change_notify::{ChangeNotifyRequest, ChangeNotifyResponse};
|
||||||
|
pub use close::{CloseRequest, CloseResponse};
|
||||||
|
pub use create::{
|
||||||
|
CreateContext, CreateRequest, CreateResponse, FileId, ImpersonationLevel, OplockLevel,
|
||||||
|
};
|
||||||
|
pub use echo::{EchoRequest, EchoResponse};
|
||||||
|
pub use error_response::{ErrorContext, ErrorResponse};
|
||||||
|
pub use flush::{FlushRequest, FlushResponse};
|
||||||
|
pub use ioctl::{Fsctl, IoctlRequest, IoctlResponse};
|
||||||
|
pub use lock::{LockElement, LockRequest, LockResponse};
|
||||||
|
pub use logoff::{LogoffRequest, LogoffResponse};
|
||||||
|
pub use negotiate::{
|
||||||
|
Dialect, EncryptionCapabilities, NegotiateContext, NegotiateContextData, NegotiateRequest,
|
||||||
|
NegotiateResponse, PreauthIntegrityCapabilities, SigningCapabilities,
|
||||||
|
};
|
||||||
|
pub use oplock_break::{OplockBreakAck, OplockBreakNotification};
|
||||||
|
pub use query_directory::{FileInfoClass, QueryDirectoryRequest, QueryDirectoryResponse};
|
||||||
|
pub use query_info::{InfoType, QueryInfoRequest, QueryInfoResponse};
|
||||||
|
pub use read::{ReadRequest, ReadResponse};
|
||||||
|
pub use session_setup::{SessionSetupRequest, SessionSetupResponse};
|
||||||
|
pub use set_info::{SetInfoRequest, SetInfoResponse};
|
||||||
|
pub use tree_connect::{TreeConnectRequest, TreeConnectResponse};
|
||||||
|
pub use tree_disconnect::{TreeDisconnectRequest, TreeDisconnectResponse};
|
||||||
|
pub use write::{WriteRequest, WriteResponse};
|
||||||
384
vendor/smb-server/src/proto/messages/negotiate.rs
vendored
Normal file
384
vendor/smb-server/src/proto/messages/negotiate.rs
vendored
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
//! NEGOTIATE Request/Response (MS-SMB2 §2.2.3 / §2.2.4) including the SMB
|
||||||
|
//! 3.1.1 negotiate-context machinery from §2.2.3.1.x and §2.2.4.x.
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dialect
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// SMB2 dialect revision codes (MS-SMB2 §2.2.3 — DialectRevision).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[repr(u16)]
|
||||||
|
pub enum Dialect {
|
||||||
|
Smb202 = 0x0202,
|
||||||
|
Smb210 = 0x0210,
|
||||||
|
Smb300 = 0x0300,
|
||||||
|
Smb302 = 0x0302,
|
||||||
|
Smb311 = 0x0311,
|
||||||
|
/// Sent by SMB 2.0.2/2.1 clients via SMB1 negotiate; we accept it as a
|
||||||
|
/// signal to multi-protocol-negotiate. Value 0x02FF.
|
||||||
|
Smb2Wildcard = 0x02FF,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dialect {
|
||||||
|
pub fn from_u16(v: u16) -> Option<Self> {
|
||||||
|
Some(match v {
|
||||||
|
0x0202 => Self::Smb202,
|
||||||
|
0x0210 => Self::Smb210,
|
||||||
|
0x0300 => Self::Smb300,
|
||||||
|
0x0302 => Self::Smb302,
|
||||||
|
0x0311 => Self::Smb311,
|
||||||
|
0x02FF => Self::Smb2Wildcard,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn as_u16(self) -> u16 {
|
||||||
|
self as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Negotiate request
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.3 NEGOTIATE Request.
|
||||||
|
///
|
||||||
|
/// `dialects` is a sequence of u16 little-endian dialect codes; for SMB 3.1.1
|
||||||
|
/// the trailing `negotiate_context_list` carries variable-length contexts at
|
||||||
|
/// `negotiate_context_offset`.
|
||||||
|
///
|
||||||
|
/// Note on parsing: we deliberately don't try to read `negotiate_context_list`
|
||||||
|
/// here automatically, because its position is given by an absolute offset
|
||||||
|
/// from the *start of the SMB2 header*, not from the start of this body.
|
||||||
|
/// The server crate decodes this body, then if `dialects` includes 3.1.1 it
|
||||||
|
/// resolves `negotiate_context_offset` against the original packet buffer
|
||||||
|
/// and parses the contexts via [`NegotiateContext::parse_list`].
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct NegotiateRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub dialect_count: u16,
|
||||||
|
pub security_mode: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
pub capabilities: u32,
|
||||||
|
pub client_guid: [u8; 16],
|
||||||
|
/// 3.1.1: NegotiateContextOffset. 2.x/3.0/3.0.2: ClientStartTime.
|
||||||
|
pub negotiate_context_offset_or_client_start_time: u64,
|
||||||
|
#[br(count = dialect_count as usize)]
|
||||||
|
pub dialects: Vec<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NegotiateRequest {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Negotiate response
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.4 NEGOTIATE Response.
|
||||||
|
///
|
||||||
|
/// The trailing `security_buffer` and (3.1.1) `negotiate_context_list` are
|
||||||
|
/// referenced by absolute offsets from the start of the SMB2 header. This
|
||||||
|
/// struct encodes the *fixed* portion plus a `security_buffer` that we treat
|
||||||
|
/// as a length-counted blob immediately following the fixed portion (the
|
||||||
|
/// common server layout). For 3.1.1 contexts, the server crate writes the
|
||||||
|
/// fixed portion via [`NegotiateResponse::write_to`], then appends 8-byte-
|
||||||
|
/// aligned negotiate contexts and patches `negotiate_context_offset` to the
|
||||||
|
/// post-padding offset.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct NegotiateResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub security_mode: u16,
|
||||||
|
pub dialect_revision: u16,
|
||||||
|
/// 3.1.1: NegotiateContextCount. 2.x/3.0/3.0.2: Reserved.
|
||||||
|
pub negotiate_context_count_or_reserved: u16,
|
||||||
|
pub server_guid: [u8; 16],
|
||||||
|
pub capabilities: u32,
|
||||||
|
pub max_transact_size: u32,
|
||||||
|
pub max_read_size: u32,
|
||||||
|
pub max_write_size: u32,
|
||||||
|
/// 100ns ticks since 1601-01-01 UTC.
|
||||||
|
pub system_time: u64,
|
||||||
|
pub server_start_time: u64,
|
||||||
|
pub security_buffer_offset: u16,
|
||||||
|
pub security_buffer_length: u16,
|
||||||
|
/// 3.1.1: NegotiateContextOffset. 2.x/3.0/3.0.2: Reserved2.
|
||||||
|
pub negotiate_context_offset_or_reserved2: u32,
|
||||||
|
#[br(count = security_buffer_length as usize)]
|
||||||
|
pub security_buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NegotiateResponse {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Negotiate contexts (SMB 3.1.1)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.3.1 / §2.2.4.x — NEGOTIATE_CONTEXT generic header.
|
||||||
|
///
|
||||||
|
/// Contexts are 8-byte-aligned in the chain (the trailing padding is between
|
||||||
|
/// contexts; see §2.2.3.1 "Each NEGOTIATE_CONTEXT MUST be 8-byte aligned").
|
||||||
|
/// `parse_list` / `encode_list` handle the alignment.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct NegotiateContext {
|
||||||
|
pub context_type: u16,
|
||||||
|
pub data_length: u16,
|
||||||
|
pub reserved: u32,
|
||||||
|
#[br(count = data_length as usize)]
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NegotiateContext {
|
||||||
|
pub const TYPE_PREAUTH_INTEGRITY: u16 = 0x0001;
|
||||||
|
pub const TYPE_ENCRYPTION: u16 = 0x0002;
|
||||||
|
pub const TYPE_COMPRESSION: u16 = 0x0003;
|
||||||
|
pub const TYPE_NETNAME_NEGOTIATE: u16 = 0x0005;
|
||||||
|
pub const TYPE_TRANSPORT_CAPS: u16 = 0x0006;
|
||||||
|
pub const TYPE_RDMA_TRANSFORM: u16 = 0x0007;
|
||||||
|
pub const TYPE_SIGNING: u16 = 0x0008;
|
||||||
|
|
||||||
|
/// Parse a chain of negotiate contexts from `buf`. The chain is a series
|
||||||
|
/// of (8-byte-aligned) [`NegotiateContext`] entries. `count` comes from
|
||||||
|
/// the parent message's `NegotiateContextCount`.
|
||||||
|
pub fn parse_list(mut buf: &[u8], count: u16) -> ProtoResult<Vec<NegotiateContext>> {
|
||||||
|
let mut out = Vec::with_capacity(count as usize);
|
||||||
|
let mut consumed_total = 0usize;
|
||||||
|
for _ in 0..count {
|
||||||
|
// Pad to 8-byte alignment relative to the start of the list.
|
||||||
|
let pad = (8 - (consumed_total % 8)) % 8;
|
||||||
|
if pad > 0 {
|
||||||
|
if buf.len() < pad {
|
||||||
|
return Err(crate::proto::error::ProtoError::Malformed(
|
||||||
|
"negotiate context alignment underflow",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
buf = &buf[pad..];
|
||||||
|
consumed_total += pad;
|
||||||
|
}
|
||||||
|
let mut c = Cursor::new(buf);
|
||||||
|
let ctx = NegotiateContext::read(&mut c)?;
|
||||||
|
let consumed = c.position() as usize;
|
||||||
|
buf = &buf[consumed..];
|
||||||
|
consumed_total += consumed;
|
||||||
|
out.push(ctx);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a chain of negotiate contexts into `out`, inserting 8-byte
|
||||||
|
/// padding between entries.
|
||||||
|
pub fn encode_list(list: &[NegotiateContext], out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let start = out.len();
|
||||||
|
for (i, ctx) in list.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
let pad = (8 - ((out.len() - start) % 8)) % 8;
|
||||||
|
out.extend(std::iter::repeat_n(0u8, pad));
|
||||||
|
}
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(ctx, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed payload of a known [`NegotiateContext`] type. Convenience wrapper —
|
||||||
|
/// the wire form is always [`NegotiateContext`]; this enum is for callers who
|
||||||
|
/// prefer typed access.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum NegotiateContextData {
|
||||||
|
PreauthIntegrity(PreauthIntegrityCapabilities),
|
||||||
|
Encryption(EncryptionCapabilities),
|
||||||
|
Signing(SigningCapabilities),
|
||||||
|
/// Unknown / unhandled context — preserve raw bytes for round-tripping.
|
||||||
|
Other {
|
||||||
|
context_type: u16,
|
||||||
|
data: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.3.1.1 / §2.2.4.1 SMB2_PREAUTH_INTEGRITY_CAPABILITIES.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct PreauthIntegrityCapabilities {
|
||||||
|
pub hash_algorithm_count: u16,
|
||||||
|
pub salt_length: u16,
|
||||||
|
#[br(count = hash_algorithm_count as usize)]
|
||||||
|
pub hash_algorithms: Vec<u16>,
|
||||||
|
#[br(count = salt_length as usize)]
|
||||||
|
pub salt: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreauthIntegrityCapabilities {
|
||||||
|
/// Hash algorithm: SHA-512 (the only one defined in MS-SMB2 §2.2.3.1.1).
|
||||||
|
pub const HASH_SHA512: u16 = 0x0001;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.3.1.2 / §2.2.4.2 SMB2_ENCRYPTION_CAPABILITIES.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct EncryptionCapabilities {
|
||||||
|
pub cipher_count: u16,
|
||||||
|
#[br(count = cipher_count as usize)]
|
||||||
|
pub ciphers: Vec<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EncryptionCapabilities {
|
||||||
|
pub const CIPHER_AES_128_CCM: u16 = 0x0001;
|
||||||
|
pub const CIPHER_AES_128_GCM: u16 = 0x0002;
|
||||||
|
pub const CIPHER_AES_256_CCM: u16 = 0x0003;
|
||||||
|
pub const CIPHER_AES_256_GCM: u16 = 0x0004;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MS-SMB2 §2.2.3.1.7 / §2.2.4.7 SMB2_SIGNING_CAPABILITIES.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SigningCapabilities {
|
||||||
|
pub signing_algorithm_count: u16,
|
||||||
|
#[br(count = signing_algorithm_count as usize)]
|
||||||
|
pub signing_algorithms: Vec<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SigningCapabilities {
|
||||||
|
pub const ALGORITHM_HMAC_SHA256: u16 = 0x0000;
|
||||||
|
pub const ALGORITHM_AES_CMAC: u16 = 0x0001;
|
||||||
|
pub const ALGORITHM_AES_GMAC: u16 = 0x0002;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_request_round_trips() {
|
||||||
|
let req = NegotiateRequest {
|
||||||
|
structure_size: 36,
|
||||||
|
dialect_count: 5,
|
||||||
|
security_mode: 0x0001, // signing enabled
|
||||||
|
reserved: 0,
|
||||||
|
capabilities: 0x0000_007F,
|
||||||
|
client_guid: [0xAB; 16],
|
||||||
|
negotiate_context_offset_or_client_start_time: 0x0000_0070_0000_0000,
|
||||||
|
dialects: vec![0x0202, 0x0210, 0x0300, 0x0302, 0x0311],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
req.write_to(&mut buf).unwrap();
|
||||||
|
let decoded = NegotiateRequest::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_response_round_trips() {
|
||||||
|
let resp = NegotiateResponse {
|
||||||
|
structure_size: 65,
|
||||||
|
security_mode: 0x0003,
|
||||||
|
dialect_revision: Dialect::Smb311.as_u16(),
|
||||||
|
negotiate_context_count_or_reserved: 3,
|
||||||
|
server_guid: [0xCD; 16],
|
||||||
|
capabilities: 0x0000_007F,
|
||||||
|
max_transact_size: 0x0010_0000,
|
||||||
|
max_read_size: 0x0010_0000,
|
||||||
|
max_write_size: 0x0010_0000,
|
||||||
|
system_time: 0x01D9_1234_5678_9ABC,
|
||||||
|
server_start_time: 0,
|
||||||
|
security_buffer_offset: 0x80,
|
||||||
|
security_buffer_length: 8,
|
||||||
|
negotiate_context_offset_or_reserved2: 0x100,
|
||||||
|
security_buffer: vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).unwrap();
|
||||||
|
let decoded = NegotiateResponse::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dialect_round_trips() {
|
||||||
|
for d in [
|
||||||
|
Dialect::Smb202,
|
||||||
|
Dialect::Smb210,
|
||||||
|
Dialect::Smb300,
|
||||||
|
Dialect::Smb302,
|
||||||
|
Dialect::Smb311,
|
||||||
|
Dialect::Smb2Wildcard,
|
||||||
|
] {
|
||||||
|
assert_eq!(Dialect::from_u16(d.as_u16()), Some(d));
|
||||||
|
}
|
||||||
|
assert_eq!(Dialect::from_u16(0xBEEF), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preauth_caps_round_trips() {
|
||||||
|
let p = PreauthIntegrityCapabilities {
|
||||||
|
hash_algorithm_count: 1,
|
||||||
|
salt_length: 32,
|
||||||
|
hash_algorithms: vec![PreauthIntegrityCapabilities::HASH_SHA512],
|
||||||
|
salt: vec![0xAA; 32],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut c = Cursor::new(&mut buf);
|
||||||
|
BinWrite::write(&p, &mut c).unwrap();
|
||||||
|
let decoded = PreauthIntegrityCapabilities::read(&mut Cursor::new(&buf)).unwrap();
|
||||||
|
assert_eq!(decoded, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_context_list_round_trips() {
|
||||||
|
let list = vec![
|
||||||
|
NegotiateContext {
|
||||||
|
context_type: NegotiateContext::TYPE_PREAUTH_INTEGRITY,
|
||||||
|
data_length: 6,
|
||||||
|
reserved: 0,
|
||||||
|
data: vec![0x01, 0x00, 0x20, 0x00, 0x01, 0x00],
|
||||||
|
},
|
||||||
|
NegotiateContext {
|
||||||
|
context_type: NegotiateContext::TYPE_ENCRYPTION,
|
||||||
|
data_length: 4,
|
||||||
|
reserved: 0,
|
||||||
|
data: vec![0x02, 0x00, 0x02, 0x00],
|
||||||
|
},
|
||||||
|
NegotiateContext {
|
||||||
|
context_type: NegotiateContext::TYPE_SIGNING,
|
||||||
|
data_length: 4,
|
||||||
|
reserved: 0,
|
||||||
|
data: vec![0x01, 0x00, 0x01, 0x00],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
NegotiateContext::encode_list(&list, &mut buf).unwrap();
|
||||||
|
let parsed = NegotiateContext::parse_list(&buf, 3).unwrap();
|
||||||
|
assert_eq!(parsed, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
vendor/smb-server/src/proto/messages/oplock_break.rs
vendored
Normal file
59
vendor/smb-server/src/proto/messages/oplock_break.rs
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//! OPLOCK_BREAK Notification + Acknowledgement (MS-SMB2 §2.2.23 / §2.2.24).
|
||||||
|
//!
|
||||||
|
//! V1 never grants oplocks, so we never *send* a notification, but the
|
||||||
|
//! handler exists for safety. A client may send an OPLOCK_BREAK ACK before
|
||||||
|
//! the server has cleared its oplock state in the (rare) edge case during
|
||||||
|
//! teardown.
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// SMB2_OPLOCK_BREAK_NOTIFICATION (MS-SMB2 §2.2.23.1).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct OplockBreakNotification {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub oplock_level: u8,
|
||||||
|
pub reserved: u8,
|
||||||
|
pub reserved2: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OplockBreakNotification {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_OPLOCK_BREAK_ACK (MS-SMB2 §2.2.24.1) — same wire shape as the
|
||||||
|
/// notification.
|
||||||
|
pub type OplockBreakAck = OplockBreakNotification;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips() {
|
||||||
|
let r = OplockBreakNotification {
|
||||||
|
structure_size: 24,
|
||||||
|
oplock_level: 0,
|
||||||
|
reserved: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
file_id: FileId::new(1, 2),
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(OplockBreakNotification::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
136
vendor/smb-server/src/proto/messages/query_directory.rs
vendored
Normal file
136
vendor/smb-server/src/proto/messages/query_directory.rs
vendored
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
//! QUERY_DIRECTORY Request/Response (MS-SMB2 §2.2.33 / §2.2.34).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// File-info-class identifiers used in QUERY_DIRECTORY (MS-SMB2 §2.2.33
|
||||||
|
/// FileInformationClass).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum FileInfoClass {
|
||||||
|
FileDirectoryInformation = 0x01,
|
||||||
|
FileFullDirectoryInformation = 0x02,
|
||||||
|
FileBothDirectoryInformation = 0x03,
|
||||||
|
FileNamesInformation = 0x0C,
|
||||||
|
FileIdBothDirectoryInformation = 0x25,
|
||||||
|
FileIdFullDirectoryInformation = 0x26,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileInfoClass {
|
||||||
|
pub fn from_u8(v: u8) -> Option<Self> {
|
||||||
|
Some(match v {
|
||||||
|
0x01 => Self::FileDirectoryInformation,
|
||||||
|
0x02 => Self::FileFullDirectoryInformation,
|
||||||
|
0x03 => Self::FileBothDirectoryInformation,
|
||||||
|
0x0C => Self::FileNamesInformation,
|
||||||
|
0x25 => Self::FileIdBothDirectoryInformation,
|
||||||
|
0x26 => Self::FileIdFullDirectoryInformation,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_QUERY_DIRECTORY_REQUEST (MS-SMB2 §2.2.33).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct QueryDirectoryRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub file_information_class: u8,
|
||||||
|
pub flags: u8,
|
||||||
|
pub file_index: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub file_name_offset: u16,
|
||||||
|
pub file_name_length: u16,
|
||||||
|
pub output_buffer_length: u32,
|
||||||
|
/// UTF-16LE search pattern (e.g. "*").
|
||||||
|
#[br(count = file_name_length as usize)]
|
||||||
|
pub file_name: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryDirectoryRequest {
|
||||||
|
pub const FLAG_RESTART_SCANS: u8 = 0x01;
|
||||||
|
pub const FLAG_RETURN_SINGLE_ENTRY: u8 = 0x02;
|
||||||
|
pub const FLAG_INDEX_SPECIFIED: u8 = 0x04;
|
||||||
|
pub const FLAG_REOPEN: u8 = 0x10;
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_QUERY_DIRECTORY_RESPONSE (MS-SMB2 §2.2.34).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct QueryDirectoryResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
/// `OutputBufferOffset` is from the start of the SMB2 header.
|
||||||
|
pub output_buffer_offset: u16,
|
||||||
|
pub output_buffer_length: u32,
|
||||||
|
/// Variable-length info-class-specific buffer.
|
||||||
|
#[br(count = output_buffer_length as usize)]
|
||||||
|
pub buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryDirectoryResponse {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn utf16le(s: &str) -> Vec<u8> {
|
||||||
|
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let pat = utf16le("*");
|
||||||
|
let r = QueryDirectoryRequest {
|
||||||
|
structure_size: 33,
|
||||||
|
file_information_class: FileInfoClass::FileIdBothDirectoryInformation as u8,
|
||||||
|
flags: QueryDirectoryRequest::FLAG_RESTART_SCANS,
|
||||||
|
file_index: 0,
|
||||||
|
file_id: FileId::new(1, 2),
|
||||||
|
file_name_offset: 0x60,
|
||||||
|
file_name_length: pat.len() as u16,
|
||||||
|
output_buffer_length: 0x10000,
|
||||||
|
file_name: pat,
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(QueryDirectoryRequest::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = QueryDirectoryResponse {
|
||||||
|
structure_size: 9,
|
||||||
|
output_buffer_offset: 0x48,
|
||||||
|
output_buffer_length: 8,
|
||||||
|
buffer: vec![1, 2, 3, 4, 5, 6, 7, 8],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(QueryDirectoryResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
140
vendor/smb-server/src/proto/messages/query_info.rs
vendored
Normal file
140
vendor/smb-server/src/proto/messages/query_info.rs
vendored
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
//! QUERY_INFO Request/Response (MS-SMB2 §2.2.37 / §2.2.38).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// `InfoType` values (MS-SMB2 §2.2.37 InfoType field).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum InfoType {
|
||||||
|
File = 0x01,
|
||||||
|
FileSystem = 0x02,
|
||||||
|
Security = 0x03,
|
||||||
|
Quota = 0x04,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InfoType {
|
||||||
|
pub fn from_u8(v: u8) -> Option<Self> {
|
||||||
|
Some(match v {
|
||||||
|
0x01 => Self::File,
|
||||||
|
0x02 => Self::FileSystem,
|
||||||
|
0x03 => Self::Security,
|
||||||
|
0x04 => Self::Quota,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_QUERY_INFO_REQUEST (MS-SMB2 §2.2.37).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct QueryInfoRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub info_type: u8,
|
||||||
|
pub file_information_class: u8,
|
||||||
|
pub output_buffer_length: u32,
|
||||||
|
pub input_buffer_offset: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
pub input_buffer_length: u32,
|
||||||
|
/// `AdditionalInformation`: which fields of the security descriptor to
|
||||||
|
/// return when `info_type == Security`. Otherwise an additional info-class
|
||||||
|
/// selector for FS info.
|
||||||
|
pub additional_information: u32,
|
||||||
|
pub flags: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
/// Optional input buffer (used by FILE/FS info classes that need it, e.g.
|
||||||
|
/// `FileFullEaInformation` extended-attribute name lists).
|
||||||
|
#[br(count = input_buffer_length as usize)]
|
||||||
|
pub input_buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryInfoRequest {
|
||||||
|
/// Flag: SL_RESTART_SCAN.
|
||||||
|
pub const FLAG_RESTART_SCAN: u32 = 0x0000_0001;
|
||||||
|
/// Flag: SL_RETURN_SINGLE_ENTRY.
|
||||||
|
pub const FLAG_RETURN_SINGLE_ENTRY: u32 = 0x0000_0002;
|
||||||
|
/// Flag: SL_INDEX_SPECIFIED.
|
||||||
|
pub const FLAG_INDEX_SPECIFIED: u32 = 0x0000_0004;
|
||||||
|
|
||||||
|
pub fn info_type_enum(&self) -> Option<InfoType> {
|
||||||
|
InfoType::from_u8(self.info_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_QUERY_INFO_RESPONSE (MS-SMB2 §2.2.38).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct QueryInfoResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub output_buffer_offset: u16,
|
||||||
|
pub output_buffer_length: u32,
|
||||||
|
#[br(count = output_buffer_length as usize)]
|
||||||
|
pub buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryInfoResponse {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let r = QueryInfoRequest {
|
||||||
|
structure_size: 41,
|
||||||
|
info_type: InfoType::File as u8,
|
||||||
|
file_information_class: 0x05, // FileStandardInformation
|
||||||
|
output_buffer_length: 0x1000,
|
||||||
|
input_buffer_offset: 0,
|
||||||
|
reserved: 0,
|
||||||
|
input_buffer_length: 0,
|
||||||
|
additional_information: 0,
|
||||||
|
flags: 0,
|
||||||
|
file_id: FileId::new(1, 2),
|
||||||
|
input_buffer: vec![],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
let decoded = QueryInfoRequest::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, r);
|
||||||
|
assert_eq!(decoded.info_type_enum(), Some(InfoType::File));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = QueryInfoResponse {
|
||||||
|
structure_size: 9,
|
||||||
|
output_buffer_offset: 0x48,
|
||||||
|
output_buffer_length: 4,
|
||||||
|
buffer: vec![0xAB, 0xCD, 0xEF, 0x01],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(QueryInfoResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
141
vendor/smb-server/src/proto/messages/read.rs
vendored
Normal file
141
vendor/smb-server/src/proto/messages/read.rs
vendored
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
//! READ Request/Response (MS-SMB2 §2.2.19 / §2.2.20).
|
||||||
|
//!
|
||||||
|
//! ## Data buffer offsets
|
||||||
|
//!
|
||||||
|
//! Both the READ request `ReadChannelInfoOffset` and the READ response
|
||||||
|
//! `DataOffset` are measured from the **start of the SMB2 header**, not from
|
||||||
|
//! the start of this structure (MS-SMB2 §2.2.20 explicitly: "DataOffset (1
|
||||||
|
//! byte): The offset, in bytes, from the beginning of the SMB2 header to the
|
||||||
|
//! data being read"). When constructing a response, the server crate must
|
||||||
|
//! compute `DataOffset = SMB2_HEADER_LEN + offset_within_body_of_data`.
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// SMB2_READ_REQUEST (MS-SMB2 §2.2.19).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ReadRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub padding: u8,
|
||||||
|
/// 3.0+ flags (`SMB2_READFLAG_*`); reserved on 2.x.
|
||||||
|
pub flags: u8,
|
||||||
|
pub length: u32,
|
||||||
|
pub offset: u64,
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub minimum_count: u32,
|
||||||
|
pub channel: u32,
|
||||||
|
pub remaining_bytes: u32,
|
||||||
|
pub read_channel_info_offset: u16,
|
||||||
|
pub read_channel_info_length: u16,
|
||||||
|
/// MS-SMB2: "If ReadChannelInfoOffset and ReadChannelInfoLength are both
|
||||||
|
/// 0, the client MUST set this field to a single 0 byte." We follow that
|
||||||
|
/// — at least one byte of buffer is required on the wire.
|
||||||
|
#[br(count = if read_channel_info_length == 0 { 1 } else { read_channel_info_length as usize })]
|
||||||
|
pub buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadRequest {
|
||||||
|
/// Flag: SMB2_READFLAG_READ_UNBUFFERED (3.0.2+).
|
||||||
|
pub const FLAG_READ_UNBUFFERED: u8 = 0x01;
|
||||||
|
/// Flag: SMB2_READFLAG_REQUEST_COMPRESSED (3.1.1+).
|
||||||
|
pub const FLAG_REQUEST_COMPRESSED: u8 = 0x02;
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_READ_RESPONSE (MS-SMB2 §2.2.20).
|
||||||
|
///
|
||||||
|
/// `data_offset` is from the start of the SMB2 header. Use
|
||||||
|
/// [`ReadResponse::standard_data_offset`] for the canonical "data immediately
|
||||||
|
/// after the fixed prefix" layout.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ReadResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub data_offset: u8,
|
||||||
|
pub reserved: u8,
|
||||||
|
pub data_length: u32,
|
||||||
|
pub data_remaining: u32,
|
||||||
|
/// 3.x: `Flags`. 2.x: reserved.
|
||||||
|
pub flags: u32,
|
||||||
|
#[br(count = data_length as usize)]
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReadResponse {
|
||||||
|
/// Canonical `DataOffset` value when the data buffer immediately follows
|
||||||
|
/// the fixed 16-byte response prefix and the SMB2 header (64 + 16 = 80).
|
||||||
|
///
|
||||||
|
/// Most servers (ksmbd, Samba) emit 0x50 = 80 here.
|
||||||
|
pub const STANDARD_DATA_OFFSET: u8 = 0x50;
|
||||||
|
|
||||||
|
pub const fn standard_data_offset() -> u8 {
|
||||||
|
Self::STANDARD_DATA_OFFSET
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let r = ReadRequest {
|
||||||
|
structure_size: 49,
|
||||||
|
padding: 0x50,
|
||||||
|
flags: 0,
|
||||||
|
length: 0x1000,
|
||||||
|
offset: 0x2000,
|
||||||
|
file_id: FileId::new(0xAAAA, 0xBBBB),
|
||||||
|
minimum_count: 1,
|
||||||
|
channel: 0,
|
||||||
|
remaining_bytes: 0,
|
||||||
|
read_channel_info_offset: 0,
|
||||||
|
read_channel_info_length: 0,
|
||||||
|
buffer: vec![0],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(ReadRequest::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = ReadResponse {
|
||||||
|
structure_size: 17,
|
||||||
|
data_offset: ReadResponse::STANDARD_DATA_OFFSET,
|
||||||
|
reserved: 0,
|
||||||
|
data_length: 5,
|
||||||
|
data_remaining: 0,
|
||||||
|
flags: 0,
|
||||||
|
data: vec![1, 2, 3, 4, 5],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(ReadResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
vendor/smb-server/src/proto/messages/session_setup.rs
vendored
Normal file
113
vendor/smb-server/src/proto/messages/session_setup.rs
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
//! SESSION_SETUP Request/Response (MS-SMB2 §2.2.5 / §2.2.6).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// SMB2_SESSION_SETUP_REQUEST (MS-SMB2 §2.2.5).
|
||||||
|
///
|
||||||
|
/// `security_buffer` is opaque GSS-API/SPNEGO data — the auth agent decodes it.
|
||||||
|
/// The wire offset is from the start of the SMB2 header; we encode/decode it
|
||||||
|
/// as length-counted data immediately following the fixed prefix, which is
|
||||||
|
/// the canonical layout. Server crate may patch the offset if it needs an
|
||||||
|
/// unusual layout.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SessionSetupRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub flags: u8,
|
||||||
|
pub security_mode: u8,
|
||||||
|
pub capabilities: u32,
|
||||||
|
pub channel: u32,
|
||||||
|
pub security_buffer_offset: u16,
|
||||||
|
pub security_buffer_length: u16,
|
||||||
|
pub previous_session_id: u64,
|
||||||
|
#[br(count = security_buffer_length as usize)]
|
||||||
|
pub security_buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionSetupRequest {
|
||||||
|
/// Flag: SMB2_SESSION_FLAG_BINDING — bind to existing session (3.x).
|
||||||
|
pub const FLAG_BINDING: u8 = 0x01;
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_SESSION_SETUP_RESPONSE (MS-SMB2 §2.2.6).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SessionSetupResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub session_flags: u16,
|
||||||
|
pub security_buffer_offset: u16,
|
||||||
|
pub security_buffer_length: u16,
|
||||||
|
#[br(count = security_buffer_length as usize)]
|
||||||
|
pub security_buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionSetupResponse {
|
||||||
|
/// Session flag: IS_GUEST.
|
||||||
|
pub const FLAG_IS_GUEST: u16 = 0x0001;
|
||||||
|
/// Session flag: IS_NULL (anonymous).
|
||||||
|
pub const FLAG_IS_NULL: u16 = 0x0002;
|
||||||
|
/// Session flag: ENCRYPT_DATA.
|
||||||
|
pub const FLAG_ENCRYPT_DATA: u16 = 0x0004;
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let r = SessionSetupRequest {
|
||||||
|
structure_size: 25,
|
||||||
|
flags: 0,
|
||||||
|
security_mode: 0x01,
|
||||||
|
capabilities: 0x01,
|
||||||
|
channel: 0,
|
||||||
|
security_buffer_offset: 0x58,
|
||||||
|
security_buffer_length: 6,
|
||||||
|
previous_session_id: 0,
|
||||||
|
security_buffer: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(SessionSetupRequest::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = SessionSetupResponse {
|
||||||
|
structure_size: 9,
|
||||||
|
session_flags: SessionSetupResponse::FLAG_IS_GUEST,
|
||||||
|
security_buffer_offset: 0x48,
|
||||||
|
security_buffer_length: 4,
|
||||||
|
security_buffer: vec![1, 2, 3, 4],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(SessionSetupResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
vendor/smb-server/src/proto/messages/set_info.rs
vendored
Normal file
94
vendor/smb-server/src/proto/messages/set_info.rs
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
//! SET_INFO Request/Response (MS-SMB2 §2.2.39 / §2.2.40).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// SMB2_SET_INFO_REQUEST (MS-SMB2 §2.2.39).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SetInfoRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub info_type: u8,
|
||||||
|
pub file_information_class: u8,
|
||||||
|
pub buffer_length: u32,
|
||||||
|
pub buffer_offset: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
pub additional_information: u32,
|
||||||
|
pub file_id: FileId,
|
||||||
|
#[br(count = buffer_length as usize)]
|
||||||
|
pub buffer: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SetInfoRequest {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_SET_INFO_RESPONSE (MS-SMB2 §2.2.40).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SetInfoResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SetInfoResponse {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { structure_size: 2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SetInfoResponse {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let r = SetInfoRequest {
|
||||||
|
structure_size: 33,
|
||||||
|
info_type: 0x01, // File
|
||||||
|
file_information_class: 0x14, // FileEndOfFileInformation
|
||||||
|
buffer_length: 8,
|
||||||
|
buffer_offset: 0x60,
|
||||||
|
reserved: 0,
|
||||||
|
additional_information: 0,
|
||||||
|
file_id: FileId::new(1, 2),
|
||||||
|
buffer: vec![0, 0, 0, 0x10, 0, 0, 0, 0],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(SetInfoRequest::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = SetInfoResponse::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(SetInfoResponse::parse(&buf).unwrap(), r);
|
||||||
|
assert_eq!(buf.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
131
vendor/smb-server/src/proto/messages/tree_connect.rs
vendored
Normal file
131
vendor/smb-server/src/proto/messages/tree_connect.rs
vendored
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
//! TREE_CONNECT Request/Response (MS-SMB2 §2.2.9 / §2.2.10).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// SMB2_TREE_CONNECT_REQUEST (MS-SMB2 §2.2.9).
|
||||||
|
///
|
||||||
|
/// `path` is UTF-16LE. The wire format gives `PathOffset` (from the start of
|
||||||
|
/// the SMB2 header) and `PathLength`; we encode/decode the path immediately
|
||||||
|
/// following the fixed prefix. The 3.1.1 tree-connect-context machinery
|
||||||
|
/// (extension `flags`, `path_offset`/`path_length` interpretation) is
|
||||||
|
/// preserved on the wire and the server crate inspects `flags` if needed.
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TreeConnectRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
/// 3.1.1: flags. 2.x/3.0/3.0.2: reserved.
|
||||||
|
pub flags: u16,
|
||||||
|
pub path_offset: u16,
|
||||||
|
pub path_length: u16,
|
||||||
|
/// UTF-16LE share path bytes (e.g. `\\server\share`).
|
||||||
|
#[br(count = path_length as usize)]
|
||||||
|
pub path: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TreeConnectRequest {
|
||||||
|
/// Flag: SMB2_TREE_CONNECT_FLAG_CLUSTER_RECONNECT (3.1.1).
|
||||||
|
pub const FLAG_CLUSTER_RECONNECT: u16 = 0x0001;
|
||||||
|
/// Flag: SMB2_TREE_CONNECT_FLAG_REDIRECT_TO_OWNER (3.1.1).
|
||||||
|
pub const FLAG_REDIRECT_TO_OWNER: u16 = 0x0002;
|
||||||
|
/// Flag: SMB2_TREE_CONNECT_FLAG_EXTENSION_PRESENT (3.1.1).
|
||||||
|
pub const FLAG_EXTENSION_PRESENT: u16 = 0x0004;
|
||||||
|
|
||||||
|
/// Decode the UTF-16LE share path into a `String`. Returns `None` if the
|
||||||
|
/// stored bytes are not an even length (malformed UTF-16LE).
|
||||||
|
pub fn path_str(&self) -> Option<String> {
|
||||||
|
if !self.path.len().is_multiple_of(2) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let units: Vec<u16> = self
|
||||||
|
.path
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||||
|
.collect();
|
||||||
|
Some(String::from_utf16_lossy(&units))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_TREE_CONNECT_RESPONSE (MS-SMB2 §2.2.10).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TreeConnectResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub share_type: u8,
|
||||||
|
pub reserved: u8,
|
||||||
|
pub share_flags: u32,
|
||||||
|
pub capabilities: u32,
|
||||||
|
pub maximal_access: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TreeConnectResponse {
|
||||||
|
/// Share type: SMB2_SHARE_TYPE_DISK.
|
||||||
|
pub const SHARE_TYPE_DISK: u8 = 0x01;
|
||||||
|
pub const SHARE_TYPE_PIPE: u8 = 0x02;
|
||||||
|
pub const SHARE_TYPE_PRINT: u8 = 0x03;
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn utf16le(s: &str) -> Vec<u8> {
|
||||||
|
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let path = utf16le(r"\\server\share");
|
||||||
|
let r = TreeConnectRequest {
|
||||||
|
structure_size: 9,
|
||||||
|
flags: 0,
|
||||||
|
path_offset: 0x48,
|
||||||
|
path_length: path.len() as u16,
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
let decoded = TreeConnectRequest::parse(&buf).unwrap();
|
||||||
|
assert_eq!(decoded, r);
|
||||||
|
assert_eq!(decoded.path_str().unwrap(), r"\\server\share");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = TreeConnectResponse {
|
||||||
|
structure_size: 16,
|
||||||
|
share_type: TreeConnectResponse::SHARE_TYPE_DISK,
|
||||||
|
reserved: 0,
|
||||||
|
share_flags: 0,
|
||||||
|
capabilities: 0,
|
||||||
|
maximal_access: 0x001F_01FF,
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(TreeConnectResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
vendor/smb-server/src/proto/messages/tree_disconnect.rs
vendored
Normal file
77
vendor/smb-server/src/proto/messages/tree_disconnect.rs
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//! TREE_DISCONNECT Request/Response (MS-SMB2 §2.2.11 / §2.2.12).
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TreeDisconnectRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TreeDisconnectRequest {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 4,
|
||||||
|
reserved: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TreeDisconnectResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TreeDisconnectResponse {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 4,
|
||||||
|
reserved: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_codec {
|
||||||
|
($t:ty) => {
|
||||||
|
impl $t {
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(<Self as BinRead>::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_codec!(TreeDisconnectRequest);
|
||||||
|
impl_codec!(TreeDisconnectResponse);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips() {
|
||||||
|
let r = TreeDisconnectRequest::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(TreeDisconnectRequest::parse(&buf).unwrap(), r);
|
||||||
|
|
||||||
|
let r = TreeDisconnectResponse::default();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(TreeDisconnectResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
vendor/smb-server/src/proto/messages/write.rs
vendored
Normal file
123
vendor/smb-server/src/proto/messages/write.rs
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
//! WRITE Request/Response (MS-SMB2 §2.2.21 / §2.2.22).
|
||||||
|
//!
|
||||||
|
//! ## Data buffer offsets
|
||||||
|
//!
|
||||||
|
//! `DataOffset` is from the **start of the SMB2 header**, not from the start
|
||||||
|
//! of this structure (MS-SMB2 §2.2.21). The canonical layout puts the data
|
||||||
|
//! immediately after the fixed 48-byte prefix, giving 64 + 48 = 112 = 0x70.
|
||||||
|
|
||||||
|
use binrw::{BinRead, BinWrite, binrw};
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::create::FileId;
|
||||||
|
use crate::proto::error::ProtoResult;
|
||||||
|
|
||||||
|
/// SMB2_WRITE_REQUEST (MS-SMB2 §2.2.21).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct WriteRequest {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub data_offset: u16,
|
||||||
|
pub length: u32,
|
||||||
|
pub offset: u64,
|
||||||
|
pub file_id: FileId,
|
||||||
|
pub channel: u32,
|
||||||
|
pub remaining_bytes: u32,
|
||||||
|
pub write_channel_info_offset: u16,
|
||||||
|
pub write_channel_info_length: u16,
|
||||||
|
pub flags: u32,
|
||||||
|
/// MS-SMB2: at least 1 byte of payload buffer is required on the wire
|
||||||
|
/// even when length=0.
|
||||||
|
#[br(count = if length == 0 { 1 } else { length as usize })]
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteRequest {
|
||||||
|
/// Canonical `DataOffset` placing the data buffer immediately after the
|
||||||
|
/// fixed 48-byte WRITE prefix: 64 (SMB2 header) + 48 = 112 = 0x70.
|
||||||
|
pub const STANDARD_DATA_OFFSET: u16 = 0x70;
|
||||||
|
/// Flag: SMB2_WRITEFLAG_WRITE_THROUGH.
|
||||||
|
pub const FLAG_WRITE_THROUGH: u32 = 0x0000_0001;
|
||||||
|
/// Flag: SMB2_WRITEFLAG_WRITE_UNBUFFERED (3.0.2+).
|
||||||
|
pub const FLAG_WRITE_UNBUFFERED: u32 = 0x0000_0002;
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SMB2_WRITE_RESPONSE (MS-SMB2 §2.2.22).
|
||||||
|
#[binrw]
|
||||||
|
#[brw(little)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct WriteResponse {
|
||||||
|
pub structure_size: u16,
|
||||||
|
pub reserved: u16,
|
||||||
|
pub count: u32,
|
||||||
|
pub remaining: u32,
|
||||||
|
pub write_channel_info_offset: u16,
|
||||||
|
pub write_channel_info_length: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteResponse {
|
||||||
|
pub fn new(count: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
structure_size: 17,
|
||||||
|
reserved: 0,
|
||||||
|
count,
|
||||||
|
remaining: 0,
|
||||||
|
write_channel_info_offset: 0,
|
||||||
|
write_channel_info_length: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(buf: &[u8]) -> ProtoResult<Self> {
|
||||||
|
Ok(Self::read(&mut Cursor::new(buf))?)
|
||||||
|
}
|
||||||
|
pub fn write_to(&self, out: &mut Vec<u8>) -> ProtoResult<()> {
|
||||||
|
let mut c = Cursor::new(Vec::new());
|
||||||
|
BinWrite::write(self, &mut c)?;
|
||||||
|
out.extend_from_slice(&c.into_inner());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_round_trips() {
|
||||||
|
let r = WriteRequest {
|
||||||
|
structure_size: 49,
|
||||||
|
data_offset: WriteRequest::STANDARD_DATA_OFFSET,
|
||||||
|
length: 4,
|
||||||
|
offset: 0x100,
|
||||||
|
file_id: FileId::new(0xAA, 0xBB),
|
||||||
|
channel: 0,
|
||||||
|
remaining_bytes: 0,
|
||||||
|
write_channel_info_offset: 0,
|
||||||
|
write_channel_info_length: 0,
|
||||||
|
flags: 0,
|
||||||
|
data: vec![1, 2, 3, 4],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(WriteRequest::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trips() {
|
||||||
|
let r = WriteResponse::new(0x1000);
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
r.write_to(&mut buf).unwrap();
|
||||||
|
assert_eq!(WriteResponse::parse(&buf).unwrap(), r);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
vendor/smb-server/src/proto/mod.rs
vendored
Normal file
16
vendor/smb-server/src/proto/mod.rs
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//! SMB2/3 wire-format types, framing, signing, and authentication primitives.
|
||||||
|
//!
|
||||||
|
//! Layered into:
|
||||||
|
//! * [`framing`] — Direct-TCP/NetBIOS transport framing.
|
||||||
|
//! * [`header`] — SMB2 64-byte fixed header.
|
||||||
|
//! * [`messages`] — Per-command request/response structs.
|
||||||
|
//! * [`auth`] — NTLMv2 server-side authentication and minimal SPNEGO.
|
||||||
|
//! * [`crypto`] — Signing, key derivation, pre-auth integrity.
|
||||||
|
//! * [`error`] — Crate-wide error type.
|
||||||
|
|
||||||
|
pub mod auth;
|
||||||
|
pub mod crypto;
|
||||||
|
pub mod error;
|
||||||
|
pub mod framing;
|
||||||
|
pub mod header;
|
||||||
|
pub mod messages;
|
||||||
566
vendor/smb-server/src/server.rs
vendored
Normal file
566
vendor/smb-server/src/server.rs
vendored
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
//! Top-level `SmbServer` lifecycle: builder integration, accept loop,
|
||||||
|
//! graceful shutdown.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
|
use crate::proto::auth::ntlm::UserCreds;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tokio::sync::{Notify, RwLock};
|
||||||
|
use tracing::{Instrument, error, info, info_span};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::backend::ShareBackend;
|
||||||
|
use crate::builder::{Access, Share, SmbServerBuilder};
|
||||||
|
use crate::conn::connection_loop;
|
||||||
|
use crate::conn::state::Connection;
|
||||||
|
use crate::utils::now_filetime;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ShareMode / ShareBindings
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ShareMode {
|
||||||
|
Public,
|
||||||
|
PublicReadOnly,
|
||||||
|
/// Default — closed share. Only users in the explicit `users` map allowed.
|
||||||
|
AuthenticatedOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ShareAcl {
|
||||||
|
pub mode: ShareMode,
|
||||||
|
pub users: HashMap<String, Access>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compiled binding for a single share — the per-server-state form of `Share`.
|
||||||
|
pub struct ShareBindings {
|
||||||
|
pub name: String,
|
||||||
|
pub backend: Arc<dyn ShareBackend>,
|
||||||
|
pub acl: RwLock<ShareAcl>,
|
||||||
|
/// `IPC$` synthetic share. Accepted at TREE_CONNECT for client compatibility
|
||||||
|
/// (Windows always probes IPC$ before mounting an actual share). All
|
||||||
|
/// downstream ops on an IPC$ tree return `STATUS_NOT_SUPPORTED`.
|
||||||
|
pub is_ipc: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShareBindings {
|
||||||
|
pub(crate) fn new(
|
||||||
|
name: String,
|
||||||
|
backend: Arc<dyn ShareBackend>,
|
||||||
|
mode: ShareMode,
|
||||||
|
users: HashMap<String, Access>,
|
||||||
|
is_ipc: bool,
|
||||||
|
) -> Arc<Self> {
|
||||||
|
Arc::new(Self {
|
||||||
|
name,
|
||||||
|
backend,
|
||||||
|
acl: RwLock::new(ShareAcl { mode, users }),
|
||||||
|
is_ipc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthetic IPC$ share. The backend is a no-op; clients that try to
|
||||||
|
/// CREATE on it get `STATUS_NOT_SUPPORTED` from the CREATE handler.
|
||||||
|
pub fn ipc() -> Arc<Self> {
|
||||||
|
Self::new(
|
||||||
|
"IPC$".to_string(),
|
||||||
|
Arc::new(crate::backend::NotSupportedBackend),
|
||||||
|
ShareMode::PublicReadOnly,
|
||||||
|
HashMap::new(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ServerConfig / ServerUsers / ServerState
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
pub listen_addr: SocketAddr,
|
||||||
|
pub netbios_name: String,
|
||||||
|
pub max_read_size: u32,
|
||||||
|
pub max_write_size: u32,
|
||||||
|
pub server_guid: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ServerUsers {
|
||||||
|
/// Username → precomputed NT hash record.
|
||||||
|
pub table: RwLock<HashMap<String, UserCreds>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ServerShares {
|
||||||
|
by_name: RwLock<HashMap<String, Arc<ShareBindings>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerShares {
|
||||||
|
pub fn new(shares: Vec<Arc<ShareBindings>>) -> Self {
|
||||||
|
let mut by_name = HashMap::with_capacity(shares.len());
|
||||||
|
for share in shares {
|
||||||
|
by_name.insert(share.name.to_ascii_lowercase(), share);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
by_name: RwLock::new(by_name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find(&self, name: &str) -> Option<Arc<ShareBindings>> {
|
||||||
|
self.by_name
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get(&name.to_ascii_lowercase())
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(&self, share: Arc<ShareBindings>) -> Result<(), ConfigError> {
|
||||||
|
let key = share.name.to_ascii_lowercase();
|
||||||
|
let mut by_name = self.by_name.write().await;
|
||||||
|
if by_name.contains_key(&key) {
|
||||||
|
return Err(ConfigError::DuplicateShare(share.name.clone()));
|
||||||
|
}
|
||||||
|
by_name.insert(key, share);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove(&self, name: &str) -> Option<Arc<ShareBindings>> {
|
||||||
|
self.by_name
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.remove(&name.to_ascii_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn all(&self) -> Vec<Arc<ShareBindings>> {
|
||||||
|
self.by_name.read().await.values().cloned().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActiveConnections {
|
||||||
|
next_id: AtomicU64,
|
||||||
|
conns: RwLock<HashMap<u64, Weak<Connection>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveConnections {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
next_id: AtomicU64::new(1),
|
||||||
|
conns: RwLock::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register(&self, conn: &Arc<Connection>) -> u64 {
|
||||||
|
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||||
|
self.conns.write().await.insert(id, Arc::downgrade(conn));
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unregister(&self, id: u64) {
|
||||||
|
self.conns.write().await.remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn live(&self) -> Vec<Arc<Connection>> {
|
||||||
|
let mut live = Vec::new();
|
||||||
|
let mut conns = self.conns.write().await;
|
||||||
|
conns.retain(|_, weak| {
|
||||||
|
if let Some(conn) = weak.upgrade() {
|
||||||
|
live.push(conn);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
live
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ActiveConnections {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top-level immutable-ish state shared across connections.
|
||||||
|
pub struct ServerState {
|
||||||
|
pub config: ServerConfig,
|
||||||
|
pub users: ServerUsers,
|
||||||
|
pub shares: ServerShares,
|
||||||
|
pub active_connections: ActiveConnections,
|
||||||
|
pub server_start_filetime: u64,
|
||||||
|
/// Set when `shutdown()` is invoked; the accept loop stops on the next
|
||||||
|
/// iteration and connection loops abandon their next read.
|
||||||
|
pub shutdown: Arc<Notify>,
|
||||||
|
pub shutting_down: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerState {
|
||||||
|
pub fn new(config: ServerConfig, users: ServerUsers, shares: Vec<Arc<ShareBindings>>) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
users,
|
||||||
|
shares: ServerShares::new(shares),
|
||||||
|
active_connections: ActiveConnections::new(),
|
||||||
|
server_start_filetime: now_filetime(),
|
||||||
|
shutdown: Arc::new(Notify::new()),
|
||||||
|
shutting_down: Arc::new(AtomicBool::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a share by case-insensitive name.
|
||||||
|
pub async fn find_share(&self, name: &str) -> Option<Arc<ShareBindings>> {
|
||||||
|
self.shares.find(name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up a user's NT hash by name.
|
||||||
|
pub async fn lookup_user(&self, name: &str) -> Option<UserCreds> {
|
||||||
|
self.users.table.read().await.get(name).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether anonymous logon is permitted (i.e. at least one share is public).
|
||||||
|
pub async fn anonymous_allowed(&self) -> bool {
|
||||||
|
for share in self.shares.all().await {
|
||||||
|
let acl = share.acl.read().await;
|
||||||
|
if matches!(acl.mode, ShareMode::Public | ShareMode::PublicReadOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, PartialEq, Eq)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("user `{0}` does not exist")]
|
||||||
|
UnknownUser(String),
|
||||||
|
#[error("share `{0}` does not exist")]
|
||||||
|
UnknownShare(String),
|
||||||
|
#[error("duplicate share `{0}`")]
|
||||||
|
DuplicateShare(String),
|
||||||
|
#[error("share `{0}` mixes public mode with explicit users")]
|
||||||
|
PublicMixedWithUsers(String),
|
||||||
|
#[error("user name `{0}` is reserved")]
|
||||||
|
ReservedUserName(String),
|
||||||
|
#[error("user name must be non-empty")]
|
||||||
|
EmptyUserName,
|
||||||
|
#[error("share name `{0}` is reserved")]
|
||||||
|
ReservedShareName(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ConfigHandle {
|
||||||
|
state: Arc<ServerState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigHandle {
|
||||||
|
pub async fn add_user(
|
||||||
|
&self,
|
||||||
|
name: impl Into<String>,
|
||||||
|
password: impl AsRef<str>,
|
||||||
|
) -> Result<(), ConfigError> {
|
||||||
|
let name = name.into();
|
||||||
|
validate_user_name(&name)?;
|
||||||
|
let creds = UserCreds::from_password(password.as_ref());
|
||||||
|
self.state.users.table.write().await.insert(name, creds);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_user(&self, name: &str) -> Result<(), ConfigError> {
|
||||||
|
validate_user_name(name)?;
|
||||||
|
let removed = self.state.users.table.write().await.remove(name);
|
||||||
|
if removed.is_none() {
|
||||||
|
return Err(ConfigError::UnknownUser(name.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for share in self.state.shares.all().await {
|
||||||
|
share.acl.write().await.users.remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for conn in self.state.active_connections.live().await {
|
||||||
|
conn.close_sessions_for_user(name).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_share(&self, share: Share) -> Result<(), ConfigError> {
|
||||||
|
validate_share_name(&share.name)?;
|
||||||
|
let is_public = matches!(share.mode, ShareMode::Public | ShareMode::PublicReadOnly);
|
||||||
|
if is_public && !share.users.is_empty() {
|
||||||
|
return Err(ConfigError::PublicMixedWithUsers(share.name));
|
||||||
|
}
|
||||||
|
let users = self.state.users.table.read().await;
|
||||||
|
for user in share.users.keys() {
|
||||||
|
if !users.contains_key(user) {
|
||||||
|
return Err(ConfigError::UnknownUser(user.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let binding = ShareBindings::new(share.name, share.backend, share.mode, share.users, false);
|
||||||
|
self.state.shares.insert(binding).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_share(&self, name: &str) -> Result<(), ConfigError> {
|
||||||
|
validate_share_name(name)?;
|
||||||
|
let removed = self.state.shares.remove(name).await;
|
||||||
|
if removed.is_none() {
|
||||||
|
return Err(ConfigError::UnknownShare(name.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for conn in self.state.active_connections.live().await {
|
||||||
|
conn.close_trees_for_share(name).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn grant_share_user(
|
||||||
|
&self,
|
||||||
|
share_name: &str,
|
||||||
|
user: &str,
|
||||||
|
access: Access,
|
||||||
|
) -> Result<(), ConfigError> {
|
||||||
|
validate_user_name(user)?;
|
||||||
|
validate_share_name(share_name)?;
|
||||||
|
let users = self.state.users.table.read().await;
|
||||||
|
if !users.contains_key(user) {
|
||||||
|
return Err(ConfigError::UnknownUser(user.to_string()));
|
||||||
|
}
|
||||||
|
let share = self
|
||||||
|
.state
|
||||||
|
.find_share(share_name)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| ConfigError::UnknownShare(share_name.to_string()))?;
|
||||||
|
let mut acl = share.acl.write().await;
|
||||||
|
if matches!(acl.mode, ShareMode::Public | ShareMode::PublicReadOnly) {
|
||||||
|
return Err(ConfigError::PublicMixedWithUsers(share.name.clone()));
|
||||||
|
}
|
||||||
|
acl.users.insert(user.to_string(), access);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_share_user(&self, share_name: &str, user: &str) -> Result<(), ConfigError> {
|
||||||
|
validate_user_name(user)?;
|
||||||
|
validate_share_name(share_name)?;
|
||||||
|
let share = self
|
||||||
|
.state
|
||||||
|
.find_share(share_name)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| ConfigError::UnknownShare(share_name.to_string()))?;
|
||||||
|
share.acl.write().await.users.remove(user);
|
||||||
|
|
||||||
|
for conn in self.state.active_connections.live().await {
|
||||||
|
conn.close_trees_for_user_share(user, share_name).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_share_mode(
|
||||||
|
&self,
|
||||||
|
share_name: &str,
|
||||||
|
mode: ShareMode,
|
||||||
|
) -> Result<(), ConfigError> {
|
||||||
|
validate_share_name(share_name)?;
|
||||||
|
let share = self
|
||||||
|
.state
|
||||||
|
.find_share(share_name)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| ConfigError::UnknownShare(share_name.to_string()))?;
|
||||||
|
let mut acl = share.acl.write().await;
|
||||||
|
if matches!(mode, ShareMode::Public | ShareMode::PublicReadOnly) && !acl.users.is_empty() {
|
||||||
|
return Err(ConfigError::PublicMixedWithUsers(share.name.clone()));
|
||||||
|
}
|
||||||
|
if acl.mode == mode {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
acl.mode = mode;
|
||||||
|
drop(acl);
|
||||||
|
|
||||||
|
for conn in self.state.active_connections.live().await {
|
||||||
|
conn.close_trees_for_share(share_name).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_user_name(name: &str) -> Result<(), ConfigError> {
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ConfigError::EmptyUserName);
|
||||||
|
}
|
||||||
|
if name.eq_ignore_ascii_case("anonymous") {
|
||||||
|
return Err(ConfigError::ReservedUserName(name.to_string()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_share_name(name: &str) -> Result<(), ConfigError> {
|
||||||
|
if name.eq_ignore_ascii_case("IPC$") {
|
||||||
|
return Err(ConfigError::ReservedShareName(name.to_string()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SmbServer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A built but not-yet-running SMB server.
|
||||||
|
///
|
||||||
|
/// Use `serve()` to bind the configured listener and run until shutdown.
|
||||||
|
pub struct SmbServer {
|
||||||
|
state: Arc<ServerState>,
|
||||||
|
/// The listener is bound lazily inside `serve()` so we can return a
|
||||||
|
/// useful `local_addr` only after binding. Pre-bind helpers: `serve` is
|
||||||
|
/// the only path that opens the socket.
|
||||||
|
bound: tokio::sync::Mutex<Option<TcpListener>>,
|
||||||
|
/// Resolved local address once `bind_local()` has been called. Tests
|
||||||
|
/// expect to ask for the address before serving (port 0 case).
|
||||||
|
local_addr: tokio::sync::Mutex<Option<SocketAddr>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmbServer {
|
||||||
|
pub fn builder() -> SmbServerBuilder {
|
||||||
|
SmbServerBuilder::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_state(state: ServerState) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Arc::new(state),
|
||||||
|
bound: tokio::sync::Mutex::new(None),
|
||||||
|
local_addr: tokio::sync::Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config_handle(&self) -> ConfigHandle {
|
||||||
|
ConfigHandle {
|
||||||
|
state: self.state.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bind the configured listen address without yet entering the accept
|
||||||
|
/// loop. Required for tests that need the actual port (e.g. when the
|
||||||
|
/// builder used port 0).
|
||||||
|
pub async fn bind(&self) -> io::Result<SocketAddr> {
|
||||||
|
let mut bound = self.bound.lock().await;
|
||||||
|
if let Some(l) = bound.as_ref() {
|
||||||
|
return l.local_addr();
|
||||||
|
}
|
||||||
|
let listener = TcpListener::bind(self.state.config.listen_addr).await?;
|
||||||
|
let addr = listener.local_addr()?;
|
||||||
|
*bound = Some(listener);
|
||||||
|
*self.local_addr.lock().await = Some(addr);
|
||||||
|
Ok(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the actual bound address. `None` if `bind()`/`serve()` have
|
||||||
|
/// not yet been called.
|
||||||
|
pub async fn local_addr(&self) -> Option<SocketAddr> {
|
||||||
|
*self.local_addr.lock().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configured listen address (the *intended* address; may be `0.0.0.0:0`
|
||||||
|
/// before binding).
|
||||||
|
pub fn configured_addr(&self) -> SocketAddr {
|
||||||
|
self.state.config.listen_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initiate a graceful shutdown. Stops the accept loop and lets in-flight
|
||||||
|
/// connection tasks complete.
|
||||||
|
pub fn shutdown(&self) {
|
||||||
|
self.state.shutting_down.store(true, Ordering::Release);
|
||||||
|
self.state.shutdown.notify_waiters();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a clonable handle that can request shutdown after `serve()`
|
||||||
|
/// has consumed the `SmbServer` value.
|
||||||
|
pub fn shutdown_handle(&self) -> ShutdownHandle {
|
||||||
|
ShutdownHandle {
|
||||||
|
shutdown: self.state.shutdown.clone(),
|
||||||
|
shutting_down: self.state.shutting_down.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the accept loop until `shutdown()` is called.
|
||||||
|
pub async fn serve(self) -> io::Result<()> {
|
||||||
|
// Ensure the listener is bound. (The user may also have called
|
||||||
|
// `bind()` to pre-extract `local_addr()` for a test.)
|
||||||
|
if self.bound.lock().await.is_none() {
|
||||||
|
self.bind().await?;
|
||||||
|
}
|
||||||
|
let listener = self
|
||||||
|
.bound
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.take()
|
||||||
|
.expect("listener bound above");
|
||||||
|
let local = listener.local_addr().ok();
|
||||||
|
let span = info_span!("smb_server", listen = ?local);
|
||||||
|
async move {
|
||||||
|
info!("server starting");
|
||||||
|
let state = self.state.clone();
|
||||||
|
let shutdown = state.shutdown.clone();
|
||||||
|
let shutting_down = state.shutting_down.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = shutdown.notified() => {
|
||||||
|
info!("shutdown requested; stopping accept loop");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
accept = listener.accept() => {
|
||||||
|
match accept {
|
||||||
|
Ok((stream, peer)) => {
|
||||||
|
if shutting_down.load(Ordering::Acquire) {
|
||||||
|
drop(stream);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let server_state = state.clone();
|
||||||
|
let span = info_span!("conn", peer = %peer);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = connection_loop(stream, server_state).await {
|
||||||
|
error!(error = %e, "connection loop exited with error");
|
||||||
|
}
|
||||||
|
}.instrument(span));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "accept failed");
|
||||||
|
if shutting_down.load(Ordering::Acquire) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info!("server stopped");
|
||||||
|
Ok::<(), io::Error>(())
|
||||||
|
}
|
||||||
|
.instrument(span)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access shared state for in-crate tests/integrations.
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn state(&self) -> Arc<ServerState> {
|
||||||
|
self.state.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cheaply-clonable shutdown handle. Outlives `SmbServer::serve` consuming
|
||||||
|
/// the server.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ShutdownHandle {
|
||||||
|
shutdown: Arc<Notify>,
|
||||||
|
shutting_down: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShutdownHandle {
|
||||||
|
/// Request a graceful shutdown.
|
||||||
|
pub fn shutdown(&self) {
|
||||||
|
self.shutting_down.store(true, Ordering::Release);
|
||||||
|
self.shutdown.notify_waiters();
|
||||||
|
}
|
||||||
|
}
|
||||||
173
vendor/smb-server/src/tests/dynamic_config.rs
vendored
Normal file
173
vendor/smb-server/src/tests/dynamic_config.rs
vendored
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::memfs::MemFsBackend;
|
||||||
|
use crate::conn::state::{Connection, Session, TreeConnect};
|
||||||
|
use crate::server::ConfigError;
|
||||||
|
use crate::{Access, Identity, Share, ShareMode, SmbServer};
|
||||||
|
|
||||||
|
fn test_server() -> SmbServer {
|
||||||
|
SmbServer::builder()
|
||||||
|
.listen("127.0.0.1:0".parse().unwrap())
|
||||||
|
.user("alice", "password")
|
||||||
|
.share(
|
||||||
|
Share::new("home", MemFsBackend::new().with_file("seed.txt", b""))
|
||||||
|
.user("alice", Access::ReadWrite),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.expect("build")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn public_server() -> SmbServer {
|
||||||
|
SmbServer::builder()
|
||||||
|
.listen("127.0.0.1:0".parse().unwrap())
|
||||||
|
.share(Share::new("public", MemFsBackend::new()).public())
|
||||||
|
.build()
|
||||||
|
.expect("build")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register_session(
|
||||||
|
server: &SmbServer,
|
||||||
|
identity: Identity,
|
||||||
|
share_name: &str,
|
||||||
|
) -> Arc<Connection> {
|
||||||
|
let state = server.state();
|
||||||
|
let conn = Arc::new(Connection::new(
|
||||||
|
state.config.server_guid,
|
||||||
|
state.config.max_read_size,
|
||||||
|
state.config.max_write_size,
|
||||||
|
));
|
||||||
|
state.active_connections.register(&conn).await;
|
||||||
|
|
||||||
|
let session = Session::new(1, identity, [0; 16], [0; 16], false, None);
|
||||||
|
let session = Arc::new(tokio::sync::RwLock::new(session));
|
||||||
|
let share = state.find_share(share_name).await.expect("share");
|
||||||
|
let tree = Arc::new(tokio::sync::RwLock::new(TreeConnect::new(
|
||||||
|
1,
|
||||||
|
share,
|
||||||
|
Access::ReadWrite,
|
||||||
|
)));
|
||||||
|
{
|
||||||
|
let sess = session.read().await;
|
||||||
|
sess.trees.write().await.insert(1, tree);
|
||||||
|
}
|
||||||
|
conn.sessions.write().await.insert(1, session);
|
||||||
|
conn
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn register_alice_session(server: &SmbServer) -> Arc<Connection> {
|
||||||
|
register_session(
|
||||||
|
server,
|
||||||
|
Identity::User {
|
||||||
|
user: "alice".to_string(),
|
||||||
|
domain: String::new(),
|
||||||
|
},
|
||||||
|
"home",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn config_handle_adds_users_and_shares() {
|
||||||
|
let server = SmbServer::builder()
|
||||||
|
.listen("127.0.0.1:0".parse().unwrap())
|
||||||
|
.build()
|
||||||
|
.expect("build");
|
||||||
|
let config = server.config_handle();
|
||||||
|
|
||||||
|
config.add_user("bob", "password").await.expect("add user");
|
||||||
|
config
|
||||||
|
.add_share(Share::new("media", MemFsBackend::new()).user("bob", Access::Read))
|
||||||
|
.await
|
||||||
|
.expect("add share");
|
||||||
|
|
||||||
|
let state = server.state();
|
||||||
|
assert!(state.lookup_user("bob").await.is_some());
|
||||||
|
assert!(state.find_share("media").await.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn removing_user_revokes_active_sessions() {
|
||||||
|
let server = test_server();
|
||||||
|
let conn = register_alice_session(&server).await;
|
||||||
|
|
||||||
|
server
|
||||||
|
.config_handle()
|
||||||
|
.remove_user("alice")
|
||||||
|
.await
|
||||||
|
.expect("remove user");
|
||||||
|
|
||||||
|
assert!(server.state().lookup_user("alice").await.is_none());
|
||||||
|
assert!(conn.sessions.read().await.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn removing_share_revokes_active_trees() {
|
||||||
|
let server = test_server();
|
||||||
|
let conn = register_alice_session(&server).await;
|
||||||
|
|
||||||
|
server
|
||||||
|
.config_handle()
|
||||||
|
.remove_share("home")
|
||||||
|
.await
|
||||||
|
.expect("remove share");
|
||||||
|
|
||||||
|
assert!(server.state().find_share("home").await.is_none());
|
||||||
|
let sessions = conn.sessions.read().await;
|
||||||
|
let session = sessions.get(&1).expect("session remains").read().await;
|
||||||
|
assert!(session.trees.read().await.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn revoking_user_from_share_revokes_only_that_tree() {
|
||||||
|
let server = test_server();
|
||||||
|
let conn = register_alice_session(&server).await;
|
||||||
|
|
||||||
|
server
|
||||||
|
.config_handle()
|
||||||
|
.revoke_share_user("home", "alice")
|
||||||
|
.await
|
||||||
|
.expect("revoke user share");
|
||||||
|
|
||||||
|
assert!(conn.sessions.read().await.contains_key(&1));
|
||||||
|
let sessions = conn.sessions.read().await;
|
||||||
|
let session = sessions.get(&1).expect("session remains").read().await;
|
||||||
|
assert!(session.trees.read().await.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn changing_share_mode_revokes_active_trees() {
|
||||||
|
let server = public_server();
|
||||||
|
let conn = register_session(&server, Identity::Anonymous, "public").await;
|
||||||
|
|
||||||
|
server
|
||||||
|
.config_handle()
|
||||||
|
.set_share_mode("public", ShareMode::PublicReadOnly)
|
||||||
|
.await
|
||||||
|
.expect("set mode");
|
||||||
|
|
||||||
|
let sessions = conn.sessions.read().await;
|
||||||
|
let session = sessions.get(&1).expect("session remains").read().await;
|
||||||
|
assert!(session.trees.read().await.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn public_share_cannot_mix_explicit_users() {
|
||||||
|
let server = SmbServer::builder()
|
||||||
|
.listen("127.0.0.1:0".parse().unwrap())
|
||||||
|
.share(Share::new("public", MemFsBackend::new()).public())
|
||||||
|
.build()
|
||||||
|
.expect("build");
|
||||||
|
|
||||||
|
let config = server.config_handle();
|
||||||
|
config
|
||||||
|
.add_user("alice", "password")
|
||||||
|
.await
|
||||||
|
.expect("add user");
|
||||||
|
|
||||||
|
let err = config
|
||||||
|
.grant_share_user("public", "alice", Access::Read)
|
||||||
|
.await
|
||||||
|
.expect_err("grant should fail");
|
||||||
|
|
||||||
|
assert_eq!(err, ConfigError::PublicMixedWithUsers("public".to_string()));
|
||||||
|
}
|
||||||
300
vendor/smb-server/src/tests/memfs.rs
vendored
Normal file
300
vendor/smb-server/src/tests/memfs.rs
vendored
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::backend::{
|
||||||
|
BackendCapabilities, DirEntry, FileInfo, FileTimes, Handle, OpenIntent, OpenOptions,
|
||||||
|
ShareBackend,
|
||||||
|
};
|
||||||
|
use crate::error::{SmbError, SmbResult};
|
||||||
|
use crate::path::SmbPath;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
/// Minimal in-memory FS used by integration tests. Files are byte vectors,
|
||||||
|
/// directories are sets of names. Not threadsafe across workers — only used
|
||||||
|
/// within one test.
|
||||||
|
pub struct MemFsBackend {
|
||||||
|
inner: std::sync::Arc<Mutex<MemInner>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct MemInner {
|
||||||
|
files: HashMap<String, Vec<u8>>,
|
||||||
|
/// All directories present (always includes "" for the root). Each
|
||||||
|
/// directory is keyed by canonical path string.
|
||||||
|
dirs: HashMap<String, ()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MemFsBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemFsBackend {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut inner = MemInner::default();
|
||||||
|
inner.dirs.insert(String::new(), ());
|
||||||
|
Self {
|
||||||
|
inner: std::sync::Arc::new(Mutex::new(inner)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_file(self, path: &str, contents: &[u8]) -> Self {
|
||||||
|
{
|
||||||
|
let mut g = self.inner.lock().unwrap();
|
||||||
|
g.files.insert(path.to_string(), contents.to_vec());
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(path: &SmbPath) -> String {
|
||||||
|
path.display_backslash()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ShareBackend for MemFsBackend {
|
||||||
|
async fn open(&self, path: &SmbPath, opts: OpenOptions) -> SmbResult<Box<dyn Handle>> {
|
||||||
|
let k = key(path);
|
||||||
|
let mut g = self.inner.lock().unwrap();
|
||||||
|
let exists_file = g.files.contains_key(&k);
|
||||||
|
let exists_dir = g.dirs.contains_key(&k);
|
||||||
|
|
||||||
|
if opts.directory {
|
||||||
|
if exists_file {
|
||||||
|
return Err(SmbError::NotADirectory);
|
||||||
|
}
|
||||||
|
if !exists_dir {
|
||||||
|
if matches!(opts.intent, OpenIntent::Create | OpenIntent::OpenOrCreate) {
|
||||||
|
g.dirs.insert(k.clone(), ());
|
||||||
|
} else {
|
||||||
|
return Err(SmbError::NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(Box::new(MemHandle::dir(self.inner.clone(), k)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists_dir {
|
||||||
|
return Err(SmbError::IsDirectory);
|
||||||
|
}
|
||||||
|
match opts.intent {
|
||||||
|
OpenIntent::Open => {
|
||||||
|
if !exists_file {
|
||||||
|
return Err(SmbError::NotFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OpenIntent::Create => {
|
||||||
|
if exists_file {
|
||||||
|
return Err(SmbError::Exists);
|
||||||
|
}
|
||||||
|
g.files.insert(k.clone(), Vec::new());
|
||||||
|
}
|
||||||
|
OpenIntent::OpenOrCreate => {
|
||||||
|
g.files.entry(k.clone()).or_default();
|
||||||
|
}
|
||||||
|
OpenIntent::Truncate => {
|
||||||
|
if !exists_file {
|
||||||
|
return Err(SmbError::NotFound);
|
||||||
|
}
|
||||||
|
g.files.insert(k.clone(), Vec::new());
|
||||||
|
}
|
||||||
|
OpenIntent::OverwriteOrCreate => {
|
||||||
|
g.files.insert(k.clone(), Vec::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Box::new(MemHandle::file(self.inner.clone(), k)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unlink(&self, path: &SmbPath) -> SmbResult<()> {
|
||||||
|
let k = key(path);
|
||||||
|
let mut g = self.inner.lock().unwrap();
|
||||||
|
if g.files.remove(&k).is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if g.dirs.remove(&k).is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(SmbError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rename(&self, from: &SmbPath, to: &SmbPath) -> SmbResult<()> {
|
||||||
|
let kf = key(from);
|
||||||
|
let kt = key(to);
|
||||||
|
let mut g = self.inner.lock().unwrap();
|
||||||
|
if g.files.contains_key(&kt) || g.dirs.contains_key(&kt) {
|
||||||
|
return Err(SmbError::Exists);
|
||||||
|
}
|
||||||
|
if let Some(data) = g.files.remove(&kf) {
|
||||||
|
g.files.insert(kt, data);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if g.dirs.remove(&kf).is_some() {
|
||||||
|
g.dirs.insert(kt, ());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(SmbError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capabilities(&self) -> BackendCapabilities {
|
||||||
|
BackendCapabilities {
|
||||||
|
is_read_only: false,
|
||||||
|
case_sensitive: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MemHandle {
|
||||||
|
inner: std::sync::Arc<Mutex<MemInner>>,
|
||||||
|
key: String,
|
||||||
|
is_dir: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemHandle {
|
||||||
|
fn file(inner: std::sync::Arc<Mutex<MemInner>>, key: String) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
key,
|
||||||
|
is_dir: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir(inner: std::sync::Arc<Mutex<MemInner>>, key: String) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
key,
|
||||||
|
is_dir: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handle for MemHandle {
|
||||||
|
async fn read(&self, offset: u64, len: u32) -> SmbResult<Bytes> {
|
||||||
|
if self.is_dir {
|
||||||
|
return Err(SmbError::IsDirectory);
|
||||||
|
}
|
||||||
|
let g = self.inner.lock().unwrap();
|
||||||
|
let data = g.files.get(&self.key).ok_or(SmbError::NotFound)?;
|
||||||
|
let start = offset as usize;
|
||||||
|
if start >= data.len() {
|
||||||
|
return Ok(Bytes::new());
|
||||||
|
}
|
||||||
|
let end = (start + len as usize).min(data.len());
|
||||||
|
Ok(Bytes::copy_from_slice(&data[start..end]))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write(&self, offset: u64, data: &[u8]) -> SmbResult<u32> {
|
||||||
|
if self.is_dir {
|
||||||
|
return Err(SmbError::IsDirectory);
|
||||||
|
}
|
||||||
|
let mut g = self.inner.lock().unwrap();
|
||||||
|
let buf = g.files.get_mut(&self.key).ok_or(SmbError::NotFound)?;
|
||||||
|
let needed = (offset as usize) + data.len();
|
||||||
|
if buf.len() < needed {
|
||||||
|
buf.resize(needed, 0);
|
||||||
|
}
|
||||||
|
buf[offset as usize..offset as usize + data.len()].copy_from_slice(data);
|
||||||
|
Ok(data.len() as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn flush(&self) -> SmbResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stat(&self) -> SmbResult<FileInfo> {
|
||||||
|
let g = self.inner.lock().unwrap();
|
||||||
|
let size = if self.is_dir {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
g.files.get(&self.key).ok_or(SmbError::NotFound)?.len() as u64
|
||||||
|
};
|
||||||
|
let name = self
|
||||||
|
.key
|
||||||
|
.rsplit_once('\\')
|
||||||
|
.map(|(_, n)| n.to_string())
|
||||||
|
.unwrap_or_else(|| self.key.clone());
|
||||||
|
Ok(FileInfo {
|
||||||
|
name,
|
||||||
|
end_of_file: size,
|
||||||
|
allocation_size: size,
|
||||||
|
creation_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_access_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_write_time: 0x01D9_0000_0000_0000,
|
||||||
|
change_time: 0x01D9_0000_0000_0000,
|
||||||
|
is_directory: self.is_dir,
|
||||||
|
file_index: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_times(&self, _times: FileTimes) -> SmbResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn truncate(&self, len: u64) -> SmbResult<()> {
|
||||||
|
if self.is_dir {
|
||||||
|
return Err(SmbError::IsDirectory);
|
||||||
|
}
|
||||||
|
let mut g = self.inner.lock().unwrap();
|
||||||
|
let buf = g.files.get_mut(&self.key).ok_or(SmbError::NotFound)?;
|
||||||
|
buf.resize(len as usize, 0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_dir(&self, _pattern: Option<&str>) -> SmbResult<Vec<DirEntry>> {
|
||||||
|
if !self.is_dir {
|
||||||
|
return Err(SmbError::NotADirectory);
|
||||||
|
}
|
||||||
|
let g = self.inner.lock().unwrap();
|
||||||
|
let prefix = if self.key.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("{}\\", self.key)
|
||||||
|
};
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for (k, v) in g.files.iter() {
|
||||||
|
if let Some(rest) = k.strip_prefix(&prefix)
|
||||||
|
&& !rest.contains('\\')
|
||||||
|
{
|
||||||
|
entries.push(DirEntry {
|
||||||
|
info: FileInfo {
|
||||||
|
name: rest.to_string(),
|
||||||
|
end_of_file: v.len() as u64,
|
||||||
|
allocation_size: v.len() as u64,
|
||||||
|
creation_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_access_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_write_time: 0x01D9_0000_0000_0000,
|
||||||
|
change_time: 0x01D9_0000_0000_0000,
|
||||||
|
is_directory: false,
|
||||||
|
file_index: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k in g.dirs.keys() {
|
||||||
|
if let Some(rest) = k.strip_prefix(&prefix)
|
||||||
|
&& !rest.is_empty()
|
||||||
|
&& !rest.contains('\\')
|
||||||
|
{
|
||||||
|
entries.push(DirEntry {
|
||||||
|
info: FileInfo {
|
||||||
|
name: rest.to_string(),
|
||||||
|
end_of_file: 0,
|
||||||
|
allocation_size: 0,
|
||||||
|
creation_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_access_time: 0x01D9_0000_0000_0000,
|
||||||
|
last_write_time: 0x01D9_0000_0000_0000,
|
||||||
|
change_time: 0x01D9_0000_0000_0000,
|
||||||
|
is_directory: true,
|
||||||
|
file_index: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(self: Box<Self>) -> SmbResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
69
vendor/smb-server/src/utils.rs
vendored
Normal file
69
vendor/smb-server/src/utils.rs
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
//! Small helpers shared across modules.
|
||||||
|
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
/// Number of 100-nanosecond intervals between 1601-01-01 (Windows FILETIME
|
||||||
|
/// epoch) and 1970-01-01 (UNIX epoch). 369 years.
|
||||||
|
const FILETIME_OFFSET: u64 = 116_444_736_000_000_000;
|
||||||
|
|
||||||
|
/// Convert a `SystemTime` to a Windows FILETIME (100ns ticks since 1601).
|
||||||
|
pub fn system_time_to_filetime(t: SystemTime) -> u64 {
|
||||||
|
match t.duration_since(UNIX_EPOCH) {
|
||||||
|
Ok(d) => FILETIME_OFFSET + (d.as_secs() * 10_000_000) + (d.subsec_nanos() as u64 / 100),
|
||||||
|
// Pre-1970 — clamp to the FILETIME epoch.
|
||||||
|
Err(_) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert "now" to FILETIME.
|
||||||
|
pub fn now_filetime() -> u64 {
|
||||||
|
system_time_to_filetime(SystemTime::now())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a `&str` to little-endian UTF-16 bytes.
|
||||||
|
pub fn utf16le(s: &str) -> Vec<u8> {
|
||||||
|
let mut out = Vec::with_capacity(s.len() * 2);
|
||||||
|
for unit in s.encode_utf16() {
|
||||||
|
out.extend_from_slice(&unit.to_le_bytes());
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a UTF-16LE byte slice. Returns an empty string if the buffer is not
|
||||||
|
/// 2-byte aligned (caller decides what to do); replacement characters on
|
||||||
|
/// invalid surrogates.
|
||||||
|
pub fn utf16le_to_string(bytes: &[u8]) -> String {
|
||||||
|
if !bytes.len().is_multiple_of(2) {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let units: Vec<u16> = bytes
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||||
|
.collect();
|
||||||
|
String::from_utf16_lossy(&units)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a UTF-16LE byte slice into a `Vec<u16>`, returning `None` on a
|
||||||
|
/// non-aligned buffer.
|
||||||
|
pub fn utf16le_to_units(bytes: &[u8]) -> Option<Vec<u16>> {
|
||||||
|
if !bytes.len().is_multiple_of(2) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
bytes
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| u16::from_le_bytes([c[0], c[1]]))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill `out` with cryptographically-strong random bytes via `getrandom`.
|
||||||
|
/// Falls back to zeros if the OS RNG fails — the caller should treat this as
|
||||||
|
/// fatal, but we never panic.
|
||||||
|
pub fn fill_random(out: &mut [u8]) {
|
||||||
|
if getrandom::fill(out).is_err() {
|
||||||
|
for b in out.iter_mut() {
|
||||||
|
*b = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
vendor/smb2/.cargo/audit.toml
vendored
Normal file
8
vendor/smb2/.cargo/audit.toml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Ignored advisories for cargo-audit
|
||||||
|
|
||||||
|
[advisories]
|
||||||
|
ignore = [
|
||||||
|
# Marvin Attack timing sidechannel in `rsa` crate. No fix available.
|
||||||
|
# Only affects benchmarks/smb/ (via sspi -> rsa), not the smb2 crate itself.
|
||||||
|
"RUSTSEC-2023-0071",
|
||||||
|
]
|
||||||
12
vendor/smb2/.claude/rules/docs-maintenance.md
vendored
Normal file
12
vendor/smb2/.claude/rules/docs-maintenance.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
When modifying code in a directory that contains a `CLAUDE.md` file, check whether your changes affect the documented
|
||||||
|
architecture, key decisions, or gotchas. If they do, update the `CLAUDE.md` to stay in sync. If you notice a `CLAUDE.md`
|
||||||
|
missing in a directory where there should be one, add it. Skip this for trivial changes (bug fixes, formatting, small
|
||||||
|
refactors that don't change the architecture).
|
||||||
|
|
||||||
|
If something failed due to a wrong assumption, add a `Gotcha/Why` entry to the nearest `CLAUDE.md`.
|
||||||
|
|
||||||
|
Add `Decision/Why` entries to the nearest colocated `CLAUDE.md` for key decisions. If the decision has rich evidence
|
||||||
|
(benchmarks, detailed analysis), put the evidence in `docs/notes/` and link from the CLAUDE.md.
|
||||||
|
|
||||||
|
When writing guides, see [this diff](https://github.com/vdavid/cmdr/commit/13ad8f3#diff-795210f) for the formatting
|
||||||
|
standard. (Before: AI-written. After: matching our standards for conciseness and clarity.)
|
||||||
14
vendor/smb2/.claude/rules/git-conventions.md
vendored
Normal file
14
vendor/smb2/.claude/rules/git-conventions.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
## Commit messages
|
||||||
|
|
||||||
|
- Use conventional commit messages.
|
||||||
|
- Title: Capture the IMPACT of the change, not the tech details. From the title, we need to understand WHY we did this,
|
||||||
|
what we ACHIEVED with the commit. Length-wise, aim for about 50 chars max.
|
||||||
|
- Body: Use bullets primarily. No word wrap. Don't hard-wrap body lines at 72 chars or any other width. Let the
|
||||||
|
terminal/viewer wrap naturally. Enclose entities in ``. No co-author!
|
||||||
|
|
||||||
|
## PRs
|
||||||
|
|
||||||
|
- Use the PR title to summarize the changes in a casual/informal tone. Be information dense and concise.
|
||||||
|
- In the desc., write a thorough, organized, but concise, often bulleted list of the changes. Use no headings.
|
||||||
|
- At the bottom of the PR description, use a single "## Test plan" heading, in which, explain how the changes were
|
||||||
|
tested. Assume that the changes were also tested manually if it makes sense for the type of changes.
|
||||||
16
vendor/smb2/.codegraph/.gitignore
vendored
Normal file
16
vendor/smb2/.codegraph/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# CodeGraph data files
|
||||||
|
# These are local to each machine and should not be committed
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
cache/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Hook markers
|
||||||
|
.dirty
|
||||||
140
vendor/smb2/.codegraph/config.json
vendored
Normal file
140
vendor/smb2/.codegraph/config.json
vendored
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.jsx",
|
||||||
|
"**/*.py",
|
||||||
|
"**/*.go",
|
||||||
|
"**/*.rs",
|
||||||
|
"**/*.java",
|
||||||
|
"**/*.c",
|
||||||
|
"**/*.h",
|
||||||
|
"**/*.cpp",
|
||||||
|
"**/*.hpp",
|
||||||
|
"**/*.cc",
|
||||||
|
"**/*.cxx",
|
||||||
|
"**/*.cs",
|
||||||
|
"**/*.php",
|
||||||
|
"**/*.rb",
|
||||||
|
"**/*.swift",
|
||||||
|
"**/*.kt",
|
||||||
|
"**/*.kts",
|
||||||
|
"**/*.dart",
|
||||||
|
"**/*.svelte",
|
||||||
|
"**/*.liquid",
|
||||||
|
"**/*.pas",
|
||||||
|
"**/*.dpr",
|
||||||
|
"**/*.dpk",
|
||||||
|
"**/*.lpr",
|
||||||
|
"**/*.dfm",
|
||||||
|
"**/*.fmx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/.git/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/vendor/**",
|
||||||
|
"**/Pods/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"**/build/**",
|
||||||
|
"**/out/**",
|
||||||
|
"**/bin/**",
|
||||||
|
"**/obj/**",
|
||||||
|
"**/target/**",
|
||||||
|
"**/*.min.js",
|
||||||
|
"**/*.bundle.js",
|
||||||
|
"**/.next/**",
|
||||||
|
"**/.nuxt/**",
|
||||||
|
"**/.svelte-kit/**",
|
||||||
|
"**/.output/**",
|
||||||
|
"**/.turbo/**",
|
||||||
|
"**/.cache/**",
|
||||||
|
"**/.parcel-cache/**",
|
||||||
|
"**/.vite/**",
|
||||||
|
"**/.astro/**",
|
||||||
|
"**/.docusaurus/**",
|
||||||
|
"**/.gatsby/**",
|
||||||
|
"**/.webpack/**",
|
||||||
|
"**/.nx/**",
|
||||||
|
"**/.yarn/cache/**",
|
||||||
|
"**/.pnpm-store/**",
|
||||||
|
"**/storybook-static/**",
|
||||||
|
"**/.expo/**",
|
||||||
|
"**/web-build/**",
|
||||||
|
"**/ios/Pods/**",
|
||||||
|
"**/ios/build/**",
|
||||||
|
"**/android/build/**",
|
||||||
|
"**/android/.gradle/**",
|
||||||
|
"**/__pycache__/**",
|
||||||
|
"**/.venv/**",
|
||||||
|
"**/venv/**",
|
||||||
|
"**/site-packages/**",
|
||||||
|
"**/dist-packages/**",
|
||||||
|
"**/.pytest_cache/**",
|
||||||
|
"**/.mypy_cache/**",
|
||||||
|
"**/.ruff_cache/**",
|
||||||
|
"**/.tox/**",
|
||||||
|
"**/.nox/**",
|
||||||
|
"**/*.egg-info/**",
|
||||||
|
"**/.eggs/**",
|
||||||
|
"**/go/pkg/mod/**",
|
||||||
|
"**/target/debug/**",
|
||||||
|
"**/target/release/**",
|
||||||
|
"**/.gradle/**",
|
||||||
|
"**/.m2/**",
|
||||||
|
"**/generated-sources/**",
|
||||||
|
"**/.kotlin/**",
|
||||||
|
"**/.dart_tool/**",
|
||||||
|
"**/.vs/**",
|
||||||
|
"**/.nuget/**",
|
||||||
|
"**/artifacts/**",
|
||||||
|
"**/publish/**",
|
||||||
|
"**/cmake-build-*/**",
|
||||||
|
"**/CMakeFiles/**",
|
||||||
|
"**/bazel-*/**",
|
||||||
|
"**/vcpkg_installed/**",
|
||||||
|
"**/.conan/**",
|
||||||
|
"**/Debug/**",
|
||||||
|
"**/Release/**",
|
||||||
|
"**/x64/**",
|
||||||
|
"**/.pio/**",
|
||||||
|
"**/release/**",
|
||||||
|
"**/*.app/**",
|
||||||
|
"**/*.asar",
|
||||||
|
"**/DerivedData/**",
|
||||||
|
"**/.build/**",
|
||||||
|
"**/.swiftpm/**",
|
||||||
|
"**/xcuserdata/**",
|
||||||
|
"**/Carthage/Build/**",
|
||||||
|
"**/SourcePackages/**",
|
||||||
|
"**/__history/**",
|
||||||
|
"**/__recovery/**",
|
||||||
|
"**/*.dcu",
|
||||||
|
"**/.composer/**",
|
||||||
|
"**/storage/framework/**",
|
||||||
|
"**/bootstrap/cache/**",
|
||||||
|
"**/.bundle/**",
|
||||||
|
"**/tmp/cache/**",
|
||||||
|
"**/public/assets/**",
|
||||||
|
"**/public/packs/**",
|
||||||
|
"**/.yardoc/**",
|
||||||
|
"**/coverage/**",
|
||||||
|
"**/htmlcov/**",
|
||||||
|
"**/.nyc_output/**",
|
||||||
|
"**/test-results/**",
|
||||||
|
"**/.coverage/**",
|
||||||
|
"**/.idea/**",
|
||||||
|
"**/logs/**",
|
||||||
|
"**/tmp/**",
|
||||||
|
"**/temp/**",
|
||||||
|
"**/_build/**",
|
||||||
|
"**/docs/_build/**",
|
||||||
|
"**/site/**"
|
||||||
|
],
|
||||||
|
"languages": [],
|
||||||
|
"frameworks": [],
|
||||||
|
"maxFileSize": 1048576,
|
||||||
|
"extractDocstrings": true,
|
||||||
|
"trackCallSites": true
|
||||||
|
}
|
||||||
4
vendor/smb2/.env.example
vendored
Normal file
4
vendor/smb2/.env.example
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copy this to .env and fill in your values.
|
||||||
|
# .env is gitignored and never committed.
|
||||||
|
|
||||||
|
SMB2_TEST_NAS_PASSWORD=your_nas_password_here
|
||||||
2
vendor/smb2/.gitattributes
vendored
Normal file
2
vendor/smb2/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Force LF line endings for all text files (consistent with rustfmt.toml newline_style = "Unix")
|
||||||
|
* text=auto eol=lf
|
||||||
136
vendor/smb2/.github/workflows/ci.yml
vendored
Normal file
136
vendor/smb2/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check (${{ matrix.os }}, rust ${{ matrix.rust }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-2025]
|
||||||
|
rust: ["1.85", stable]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Cache cargo registry and target
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: cargo fmt --check
|
||||||
|
|
||||||
|
- name: Run clippy lints
|
||||||
|
run: cargo clippy --all-targets -- -D warnings
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test
|
||||||
|
|
||||||
|
- name: Build documentation
|
||||||
|
run: cargo doc --no-deps
|
||||||
|
|
||||||
|
docker-tests:
|
||||||
|
name: Docker integration tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache cargo registry and target
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Start SMB containers
|
||||||
|
run: ./tests/docker/start.sh internal
|
||||||
|
|
||||||
|
- name: Run Docker integration tests
|
||||||
|
run: cargo test --test docker_integration -- --ignored
|
||||||
|
env:
|
||||||
|
RUST_LOG: smb2=info
|
||||||
|
|
||||||
|
- name: Stop containers
|
||||||
|
if: always()
|
||||||
|
run: ./tests/docker/stop.sh
|
||||||
|
|
||||||
|
consumer-tests:
|
||||||
|
name: Consumer integration tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache cargo registry and target
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Start consumer containers
|
||||||
|
run: ./tests/docker/start.sh consumer
|
||||||
|
|
||||||
|
- name: Run consumer integration tests
|
||||||
|
run: cargo test --features testing --test consumer_integration -- --ignored
|
||||||
|
env:
|
||||||
|
RUST_LOG: smb2=info
|
||||||
|
|
||||||
|
- name: Stop containers
|
||||||
|
if: always()
|
||||||
|
run: ./tests/docker/stop.sh
|
||||||
|
|
||||||
|
msrv:
|
||||||
|
name: Verify MSRV (1.85)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Install Rust toolchain (MSRV)
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: "1.85"
|
||||||
|
|
||||||
|
- name: Cache cargo registry and target
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Check compilation on MSRV
|
||||||
|
run: cargo check
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: "-D warnings"
|
||||||
|
|
||||||
|
ci-ok:
|
||||||
|
name: CI OK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [check, docker-tests, consumer-tests, msrv]
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Check all jobs passed
|
||||||
|
run: |
|
||||||
|
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
|
||||||
|
echo "Some jobs failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
|
||||||
|
echo "Some jobs were cancelled"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "All jobs passed"
|
||||||
74
vendor/smb2/.github/workflows/fuzz.yml
vendored
Normal file
74
vendor/smb2/.github/workflows/fuzz.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
name: Fuzz
|
||||||
|
|
||||||
|
# Short-duration fuzz run: weekly schedule + manual dispatch. Each target
|
||||||
|
# runs for 5 minutes with the committed seed corpus. For longer hunts, run
|
||||||
|
# locally: `cargo +nightly fuzz run <target> -- -max_total_time=1800`.
|
||||||
|
#
|
||||||
|
# We deliberately do NOT fuzz on every push -- runs are too long for that.
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Mondays 04:15 UTC.
|
||||||
|
- cron: "15 4 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
duration_seconds:
|
||||||
|
description: "Per-target fuzz time (seconds)"
|
||||||
|
required: false
|
||||||
|
default: "300"
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fuzz:
|
||||||
|
name: Fuzz ${{ matrix.target }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
- fuzz_header_parse
|
||||||
|
- fuzz_transform_header_parse
|
||||||
|
- fuzz_compression_transform_header_parse
|
||||||
|
- fuzz_compound_split
|
||||||
|
- fuzz_frame_parse
|
||||||
|
- fuzz_sub_frame_parse
|
||||||
|
- fuzz_negotiate_request_parse
|
||||||
|
- fuzz_negotiate_response_parse
|
||||||
|
- fuzz_create_request_parse
|
||||||
|
- fuzz_create_response_parse
|
||||||
|
- fuzz_query_info_response_parse
|
||||||
|
- fuzz_dfs_referral_response_parse
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Install Rust nightly
|
||||||
|
uses: dtolnay/rust-toolchain@nightly
|
||||||
|
|
||||||
|
- name: Cache cargo registry and target
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: |
|
||||||
|
.
|
||||||
|
fuzz
|
||||||
|
|
||||||
|
- name: Install cargo-fuzz
|
||||||
|
run: cargo install cargo-fuzz
|
||||||
|
|
||||||
|
- name: Run fuzz target
|
||||||
|
env:
|
||||||
|
DURATION: ${{ github.event.inputs.duration_seconds || '300' }}
|
||||||
|
run: |
|
||||||
|
cargo +nightly fuzz run "${{ matrix.target }}" \
|
||||||
|
-- -max_total_time="${DURATION}" -print_final_stats=1
|
||||||
|
|
||||||
|
- name: Upload crash artifacts (if any)
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: fuzz-crash-${{ matrix.target }}
|
||||||
|
path: fuzz/artifacts/${{ matrix.target }}/
|
||||||
|
if-no-files-found: ignore
|
||||||
7
vendor/smb2/.gitignore
vendored
Normal file
7
vendor/smb2/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.idea/
|
||||||
|
.claude/projects/
|
||||||
|
.claude/worktrees/
|
||||||
|
related-repos/
|
||||||
|
target/
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
81
vendor/smb2/Cargo.toml
vendored
Normal file
81
vendor/smb2/Cargo.toml
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
[package]
|
||||||
|
name = "smb2"
|
||||||
|
version = "0.11.3"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.85"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
description = "Pure-Rust SMB2/3 client library with pipelined I/O"
|
||||||
|
repository = "https://github.com/vdavid/smb2"
|
||||||
|
keywords = ["smb", "smb2", "smb3", "cifs", "network"]
|
||||||
|
categories = ["network-programming", "filesystem"]
|
||||||
|
readme = "README.md"
|
||||||
|
documentation = "https://docs.rs/smb2"
|
||||||
|
exclude = [
|
||||||
|
".github/",
|
||||||
|
"AGENTS.md",
|
||||||
|
"docs/",
|
||||||
|
"justfile",
|
||||||
|
"deny.toml",
|
||||||
|
"clippy.toml",
|
||||||
|
"rustfmt.toml",
|
||||||
|
"related-repos/",
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Logging facade -- application picks the backend (env_logger, tracing, etc.)
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
# Async runtime agnostic
|
||||||
|
async-trait = "0.1"
|
||||||
|
|
||||||
|
# `FuturesUnordered` for pipelined concurrent `execute` calls.
|
||||||
|
futures-util = { version = "0.3", default-features = false, features = ["std", "async-await"] }
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
thiserror = "2"
|
||||||
|
|
||||||
|
# Enum conversion derives
|
||||||
|
num_enum = "0.7"
|
||||||
|
|
||||||
|
# Async runtime -- transport layer needs net, io-util, time, sync
|
||||||
|
tokio = { version = "1", features = ["net", "io-util", "time", "sync", "rt"] }
|
||||||
|
|
||||||
|
# Crypto -- signing, encryption, key derivation
|
||||||
|
hmac = "0.13"
|
||||||
|
sha2 = "0.11"
|
||||||
|
aes = "0.9"
|
||||||
|
aes-gcm = "=0.11.0-rc.4"
|
||||||
|
ccm = "=0.6.0-rc.3"
|
||||||
|
cmac = "=0.8.0-rc.5"
|
||||||
|
digest = "0.11"
|
||||||
|
|
||||||
|
# NTLM authentication (MS-NLMP)
|
||||||
|
md-5 = "0.11"
|
||||||
|
md4 = "0.11"
|
||||||
|
|
||||||
|
# Kerberos key derivation (AES string-to-key)
|
||||||
|
pbkdf2 = "0.13"
|
||||||
|
sha1 = "0.11"
|
||||||
|
|
||||||
|
# Cryptographically secure random
|
||||||
|
getrandom = "0.4"
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
lz4_flex = "0.13"
|
||||||
|
|
||||||
|
# Optional: `Serialize` derives on diagnostics types. Off by default.
|
||||||
|
serde = { version = "1", optional = true, features = ["derive"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
testing = [] # Enables smb2::testing module for Docker-based test servers
|
||||||
|
fuzzing = [] # Exposes parser entry points for `fuzz/` targets; not for applications
|
||||||
|
serde = ["dep:serde"] # `Serialize` impls on `Diagnostics` types and the protocol enums they embed.
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "net", "io-util"] }
|
||||||
|
proptest = "1"
|
||||||
|
env_logger = "0.11"
|
||||||
|
serde_json = "1" # JSON round-trip tests for the `serde` feature
|
||||||
120
vendor/smb2/src/auth/CLAUDE.md
vendored
Normal file
120
vendor/smb2/src/auth/CLAUDE.md
vendored
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Auth -- NTLM and Kerberos authentication
|
||||||
|
|
||||||
|
NTLMv2 and Kerberos authentication for SMB2 session setup.
|
||||||
|
|
||||||
|
## Key files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `mod.rs` | Module exports |
|
||||||
|
| `der.rs` | Shared ASN.1/DER primitives (TLV encode/decode) |
|
||||||
|
| `ntlm.rs` | `NtlmAuthenticator` -- 3-message NTLM exchange |
|
||||||
|
| `spnego.rs` | SPNEGO NegTokenInit/NegTokenResp wrapping |
|
||||||
|
| `kerberos/mod.rs` | Kerberos module root, re-exports authenticator |
|
||||||
|
| `kerberos/authenticator.rs` | `KerberosAuthenticator` -- full AS + TGS + AP-REQ flow |
|
||||||
|
| `kerberos/crypto.rs` | AES-CTS, RC4-HMAC, string-to-key, key derivation |
|
||||||
|
| `kerberos/messages.rs` | ASN.1/DER encoding/decoding for Kerberos messages |
|
||||||
|
| `kerberos/kdc.rs` | KDC transport client (UDP/TCP with fallback) |
|
||||||
|
|
||||||
|
## NTLM exchange
|
||||||
|
|
||||||
|
1. `negotiate()` -- builds NEGOTIATE_MESSAGE (Type 1) with default flags
|
||||||
|
2. Server sends CHALLENGE_MESSAGE (Type 2) with server challenge and target info
|
||||||
|
3. `authenticate(&challenge_bytes)` -- builds AUTHENTICATE_MESSAGE (Type 3) with NTLMv2 response
|
||||||
|
|
||||||
|
Only NTLMv2 is supported. NTLMv1 is insecure and not implemented.
|
||||||
|
|
||||||
|
## Kerberos exchange
|
||||||
|
|
||||||
|
`KerberosAuthenticator` performs the full Kerberos flow in three steps:
|
||||||
|
|
||||||
|
1. **AS exchange** (client -> KDC): derive user key from password, build PA-ENC-TIMESTAMP + PA-PAC-REQUEST, send AS-REQ, parse AS-REP, decrypt enc-part with user key to get TGT + AS session key.
|
||||||
|
2. **TGS exchange** (client -> KDC): build AP-REQ wrapping TGT + authenticator (encrypted with AS session key), send TGS-REQ for `cifs/hostname`, parse TGS-REP, decrypt enc-part with AS session key to get service ticket + TGS session key.
|
||||||
|
3. **AP-REQ construction**: build Authenticator with subkey, encrypt with TGS session key, build AP-REQ with service ticket, wrap in SPNEGO NegTokenInit.
|
||||||
|
|
||||||
|
The flow differs from NTLM: Kerberos contacts the KDC directly (async, network I/O), then produces a single token for SESSION_SETUP (usually 1 round-trip with the SMB server).
|
||||||
|
|
||||||
|
### Key usage numbers (RFC 4120 section 7.5.1)
|
||||||
|
|
||||||
|
- 1: PA-ENC-TIMESTAMP encryption
|
||||||
|
- 3: AS-REP EncKDCRepPart decryption
|
||||||
|
- 6: TGS-REQ PA-TGS-REQ Authenticator cksum (body checksum)
|
||||||
|
- 7: AP-REQ Authenticator encryption
|
||||||
|
- 8: TGS-REP EncKDCRepPart decryption (tries 8 first, falls back to 9)
|
||||||
|
|
||||||
|
### Encryption types supported
|
||||||
|
|
||||||
|
- AES-256-CTS-HMAC-SHA1-96 (etype 18) -- preferred
|
||||||
|
- AES-128-CTS-HMAC-SHA1-96 (etype 17)
|
||||||
|
- RC4-HMAC (etype 23) -- legacy
|
||||||
|
|
||||||
|
### Key derivation constants (RFC 3961)
|
||||||
|
|
||||||
|
Three subkeys are derived from each base key + usage number:
|
||||||
|
- **Ke** = DK(key, usage || 0xAA) -- encryption subkey, used for AES-CTS
|
||||||
|
- **Ki** = DK(key, usage || 0x55) -- integrity subkey, used for HMAC inside encrypt/decrypt
|
||||||
|
- **Kc** = DK(key, usage || 0x99) -- checksum subkey, used for standalone checksum/MIC
|
||||||
|
|
||||||
|
Ki and Kc are NOT the same key. Ki is for the HMAC that's appended to ciphertext in the encrypt() function. Kc is for standalone operations like the body checksum in the TGS-REQ Authenticator.
|
||||||
|
|
||||||
|
### Kerberos wire encryption format (AES)
|
||||||
|
|
||||||
|
1. Derive Ke (with 0xAA) and Ki (with 0x55) from base key + usage
|
||||||
|
2. Generate 16-byte random confounder
|
||||||
|
3. plaintext' = confounder || plaintext
|
||||||
|
4. ciphertext = AES-CTS(Ke, iv=0, plaintext')
|
||||||
|
5. hmac = HMAC-SHA1-96(Ki, plaintext') -- 12 bytes
|
||||||
|
6. output = ciphertext || hmac
|
||||||
|
|
||||||
|
## NTLM key derivation flow
|
||||||
|
|
||||||
|
1. `NTOWFv2`: `HMAC-MD5(MD4(password_utf16), uppercase(username) + domain)`
|
||||||
|
2. `NTProofStr`: `HMAC-MD5(NTOWFv2, server_challenge + client_blob)`
|
||||||
|
3. `SessionBaseKey`: `HMAC-MD5(NTOWFv2, NTProofStr)`
|
||||||
|
4. If KEY_EXCH flag: generate random session key, RC4-encrypt with SessionBaseKey
|
||||||
|
5. `ExportedSessionKey` feeds into SP800-108 KDF (in `crypto/kdf.rs`)
|
||||||
|
|
||||||
|
## MIC computation
|
||||||
|
|
||||||
|
Modern servers include `MsvAvTimestamp` in the challenge target info, which triggers MIC validation. When present:
|
||||||
|
1. Add `MsvAvFlags` with bit 0x2 (MIC present) to the target info
|
||||||
|
2. Build the AUTHENTICATE_MESSAGE with a zeroed 16-byte MIC field at offset 72
|
||||||
|
3. Compute `HMAC-MD5(ExportedSessionKey, negotiate_msg || challenge_msg || authenticate_msg)`
|
||||||
|
4. Patch the MIC into bytes 72..88
|
||||||
|
|
||||||
|
The authenticator retains raw bytes of NEGOTIATE and CHALLENGE messages for this computation.
|
||||||
|
|
||||||
|
## Key decisions
|
||||||
|
|
||||||
|
- **`getrandom` for random values**: Client challenge, random session key, nonces, and confounders use `getrandom` (OS CSPRNG).
|
||||||
|
- **`test_random_session_key` override**: Tests can inject a deterministic session key for reproducibility. Never used in production.
|
||||||
|
- **Subkey in AP-REQ Authenticator**: The Kerberos authenticator includes a random subkey, which becomes the SMB session key. This provides forward secrecy.
|
||||||
|
- **No full `authenticate()` unit tests**: The full flow requires a real KDC. Unit tests cover individual steps (encrypt/decrypt roundtrip, message encoding, etype parsing). Integration tests with Docker are planned.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Retain raw challenge bytes for MIC (NTLM)**: The MIC is computed over the exact wire bytes of all three messages.
|
||||||
|
- **RC4 for key exchange is inline (NTLM)**: ~15 lines of RC4 implementation.
|
||||||
|
- **MsvAvTimestamp presence changes behavior (NTLM)**: Without it, no MIC is computed. With it, MIC is mandatory.
|
||||||
|
- **NTLMv1 not supported**: No fallback.
|
||||||
|
- **Target info modification (NTLM)**: The client modifies the server's target info before including it in the client blob.
|
||||||
|
- **TGS-REP key usage ambiguity (Kerberos)**: RFC 4120 says key usage 8 for TGS-REP encrypted with session key, but some KDCs use 9. The authenticator tries 8 first, falls back to 9.
|
||||||
|
- **KDC_ERR_PREAUTH_REQUIRED handling (Kerberos)**: First AS-REQ without pre-auth gets error 25. The authenticator extracts supported etypes from the e-data (ETYPE-INFO2) and retries with pre-authentication.
|
||||||
|
- **DER primitives in `auth::der`**: Core DER encoding/decoding helpers (`der_length`, `der_tlv`, `parse_der_length`, `parse_der_tlv`) live in `auth/der.rs` and are shared by `spnego.rs` and `kerberos/messages.rs`. Type-specific helpers (INTEGER, GeneralString, etc.) stay in their respective modules.
|
||||||
|
|
||||||
|
## Kerberos key design decisions (from end-to-end testing)
|
||||||
|
|
||||||
|
- **MS Kerberos OID (`1.2.840.48018.1.2.2`)**: Windows AD requires the Microsoft Kerberos OID in SPNEGO NegTokenInit, not the standard RFC 4120 OID. Both are included in mechTypes, with MS OID first.
|
||||||
|
- **Key usage 11 for SPNEGO AP-REQ Authenticator**: Standard RFC 4120 uses key usage 7 for AP-REQ Authenticator encryption. Windows expects key usage 11 when the AP-REQ is wrapped in SPNEGO (per MS-KILE). Using 7 causes `KRB_AP_ERR_MODIFIED`.
|
||||||
|
- **Session key etype detection**: The TGS-REQ requests AES-256, AES-128, and RC4 (preference order). The KDC picks the session key type from this list — it may differ from the ticket encryption type. The authenticator detects the actual etype from the TGS-REP `EncKDCRepPart.key.keytype` and uses the matching cipher for Authenticator encryption.
|
||||||
|
- **Raw ticket pass-through**: The service ticket bytes must be sent to the SMB server exactly as received from the KDC. Re-encoding the ticket from parsed fields produces different DER and causes `KRB_AP_ERR_MODIFIED`. The `Ticket` struct carries `raw_bytes` for this.
|
||||||
|
- **GSS-API wrapping**: The AP-REQ in SPNEGO NegTokenInit must include the GSS-API OID header (`0x60 len OID ap-req`), not just the raw AP-REQ bytes.
|
||||||
|
- **Mutual authentication**: AP-REQ sets the mutual-required flag. The server returns an AP-REP (in SPNEGO NegTokenResp) containing a server sub-session key. The client decrypts the AP-REP (key usage 12) to extract this subkey, which becomes the SMB session key. This provides cryptographic proof that the server possesses the service key. The AP-REP may arrive in a `STATUS_SUCCESS` response (not always `STATUS_MORE_PROCESSING_REQUIRED`).
|
||||||
|
|
||||||
|
- **Credential cache (ccache) support**: `kerberos/ccache.rs` parses MIT Kerberos ccache files (v3 and v4). Supports loading cached TGTs (skip AS exchange, do TGS) and cached service tickets (skip both AS and TGS). Integrates via `Session::setup_kerberos_from_ccache()` and `KerberosAuthenticator::authenticate_from_ccache()`. `load_ccache()` reads from a path or `$KRB5CCNAME`.
|
||||||
|
|
||||||
|
## Known tech debt (Kerberos)
|
||||||
|
|
||||||
|
- ~~DER helpers duplicated between `spnego.rs` and `kerberos/messages.rs`~~ (resolved: shared `auth/der.rs`)
|
||||||
|
- ~~`kerberos/authenticator.rs` mixes crypto wrappers with protocol flow~~ (resolved: `kerberos_encrypt`, `kerberos_decrypt`, `etype_from_i32`, and `generate_random_key` moved to `kerberos/crypto.rs`)
|
||||||
|
- ~~`#![allow(rustdoc::broken_intra_doc_links)]` hack in `kerberos/messages.rs`~~ (resolved: ASN.1 context tags in doc comments wrapped in backticks)
|
||||||
196
vendor/smb2/src/auth/der.rs
vendored
Normal file
196
vendor/smb2/src/auth/der.rs
vendored
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
//! Shared ASN.1/DER encoding and decoding primitives.
|
||||||
|
//!
|
||||||
|
//! These low-level helpers are used by both `spnego.rs` and `kerberos/messages.rs`
|
||||||
|
//! to build and parse DER-encoded structures. Only the core TLV operations live
|
||||||
|
//! here; type-specific helpers (INTEGER, GeneralString, etc.) stay in their
|
||||||
|
//! respective modules.
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// Encode a DER length field.
|
||||||
|
///
|
||||||
|
/// - Lengths < 128 are encoded as a single byte.
|
||||||
|
/// - Lengths < 256 are encoded as `0x81` followed by one byte.
|
||||||
|
/// - Lengths < 65536 are encoded as `0x82` followed by two bytes (big-endian).
|
||||||
|
pub(crate) fn der_length(len: usize) -> Vec<u8> {
|
||||||
|
if len < 128 {
|
||||||
|
vec![len as u8]
|
||||||
|
} else if len < 256 {
|
||||||
|
vec![0x81, len as u8]
|
||||||
|
} else {
|
||||||
|
vec![0x82, (len >> 8) as u8, (len & 0xff) as u8]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrap data in a DER TLV (tag-length-value).
|
||||||
|
pub(crate) fn der_tlv(tag: u8, data: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = vec![tag];
|
||||||
|
out.extend_from_slice(&der_length(data.len()));
|
||||||
|
out.extend_from_slice(data);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a DER length field, returning `(length, bytes_consumed)`.
|
||||||
|
pub(crate) fn parse_der_length(data: &[u8]) -> Result<(usize, usize), Error> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Err(Error::invalid_data("DER: truncated length"));
|
||||||
|
}
|
||||||
|
let first = data[0];
|
||||||
|
if first < 128 {
|
||||||
|
Ok((first as usize, 1))
|
||||||
|
} else if first == 0x81 {
|
||||||
|
if data.len() < 2 {
|
||||||
|
return Err(Error::invalid_data("DER: truncated length (0x81)"));
|
||||||
|
}
|
||||||
|
Ok((data[1] as usize, 2))
|
||||||
|
} else if first == 0x82 {
|
||||||
|
if data.len() < 3 {
|
||||||
|
return Err(Error::invalid_data("DER: truncated length (0x82)"));
|
||||||
|
}
|
||||||
|
let len = ((data[1] as usize) << 8) | (data[2] as usize);
|
||||||
|
Ok((len, 3))
|
||||||
|
} else if first == 0x83 {
|
||||||
|
if data.len() < 4 {
|
||||||
|
return Err(Error::invalid_data("DER: truncated length (0x83)"));
|
||||||
|
}
|
||||||
|
let len = ((data[1] as usize) << 16) | ((data[2] as usize) << 8) | (data[3] as usize);
|
||||||
|
Ok((len, 4))
|
||||||
|
} else {
|
||||||
|
Err(Error::invalid_data(format!(
|
||||||
|
"DER: unsupported length encoding: 0x{first:02x}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a DER TLV, returning `(tag, value_slice, total_bytes_consumed)`.
|
||||||
|
pub(crate) fn parse_der_tlv(data: &[u8]) -> Result<(u8, &[u8], usize), Error> {
|
||||||
|
if data.is_empty() {
|
||||||
|
return Err(Error::invalid_data("DER: truncated TLV"));
|
||||||
|
}
|
||||||
|
let tag = data[0];
|
||||||
|
let (len, len_bytes) = parse_der_length(&data[1..])?;
|
||||||
|
let header_len = 1 + len_bytes;
|
||||||
|
let total = header_len + len;
|
||||||
|
if data.len() < total {
|
||||||
|
return Err(Error::invalid_data(format!(
|
||||||
|
"DER: TLV truncated: need {total} bytes, have {}",
|
||||||
|
data.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok((tag, &data[header_len..total], total))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// DER length encoding
|
||||||
|
// =======================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn length_single_byte() {
|
||||||
|
assert_eq!(der_length(0), vec![0x00]);
|
||||||
|
assert_eq!(der_length(1), vec![0x01]);
|
||||||
|
assert_eq!(der_length(127), vec![0x7f]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn length_two_byte() {
|
||||||
|
assert_eq!(der_length(128), vec![0x81, 0x80]);
|
||||||
|
assert_eq!(der_length(255), vec![0x81, 0xff]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn length_three_byte() {
|
||||||
|
assert_eq!(der_length(256), vec![0x82, 0x01, 0x00]);
|
||||||
|
assert_eq!(der_length(65535), vec![0x82, 0xff, 0xff]);
|
||||||
|
assert_eq!(der_length(1000), vec![0x82, 0x03, 0xe8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// DER TLV encoding
|
||||||
|
// =======================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tlv_simple() {
|
||||||
|
let result = der_tlv(0x04, &[0x01, 0x02]);
|
||||||
|
assert_eq!(result, vec![0x04, 0x02, 0x01, 0x02]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tlv_empty() {
|
||||||
|
let result = der_tlv(0x30, &[]);
|
||||||
|
assert_eq!(result, vec![0x30, 0x00]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tlv_long_content() {
|
||||||
|
let data = vec![0xaa; 200];
|
||||||
|
let result = der_tlv(0x04, &data);
|
||||||
|
assert_eq!(result[0], 0x04);
|
||||||
|
assert_eq!(result[1], 0x81);
|
||||||
|
assert_eq!(result[2], 200);
|
||||||
|
assert_eq!(result.len(), 3 + 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// DER length parsing
|
||||||
|
// =======================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_length_single_byte() {
|
||||||
|
let (len, consumed) = parse_der_length(&[0x05]).unwrap();
|
||||||
|
assert_eq!(len, 5);
|
||||||
|
assert_eq!(consumed, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_length_two_byte() {
|
||||||
|
let (len, consumed) = parse_der_length(&[0x81, 0x80]).unwrap();
|
||||||
|
assert_eq!(len, 128);
|
||||||
|
assert_eq!(consumed, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_length_three_byte() {
|
||||||
|
let (len, consumed) = parse_der_length(&[0x82, 0x01, 0x00]).unwrap();
|
||||||
|
assert_eq!(len, 256);
|
||||||
|
assert_eq!(consumed, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_length_four_byte() {
|
||||||
|
let (len, consumed) = parse_der_length(&[0x83, 0x01, 0x00, 0x00]).unwrap();
|
||||||
|
assert_eq!(len, 65536);
|
||||||
|
assert_eq!(consumed, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_length_truncated() {
|
||||||
|
assert!(parse_der_length(&[]).is_err());
|
||||||
|
assert!(parse_der_length(&[0x81]).is_err());
|
||||||
|
assert!(parse_der_length(&[0x82, 0x01]).is_err());
|
||||||
|
assert!(parse_der_length(&[0x83, 0x01, 0x00]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// DER TLV parsing
|
||||||
|
// =======================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_tlv_roundtrip() {
|
||||||
|
let original = der_tlv(0x04, &[0xde, 0xad, 0xbe, 0xef]);
|
||||||
|
let (tag, value, total) = parse_der_tlv(&original).unwrap();
|
||||||
|
assert_eq!(tag, 0x04);
|
||||||
|
assert_eq!(value, &[0xde, 0xad, 0xbe, 0xef]);
|
||||||
|
assert_eq!(total, original.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_tlv_truncated() {
|
||||||
|
assert!(parse_der_tlv(&[]).is_err());
|
||||||
|
// Tag present, length says 10 bytes but only 2 available
|
||||||
|
assert!(parse_der_tlv(&[0x04, 0x0a, 0x01, 0x02]).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
1637
vendor/smb2/src/auth/kerberos/authenticator.rs
vendored
Normal file
1637
vendor/smb2/src/auth/kerberos/authenticator.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
447
vendor/smb2/src/auth/kerberos/ccache.rs
vendored
Normal file
447
vendor/smb2/src/auth/kerberos/ccache.rs
vendored
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
//! MIT Kerberos credential cache (ccache) file parser.
|
||||||
|
//!
|
||||||
|
//! Reads ccache files (v3 and v4) to extract cached TGTs and service tickets,
|
||||||
|
//! enabling Kerberos authentication without a password when the user already
|
||||||
|
//! has a valid ticket (for example, from `kinit`).
|
||||||
|
//!
|
||||||
|
//! References:
|
||||||
|
//! - MIT Kerberos source: `lib/krb5/ccache/cc_file.c`
|
||||||
|
//! - Format: version(2) + [header(v4)] + default_principal + credentials*
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// A parsed Kerberos credential cache.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CCache {
|
||||||
|
/// File format version (3 or 4).
|
||||||
|
pub version: u16,
|
||||||
|
/// Default principal (typically the user who ran `kinit`).
|
||||||
|
pub default_principal: CcachePrincipal,
|
||||||
|
/// Cached credentials (TGTs and service tickets).
|
||||||
|
pub credentials: Vec<CcacheCredential>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A principal name in the ccache.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CcachePrincipal {
|
||||||
|
/// Name type (1 = KRB_NT_PRINCIPAL, 2 = KRB_NT_SRV_INST, etc.).
|
||||||
|
pub name_type: u32,
|
||||||
|
/// Kerberos realm.
|
||||||
|
pub realm: String,
|
||||||
|
/// Name components (for example, `["smbtest"]` or `["cifs", "server.domain.com"]`).
|
||||||
|
pub components: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single cached credential (ticket + metadata).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CcacheCredential {
|
||||||
|
/// Client principal.
|
||||||
|
pub client: CcachePrincipal,
|
||||||
|
/// Server (service) principal.
|
||||||
|
pub server: CcachePrincipal,
|
||||||
|
/// Session key encryption type.
|
||||||
|
pub key_etype: u16,
|
||||||
|
/// Session key bytes.
|
||||||
|
pub key_data: Vec<u8>,
|
||||||
|
/// Time the ticket was issued (Unix timestamp).
|
||||||
|
pub authtime: u32,
|
||||||
|
/// Time the ticket becomes valid (Unix timestamp).
|
||||||
|
pub starttime: u32,
|
||||||
|
/// Time the ticket expires (Unix timestamp).
|
||||||
|
pub endtime: u32,
|
||||||
|
/// Time the ticket's renewable lifetime expires (Unix timestamp).
|
||||||
|
pub renew_till: u32,
|
||||||
|
/// Raw ticket bytes (DER-encoded Kerberos Ticket).
|
||||||
|
pub ticket: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Parsing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Read and parse a ccache file from a filesystem path.
|
||||||
|
///
|
||||||
|
/// Reads `$KRB5CCNAME` if `path` is `None`, falling back to
|
||||||
|
/// `/tmp/krb5cc_<uid>` on Unix.
|
||||||
|
pub fn load_ccache(path: Option<&std::path::Path>) -> Result<CCache> {
|
||||||
|
let path = match path {
|
||||||
|
Some(p) => p.to_path_buf(),
|
||||||
|
None => {
|
||||||
|
if let Ok(env_path) = std::env::var("KRB5CCNAME") {
|
||||||
|
// Strip "FILE:" prefix if present.
|
||||||
|
let p = env_path.strip_prefix("FILE:").unwrap_or(&env_path);
|
||||||
|
std::path::PathBuf::from(p)
|
||||||
|
} else {
|
||||||
|
// Default: /tmp/krb5cc_<uid>
|
||||||
|
return Err(Error::invalid_data(
|
||||||
|
"ccache: no path specified and $KRB5CCNAME not set",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = std::fs::read(&path).map_err(|e| {
|
||||||
|
Error::invalid_data(format!("ccache: failed to read {}: {e}", path.display()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
parse_ccache(&data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a ccache file from raw bytes.
|
||||||
|
pub fn parse_ccache(data: &[u8]) -> Result<CCache> {
|
||||||
|
let mut pos = 0;
|
||||||
|
|
||||||
|
// Version: 2 bytes, big-endian. We support 0x0503 (v3) and 0x0504 (v4).
|
||||||
|
if data.len() < 2 {
|
||||||
|
return Err(Error::invalid_data("ccache: file too short for version"));
|
||||||
|
}
|
||||||
|
let version = read_u16(data, &mut pos)?;
|
||||||
|
if version != 0x0503 && version != 0x0504 {
|
||||||
|
return Err(Error::invalid_data(format!(
|
||||||
|
"ccache: unsupported version 0x{version:04x} (expected 0x0503 or 0x0504)"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// V4 has a header section after the version.
|
||||||
|
if version == 0x0504 {
|
||||||
|
let header_len = read_u16(data, &mut pos)? as usize;
|
||||||
|
if pos + header_len > data.len() {
|
||||||
|
return Err(Error::invalid_data(
|
||||||
|
"ccache: header extends past end of file",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// Skip header tags (we don't need them).
|
||||||
|
pos += header_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default principal.
|
||||||
|
let default_principal = read_principal(data, &mut pos)?;
|
||||||
|
|
||||||
|
// Credentials: read until EOF.
|
||||||
|
let mut credentials = Vec::new();
|
||||||
|
while pos < data.len() {
|
||||||
|
match read_credential(data, &mut pos) {
|
||||||
|
Ok(cred) => credentials.push(cred),
|
||||||
|
Err(_) => break, // Treat parse errors at the end as EOF.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"ccache: parsed v{}, principal={}@{}, {} credentials",
|
||||||
|
version & 0xFF,
|
||||||
|
default_principal.components.join("/"),
|
||||||
|
default_principal.realm,
|
||||||
|
credentials.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(CCache {
|
||||||
|
version,
|
||||||
|
default_principal,
|
||||||
|
credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lookup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl CCache {
|
||||||
|
/// Find a cached service ticket for the given SPN and realm.
|
||||||
|
///
|
||||||
|
/// Looks for a credential where the server principal matches
|
||||||
|
/// `service/hostname@realm` (case-insensitive hostname comparison).
|
||||||
|
pub fn find_service_ticket(
|
||||||
|
&self,
|
||||||
|
service: &str,
|
||||||
|
hostname: &str,
|
||||||
|
realm: &str,
|
||||||
|
) -> Option<&CcacheCredential> {
|
||||||
|
self.credentials.iter().find(|c| {
|
||||||
|
c.server.realm.eq_ignore_ascii_case(realm)
|
||||||
|
&& c.server.components.len() == 2
|
||||||
|
&& c.server.components[0].eq_ignore_ascii_case(service)
|
||||||
|
&& c.server.components[1].eq_ignore_ascii_case(hostname)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a cached TGT for the given realm.
|
||||||
|
///
|
||||||
|
/// Looks for a credential where the server principal is `krbtgt/REALM@REALM`.
|
||||||
|
pub fn find_tgt(&self, realm: &str) -> Option<&CcacheCredential> {
|
||||||
|
self.credentials.iter().find(|c| {
|
||||||
|
c.server.realm.eq_ignore_ascii_case(realm)
|
||||||
|
&& c.server.components.len() == 2
|
||||||
|
&& c.server.components[0] == "krbtgt"
|
||||||
|
&& c.server.components[1].eq_ignore_ascii_case(realm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Binary reading helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8> {
|
||||||
|
if *pos >= data.len() {
|
||||||
|
return Err(Error::invalid_data("ccache: unexpected end of data"));
|
||||||
|
}
|
||||||
|
let val = data[*pos];
|
||||||
|
*pos += 1;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u16(data: &[u8], pos: &mut usize) -> Result<u16> {
|
||||||
|
if *pos + 2 > data.len() {
|
||||||
|
return Err(Error::invalid_data("ccache: unexpected end of data"));
|
||||||
|
}
|
||||||
|
let val = u16::from_be_bytes([data[*pos], data[*pos + 1]]);
|
||||||
|
*pos += 2;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32> {
|
||||||
|
if *pos + 4 > data.len() {
|
||||||
|
return Err(Error::invalid_data("ccache: unexpected end of data"));
|
||||||
|
}
|
||||||
|
let val = u32::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
|
||||||
|
*pos += 4;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bytes(data: &[u8], pos: &mut usize, len: usize) -> Result<Vec<u8>> {
|
||||||
|
if *pos + len > data.len() {
|
||||||
|
return Err(Error::invalid_data("ccache: unexpected end of data"));
|
||||||
|
}
|
||||||
|
let val = data[*pos..*pos + len].to_vec();
|
||||||
|
*pos += len;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_string(data: &[u8], pos: &mut usize) -> Result<String> {
|
||||||
|
let len = read_u32(data, pos)? as usize;
|
||||||
|
let bytes = read_bytes(data, pos, len)?;
|
||||||
|
String::from_utf8(bytes).map_err(|_| Error::invalid_data("ccache: invalid UTF-8 in string"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_principal(data: &[u8], pos: &mut usize) -> Result<CcachePrincipal> {
|
||||||
|
let name_type = read_u32(data, pos)?;
|
||||||
|
let num_components = read_u32(data, pos)?;
|
||||||
|
let realm = read_string(data, pos)?;
|
||||||
|
let mut components = Vec::with_capacity(num_components as usize);
|
||||||
|
for _ in 0..num_components {
|
||||||
|
components.push(read_string(data, pos)?);
|
||||||
|
}
|
||||||
|
Ok(CcachePrincipal {
|
||||||
|
name_type,
|
||||||
|
realm,
|
||||||
|
components,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_keyblock(data: &[u8], pos: &mut usize) -> Result<(u16, Vec<u8>)> {
|
||||||
|
let enctype = read_u16(data, pos)?;
|
||||||
|
let key_len = read_u32(data, pos)? as usize;
|
||||||
|
let key_data = read_bytes(data, pos, key_len)?;
|
||||||
|
Ok((enctype, key_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_credential(data: &[u8], pos: &mut usize) -> Result<CcacheCredential> {
|
||||||
|
let client = read_principal(data, pos)?;
|
||||||
|
let server = read_principal(data, pos)?;
|
||||||
|
let (key_etype, key_data) = read_keyblock(data, pos)?;
|
||||||
|
let authtime = read_u32(data, pos)?;
|
||||||
|
let starttime = read_u32(data, pos)?;
|
||||||
|
let endtime = read_u32(data, pos)?;
|
||||||
|
let renew_till = read_u32(data, pos)?;
|
||||||
|
let _is_skey = read_u8(data, pos)?;
|
||||||
|
let _ticket_flags = read_u32(data, pos)?;
|
||||||
|
|
||||||
|
// Addresses (count + entries).
|
||||||
|
let addr_count = read_u32(data, pos)?;
|
||||||
|
for _ in 0..addr_count {
|
||||||
|
let _addr_type = read_u16(data, pos)?;
|
||||||
|
let addr_len = read_u32(data, pos)? as usize;
|
||||||
|
*pos += addr_len; // skip address data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth data (count + entries).
|
||||||
|
let authdata_count = read_u32(data, pos)?;
|
||||||
|
for _ in 0..authdata_count {
|
||||||
|
let _ad_type = read_u16(data, pos)?;
|
||||||
|
let ad_len = read_u32(data, pos)? as usize;
|
||||||
|
*pos += ad_len; // skip authdata
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ticket.
|
||||||
|
let ticket_len = read_u32(data, pos)? as usize;
|
||||||
|
let ticket = read_bytes(data, pos, ticket_len)?;
|
||||||
|
|
||||||
|
// Second ticket.
|
||||||
|
let second_ticket_len = read_u32(data, pos)? as usize;
|
||||||
|
let _second_ticket = read_bytes(data, pos, second_ticket_len)?;
|
||||||
|
|
||||||
|
Ok(CcacheCredential {
|
||||||
|
client,
|
||||||
|
server,
|
||||||
|
key_etype,
|
||||||
|
key_data,
|
||||||
|
authtime,
|
||||||
|
starttime,
|
||||||
|
endtime,
|
||||||
|
renew_till,
|
||||||
|
ticket,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_v4_ccache_from_fixture() {
|
||||||
|
let data = include_bytes!("../../../tests/fixtures/test.ccache");
|
||||||
|
let ccache = parse_ccache(data).expect("failed to parse v4 ccache");
|
||||||
|
|
||||||
|
assert_eq!(ccache.version, 0x0504);
|
||||||
|
assert_eq!(ccache.default_principal.realm, "TEST.LOCAL");
|
||||||
|
assert_eq!(ccache.default_principal.components, vec!["smbtest"]);
|
||||||
|
assert_eq!(ccache.credentials.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_v3_ccache_from_fixture() {
|
||||||
|
let data = include_bytes!("../../../tests/fixtures/test_v3.ccache");
|
||||||
|
let ccache = parse_ccache(data).expect("failed to parse v3 ccache");
|
||||||
|
|
||||||
|
assert_eq!(ccache.version, 0x0503);
|
||||||
|
assert_eq!(ccache.default_principal.realm, "EXAMPLE.COM");
|
||||||
|
assert_eq!(ccache.default_principal.components, vec!["user"]);
|
||||||
|
assert_eq!(ccache.credentials.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tgt_credential_has_correct_fields() {
|
||||||
|
let data = include_bytes!("../../../tests/fixtures/test.ccache");
|
||||||
|
let ccache = parse_ccache(data).unwrap();
|
||||||
|
|
||||||
|
let tgt = &ccache.credentials[0];
|
||||||
|
assert_eq!(tgt.client.realm, "TEST.LOCAL");
|
||||||
|
assert_eq!(tgt.client.components, vec!["smbtest"]);
|
||||||
|
assert_eq!(tgt.server.realm, "TEST.LOCAL");
|
||||||
|
assert_eq!(tgt.server.components, vec!["krbtgt", "TEST.LOCAL"]);
|
||||||
|
assert_eq!(tgt.key_etype, 23); // RC4-HMAC
|
||||||
|
assert_eq!(tgt.key_data.len(), 16);
|
||||||
|
assert_eq!(tgt.authtime, 1744100000);
|
||||||
|
assert_eq!(tgt.endtime, 1744200000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn service_ticket_has_correct_fields() {
|
||||||
|
let data = include_bytes!("../../../tests/fixtures/test.ccache");
|
||||||
|
let ccache = parse_ccache(data).unwrap();
|
||||||
|
|
||||||
|
let svc = &ccache.credentials[1];
|
||||||
|
assert_eq!(svc.server.components, vec!["cifs", "server.test.local"]);
|
||||||
|
assert_eq!(svc.key_etype, 23);
|
||||||
|
assert_eq!(svc.key_data, (16u8..32).collect::<Vec<_>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_tgt_by_realm() {
|
||||||
|
let data = include_bytes!("../../../tests/fixtures/test.ccache");
|
||||||
|
let ccache = parse_ccache(data).unwrap();
|
||||||
|
|
||||||
|
let tgt = ccache.find_tgt("TEST.LOCAL");
|
||||||
|
assert!(tgt.is_some());
|
||||||
|
assert_eq!(tgt.unwrap().server.components[0], "krbtgt");
|
||||||
|
|
||||||
|
assert!(ccache.find_tgt("OTHER.REALM").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_service_ticket_by_spn() {
|
||||||
|
let data = include_bytes!("../../../tests/fixtures/test.ccache");
|
||||||
|
let ccache = parse_ccache(data).unwrap();
|
||||||
|
|
||||||
|
let svc = ccache.find_service_ticket("cifs", "server.test.local", "TEST.LOCAL");
|
||||||
|
assert!(svc.is_some());
|
||||||
|
assert_eq!(svc.unwrap().key_data, (16u8..32).collect::<Vec<_>>());
|
||||||
|
|
||||||
|
// Case-insensitive hostname.
|
||||||
|
assert!(ccache
|
||||||
|
.find_service_ticket("cifs", "SERVER.TEST.LOCAL", "TEST.LOCAL")
|
||||||
|
.is_some());
|
||||||
|
|
||||||
|
// Wrong hostname.
|
||||||
|
assert!(ccache
|
||||||
|
.find_service_ticket("cifs", "other.test.local", "TEST.LOCAL")
|
||||||
|
.is_none());
|
||||||
|
|
||||||
|
// Wrong service.
|
||||||
|
assert!(ccache
|
||||||
|
.find_service_ticket("ldap", "server.test.local", "TEST.LOCAL")
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_tgt_case_insensitive() {
|
||||||
|
let data = include_bytes!("../../../tests/fixtures/test.ccache");
|
||||||
|
let ccache = parse_ccache(data).unwrap();
|
||||||
|
|
||||||
|
assert!(ccache.find_tgt("test.local").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v3_ccache_tgt_has_aes256_key() {
|
||||||
|
let data = include_bytes!("../../../tests/fixtures/test_v3.ccache");
|
||||||
|
let ccache = parse_ccache(data).unwrap();
|
||||||
|
|
||||||
|
let tgt = ccache.find_tgt("EXAMPLE.COM").unwrap();
|
||||||
|
assert_eq!(tgt.key_etype, 18); // AES-256
|
||||||
|
assert_eq!(tgt.key_data.len(), 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reject_unsupported_version() {
|
||||||
|
let data = [0x05, 0x02]; // v2
|
||||||
|
let result = parse_ccache(&data);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("unsupported version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reject_truncated_file() {
|
||||||
|
let result = parse_ccache(&[0x05]);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_credentials_list() {
|
||||||
|
// A valid ccache with just a version + principal + no credentials
|
||||||
|
let mut data = vec![0x05, 0x04, 0x00, 0x00]; // v4, no header
|
||||||
|
// Principal: type=1, components=1, realm="R", component="u"
|
||||||
|
data.extend_from_slice(&[0, 0, 0, 1]); // name_type
|
||||||
|
data.extend_from_slice(&[0, 0, 0, 1]); // num_components
|
||||||
|
data.extend_from_slice(&[0, 0, 0, 1]); // realm length
|
||||||
|
data.push(b'R');
|
||||||
|
data.extend_from_slice(&[0, 0, 0, 1]); // component length
|
||||||
|
data.push(b'u');
|
||||||
|
|
||||||
|
let ccache = parse_ccache(&data).unwrap();
|
||||||
|
assert_eq!(ccache.credentials.len(), 0);
|
||||||
|
assert_eq!(ccache.default_principal.realm, "R");
|
||||||
|
assert_eq!(ccache.default_principal.components, vec!["u"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
1329
vendor/smb2/src/auth/kerberos/crypto.rs
vendored
Normal file
1329
vendor/smb2/src/auth/kerberos/crypto.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
890
vendor/smb2/src/auth/kerberos/kdc.rs
vendored
Normal file
890
vendor/smb2/src/auth/kerberos/kdc.rs
vendored
Normal file
@@ -0,0 +1,890 @@
|
|||||||
|
//! KDC (Key Distribution Center) transport client.
|
||||||
|
//!
|
||||||
|
//! Sends AS-REQ and TGS-REQ messages to a Kerberos KDC on port 88.
|
||||||
|
//! Tries UDP first (no framing), falls back to TCP (4-byte big-endian
|
||||||
|
//! length prefix) when the response indicates KRB_ERR_RESPONSE_TOO_BIG
|
||||||
|
//! (error code 52).
|
||||||
|
//!
|
||||||
|
//! Transport details per RFC 4120 section 7.2 and MS-KILE section 2.1:
|
||||||
|
//! - UDP: raw DER bytes, no length prefix, max 65535 bytes
|
||||||
|
//! - TCP: 4-byte big-endian length prefix, then DER bytes
|
||||||
|
//! - Retry: up to 3 attempts with exponential backoff (1s, 2s, 4s)
|
||||||
|
//!
|
||||||
|
//! The functions here are transport-only: they send raw bytes and return
|
||||||
|
//! raw bytes. No ASN.1 parsing beyond detecting error code 52 in the
|
||||||
|
//! UDP-to-TCP fallback path.
|
||||||
|
|
||||||
|
use log::{debug, trace, warn};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::{TcpStream, UdpSocket};
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
/// Default Kerberos port (RFC 4120).
|
||||||
|
const KERBEROS_PORT: u16 = 88;
|
||||||
|
|
||||||
|
/// Maximum UDP receive buffer size.
|
||||||
|
const UDP_MAX_SIZE: usize = 65535;
|
||||||
|
|
||||||
|
/// KRB_ERR_RESPONSE_TOO_BIG error code (RFC 4120 section 7.2.1).
|
||||||
|
const KRB_ERR_RESPONSE_TOO_BIG: u32 = 52;
|
||||||
|
|
||||||
|
/// Maximum TCP frame size we accept (1 MB, generous for Kerberos).
|
||||||
|
const MAX_KDC_FRAME_SIZE: usize = 1024 * 1024;
|
||||||
|
|
||||||
|
/// Number of retry attempts per transport.
|
||||||
|
const MAX_RETRIES: u32 = 3;
|
||||||
|
|
||||||
|
/// Base retry delay (doubles each attempt).
|
||||||
|
const RETRY_BASE_DELAY: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
/// Configuration for connecting to a KDC.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct KdcConfig {
|
||||||
|
/// KDC address (host:port or just host, defaults to port 88).
|
||||||
|
pub address: String,
|
||||||
|
/// Connection/request timeout.
|
||||||
|
pub timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the KDC address to include a port if not specified.
|
||||||
|
fn resolve_address(address: &str) -> String {
|
||||||
|
if address.contains(':') {
|
||||||
|
address.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}:{}", address, KERBEROS_PORT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a Kerberos message to the KDC and receive the response.
|
||||||
|
///
|
||||||
|
/// Tries UDP first. If the response indicates the message was too
|
||||||
|
/// large for UDP (KRB_ERR_RESPONSE_TOO_BIG), retries with TCP.
|
||||||
|
///
|
||||||
|
/// UDP framing: raw DER bytes, no length prefix.
|
||||||
|
/// TCP framing: 4-byte big-endian length prefix, then DER bytes.
|
||||||
|
pub async fn send_to_kdc(config: &KdcConfig, message: &[u8]) -> Result<Vec<u8>> {
|
||||||
|
let addr = resolve_address(&config.address);
|
||||||
|
debug!("kdc: sending {} bytes to {}", message.len(), addr);
|
||||||
|
|
||||||
|
// Try UDP first.
|
||||||
|
match send_udp(&addr, message, config.timeout).await {
|
||||||
|
Ok(response) => {
|
||||||
|
if is_response_too_big(&response) {
|
||||||
|
debug!("kdc: got KRB_ERR_RESPONSE_TOO_BIG, retrying with TCP");
|
||||||
|
send_tcp(&addr, message, config.timeout).await
|
||||||
|
} else {
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("kdc: UDP failed ({}), falling back to TCP", e);
|
||||||
|
send_tcp(&addr, message, config.timeout).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a Kerberos message via UDP.
|
||||||
|
async fn send_udp(addr: &str, message: &[u8], timeout: Duration) -> Result<Vec<u8>> {
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:0").await.map_err(Error::Io)?;
|
||||||
|
|
||||||
|
let mut last_err = None;
|
||||||
|
|
||||||
|
for attempt in 0..MAX_RETRIES {
|
||||||
|
if attempt > 0 {
|
||||||
|
let delay = RETRY_BASE_DELAY * 2u32.pow(attempt - 1);
|
||||||
|
debug!("kdc: UDP retry {} after {:?}", attempt, delay);
|
||||||
|
tokio::time::sleep(delay).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the raw DER bytes (no framing for UDP).
|
||||||
|
match tokio::time::timeout(timeout, socket.send_to(message, addr)).await {
|
||||||
|
Ok(Ok(n)) => {
|
||||||
|
trace!("kdc: UDP sent {} bytes", n);
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
last_err = Some(Error::Io(e));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
last_err = Some(Error::Timeout);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive the response.
|
||||||
|
let mut buf = vec![0u8; UDP_MAX_SIZE];
|
||||||
|
match tokio::time::timeout(timeout, socket.recv_from(&mut buf)).await {
|
||||||
|
Ok(Ok((n, _src))) => {
|
||||||
|
trace!("kdc: UDP received {} bytes", n);
|
||||||
|
buf.truncate(n);
|
||||||
|
return Ok(buf);
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
last_err = Some(Error::Io(e));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
last_err = Some(Error::Timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(last_err.unwrap_or(Error::Timeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a Kerberos message via TCP.
|
||||||
|
async fn send_tcp(addr: &str, message: &[u8], timeout: Duration) -> Result<Vec<u8>> {
|
||||||
|
let mut last_err = None;
|
||||||
|
|
||||||
|
for attempt in 0..MAX_RETRIES {
|
||||||
|
if attempt > 0 {
|
||||||
|
let delay = RETRY_BASE_DELAY * 2u32.pow(attempt - 1);
|
||||||
|
debug!("kdc: TCP retry {} after {:?}", attempt, delay);
|
||||||
|
tokio::time::sleep(delay).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
match send_tcp_once(addr, message, timeout).await {
|
||||||
|
Ok(response) => return Ok(response),
|
||||||
|
Err(e) => {
|
||||||
|
last_err = Some(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(last_err.unwrap_or(Error::Timeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single TCP send/receive attempt.
|
||||||
|
async fn send_tcp_once(addr: &str, message: &[u8], timeout: Duration) -> Result<Vec<u8>> {
|
||||||
|
// Connect with timeout.
|
||||||
|
let mut stream = tokio::time::timeout(timeout, TcpStream::connect(addr))
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Timeout)?
|
||||||
|
.map_err(Error::Io)?;
|
||||||
|
|
||||||
|
// Disable Nagle for lower latency.
|
||||||
|
stream.set_nodelay(true).map_err(Error::Io)?;
|
||||||
|
|
||||||
|
// Send: 4-byte big-endian length prefix + DER bytes.
|
||||||
|
let len = message.len() as u32;
|
||||||
|
let len_bytes = len.to_be_bytes();
|
||||||
|
|
||||||
|
tokio::time::timeout(timeout, async {
|
||||||
|
stream.write_all(&len_bytes).await.map_err(Error::Io)?;
|
||||||
|
stream.write_all(message).await.map_err(Error::Io)?;
|
||||||
|
stream.flush().await.map_err(Error::Io)?;
|
||||||
|
trace!("kdc: TCP sent {} bytes", message.len());
|
||||||
|
Ok::<(), Error>(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Timeout)??;
|
||||||
|
|
||||||
|
// Receive: 4-byte big-endian length prefix.
|
||||||
|
let mut len_buf = [0u8; 4];
|
||||||
|
tokio::time::timeout(timeout, stream.read_exact(&mut len_buf))
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Timeout)?
|
||||||
|
.map_err(|e| {
|
||||||
|
if e.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||||
|
Error::Disconnected
|
||||||
|
} else {
|
||||||
|
Error::Io(e)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let resp_len = u32::from_be_bytes(len_buf) as usize;
|
||||||
|
if resp_len > MAX_KDC_FRAME_SIZE {
|
||||||
|
return Err(Error::invalid_data(format!(
|
||||||
|
"KDC TCP response length {} exceeds maximum {}",
|
||||||
|
resp_len, MAX_KDC_FRAME_SIZE
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the response body.
|
||||||
|
let mut buf = vec![0u8; resp_len];
|
||||||
|
tokio::time::timeout(timeout, stream.read_exact(&mut buf))
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::Timeout)?
|
||||||
|
.map_err(|e| {
|
||||||
|
if e.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||||
|
Error::Disconnected
|
||||||
|
} else {
|
||||||
|
Error::Io(e)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
trace!("kdc: TCP received {} bytes", resp_len);
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect KRB_ERR_RESPONSE_TOO_BIG (error code 52) in a KRB-ERROR response.
|
||||||
|
///
|
||||||
|
/// KRB-ERROR is APPLICATION [30] (tag 0x7e). We parse just enough DER
|
||||||
|
/// to extract the error-code field (context tag [6]) without a full
|
||||||
|
/// ASN.1 parser.
|
||||||
|
fn is_response_too_big(response: &[u8]) -> bool {
|
||||||
|
// KRB-ERROR starts with APPLICATION [30] = 0x7e.
|
||||||
|
if response.is_empty() || response[0] != 0x7e {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match extract_krb_error_code(response) {
|
||||||
|
Some(code) => code == KRB_ERR_RESPONSE_TOO_BIG,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the error-code from a KRB-ERROR message.
|
||||||
|
///
|
||||||
|
/// KRB-ERROR structure (simplified DER):
|
||||||
|
/// ```text
|
||||||
|
/// APPLICATION [30] {
|
||||||
|
/// SEQUENCE {
|
||||||
|
/// [0] pvno INTEGER,
|
||||||
|
/// [1] msg-type INTEGER,
|
||||||
|
/// [2] ctime (optional),
|
||||||
|
/// [3] cusec (optional),
|
||||||
|
/// [4] stime,
|
||||||
|
/// [5] susec,
|
||||||
|
/// [6] error-code INTEGER, <-- we want this
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn extract_krb_error_code(data: &[u8]) -> Option<u32> {
|
||||||
|
let mut pos = 0;
|
||||||
|
|
||||||
|
// Skip APPLICATION [30] tag.
|
||||||
|
if pos >= data.len() || data[pos] != 0x7e {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
pos = skip_der_length(data, pos)?;
|
||||||
|
|
||||||
|
// Skip SEQUENCE tag (0x30).
|
||||||
|
if pos >= data.len() || data[pos] != 0x30 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
pos = skip_der_length(data, pos)?;
|
||||||
|
|
||||||
|
// Now iterate through context-tagged fields until we find [6].
|
||||||
|
loop {
|
||||||
|
if pos >= data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tag = data[pos];
|
||||||
|
// Context tags are 0xa0..0xbf for constructed.
|
||||||
|
if tag & 0xe0 != 0xa0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let tag_num = tag & 0x1f;
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
let (field_len, new_pos) = read_der_length(data, pos)?;
|
||||||
|
let field_end = new_pos + field_len;
|
||||||
|
|
||||||
|
if tag_num == 6 {
|
||||||
|
// This field contains an INTEGER with the error code.
|
||||||
|
return parse_der_integer(data, new_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = field_end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skip a DER length field and return the position after it.
|
||||||
|
fn skip_der_length(data: &[u8], pos: usize) -> Option<usize> {
|
||||||
|
let (_len, new_pos) = read_der_length(data, pos)?;
|
||||||
|
Some(new_pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a DER length field, returning (length, position_after_length).
|
||||||
|
fn read_der_length(data: &[u8], pos: usize) -> Option<(usize, usize)> {
|
||||||
|
if pos >= data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = data[pos];
|
||||||
|
match first.cmp(&0x80) {
|
||||||
|
std::cmp::Ordering::Less => {
|
||||||
|
// Short form: length is the byte itself.
|
||||||
|
Some((first as usize, pos + 1))
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Equal => {
|
||||||
|
// Indefinite length, not used in DER.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Greater => {
|
||||||
|
// Long form: first byte & 0x7f = number of subsequent length bytes.
|
||||||
|
let num_bytes = (first & 0x7f) as usize;
|
||||||
|
if num_bytes > 4 || pos + 1 + num_bytes > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut length: usize = 0;
|
||||||
|
for i in 0..num_bytes {
|
||||||
|
length = (length << 8) | (data[pos + 1 + i] as usize);
|
||||||
|
}
|
||||||
|
Some((length, pos + 1 + num_bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a DER INTEGER at the given position, returning its value as u32.
|
||||||
|
fn parse_der_integer(data: &[u8], pos: usize) -> Option<u32> {
|
||||||
|
if pos >= data.len() || data[pos] != 0x02 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (len, val_pos) = read_der_length(data, pos + 1)?;
|
||||||
|
if val_pos + len > data.len() || len == 0 || len > 4 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut value: u32 = 0;
|
||||||
|
for i in 0..len {
|
||||||
|
value = (value << 8) | (data[val_pos + i] as u32);
|
||||||
|
}
|
||||||
|
Some(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discover KDC addresses for a realm via DNS SRV records.
|
||||||
|
///
|
||||||
|
/// Looks up `_kerberos._udp.{realm}` and `_kerberos._tcp.{realm}`.
|
||||||
|
/// Returns addresses sorted by priority.
|
||||||
|
///
|
||||||
|
/// For now, this is a placeholder -- initial implementation uses
|
||||||
|
/// the hardcoded address from KdcConfig. DNS SRV discovery will
|
||||||
|
/// be added in a future version.
|
||||||
|
pub async fn discover_kdc(_realm: &str) -> Vec<String> {
|
||||||
|
// Placeholder: DNS SRV lookup not yet implemented.
|
||||||
|
// Callers should use KdcConfig.address directly.
|
||||||
|
debug!("kdc: DNS SRV discovery not yet implemented, returning empty list");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
// ── DER parsing tests ──────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_der_length_short_form() {
|
||||||
|
assert_eq!(read_der_length(&[0x05], 0), Some((5, 1)));
|
||||||
|
assert_eq!(read_der_length(&[0x7f], 0), Some((127, 1)));
|
||||||
|
assert_eq!(read_der_length(&[0x00], 0), Some((0, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_der_length_long_form_one_byte() {
|
||||||
|
// 0x81, 0x80 = 128 bytes
|
||||||
|
assert_eq!(read_der_length(&[0x81, 0x80], 0), Some((128, 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_der_length_long_form_two_bytes() {
|
||||||
|
// 0x82, 0x01, 0x00 = 256 bytes
|
||||||
|
assert_eq!(read_der_length(&[0x82, 0x01, 0x00], 0), Some((256, 3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_der_length_indefinite_returns_none() {
|
||||||
|
assert_eq!(read_der_length(&[0x80], 0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_der_length_truncated_returns_none() {
|
||||||
|
// Says 2 length bytes follow but only 1 is present.
|
||||||
|
assert_eq!(read_der_length(&[0x82, 0x01], 0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_der_integer_single_byte() {
|
||||||
|
// INTEGER tag 0x02, length 1, value 52.
|
||||||
|
assert_eq!(parse_der_integer(&[0x02, 0x01, 0x34], 0), Some(52));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_der_integer_two_bytes() {
|
||||||
|
// INTEGER tag 0x02, length 2, value 0x0100 = 256.
|
||||||
|
assert_eq!(parse_der_integer(&[0x02, 0x02, 0x01, 0x00], 0), Some(256));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_der_integer_not_integer_tag() {
|
||||||
|
assert_eq!(parse_der_integer(&[0x03, 0x01, 0x34], 0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── KRB-ERROR detection tests ──────────────────────────────────
|
||||||
|
|
||||||
|
/// Build a minimal KRB-ERROR with the given error code.
|
||||||
|
///
|
||||||
|
/// This constructs a valid DER-encoded KRB-ERROR with fields:
|
||||||
|
/// [0] pvno = 5, [1] msg-type = 30, [4] stime, [5] susec = 0,
|
||||||
|
/// [6] error-code = the given code.
|
||||||
|
fn build_krb_error(error_code: u32) -> Vec<u8> {
|
||||||
|
// Helper: wrap value in context tag.
|
||||||
|
fn context_tag(tag_num: u8, contents: &[u8]) -> Vec<u8> {
|
||||||
|
let mut out = vec![0xa0 | tag_num];
|
||||||
|
push_der_length(&mut out, contents.len());
|
||||||
|
out.extend_from_slice(contents);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: encode a DER INTEGER.
|
||||||
|
fn der_integer(value: u32) -> Vec<u8> {
|
||||||
|
// Encode as minimal bytes.
|
||||||
|
let bytes = if value == 0 {
|
||||||
|
vec![0x00]
|
||||||
|
} else if value < 0x80 {
|
||||||
|
vec![value as u8]
|
||||||
|
} else if value < 0x8000 {
|
||||||
|
vec![(value >> 8) as u8, (value & 0xff) as u8]
|
||||||
|
} else if value < 0x800000 {
|
||||||
|
vec![
|
||||||
|
(value >> 16) as u8,
|
||||||
|
(value >> 8) as u8,
|
||||||
|
(value & 0xff) as u8,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
(value >> 24) as u8,
|
||||||
|
(value >> 16) as u8,
|
||||||
|
(value >> 8) as u8,
|
||||||
|
(value & 0xff) as u8,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let mut out = vec![0x02];
|
||||||
|
push_der_length(&mut out, bytes.len());
|
||||||
|
out.extend_from_slice(&bytes);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_der_length(out: &mut Vec<u8>, len: usize) {
|
||||||
|
if len < 0x80 {
|
||||||
|
out.push(len as u8);
|
||||||
|
} else if len < 0x100 {
|
||||||
|
out.push(0x81);
|
||||||
|
out.push(len as u8);
|
||||||
|
} else {
|
||||||
|
out.push(0x82);
|
||||||
|
out.push((len >> 8) as u8);
|
||||||
|
out.push((len & 0xff) as u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the SEQUENCE contents.
|
||||||
|
let pvno = context_tag(0, &der_integer(5));
|
||||||
|
let msg_type = context_tag(1, &der_integer(30));
|
||||||
|
// Skip [2] ctime and [3] cusec (optional).
|
||||||
|
// [4] stime: GeneralizedTime "20250101000000Z"
|
||||||
|
let stime_val = b"20250101000000Z";
|
||||||
|
let mut stime_der = vec![0x18]; // GeneralizedTime tag
|
||||||
|
push_der_length(&mut stime_der, stime_val.len());
|
||||||
|
stime_der.extend_from_slice(stime_val);
|
||||||
|
let stime = context_tag(4, &stime_der);
|
||||||
|
let susec = context_tag(5, &der_integer(0));
|
||||||
|
let error_code_field = context_tag(6, &der_integer(error_code));
|
||||||
|
|
||||||
|
let mut seq_contents = Vec::new();
|
||||||
|
seq_contents.extend_from_slice(&pvno);
|
||||||
|
seq_contents.extend_from_slice(&msg_type);
|
||||||
|
seq_contents.extend_from_slice(&stime);
|
||||||
|
seq_contents.extend_from_slice(&susec);
|
||||||
|
seq_contents.extend_from_slice(&error_code_field);
|
||||||
|
|
||||||
|
// Wrap in SEQUENCE.
|
||||||
|
let mut seq = vec![0x30];
|
||||||
|
push_der_length(&mut seq, seq_contents.len());
|
||||||
|
seq.extend_from_slice(&seq_contents);
|
||||||
|
|
||||||
|
// Wrap in APPLICATION [30].
|
||||||
|
let mut msg = vec![0x7e];
|
||||||
|
push_der_length(&mut msg, seq.len());
|
||||||
|
msg.extend_from_slice(&seq);
|
||||||
|
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_response_too_big_detects_error_52() {
|
||||||
|
let error = build_krb_error(KRB_ERR_RESPONSE_TOO_BIG);
|
||||||
|
assert!(is_response_too_big(&error));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_response_too_big_ignores_other_errors() {
|
||||||
|
// Error code 6 = KDC_ERR_C_PRINCIPAL_UNKNOWN
|
||||||
|
let error = build_krb_error(6);
|
||||||
|
assert!(!is_response_too_big(&error));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_response_too_big_ignores_non_error_messages() {
|
||||||
|
// AS-REP starts with APPLICATION [11] = 0x6b
|
||||||
|
assert!(!is_response_too_big(&[0x6b, 0x03, 0x30, 0x01, 0x00]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_response_too_big_handles_empty_response() {
|
||||||
|
assert!(!is_response_too_big(&[]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_response_too_big_handles_truncated_response() {
|
||||||
|
// Just the APPLICATION tag and nothing else.
|
||||||
|
assert!(!is_response_too_big(&[0x7e]));
|
||||||
|
assert!(!is_response_too_big(&[0x7e, 0x00]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_error_code_from_valid_krb_error() {
|
||||||
|
let error = build_krb_error(25);
|
||||||
|
assert_eq!(extract_krb_error_code(&error), Some(25));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_error_code_returns_none_for_non_error() {
|
||||||
|
assert_eq!(
|
||||||
|
extract_krb_error_code(&[0x6b, 0x03, 0x30, 0x01, 0x00]),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Address resolution tests ───────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_address_adds_default_port() {
|
||||||
|
assert_eq!(resolve_address("kdc.example.com"), "kdc.example.com:88");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_address_preserves_explicit_port() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_address("kdc.example.com:8888"),
|
||||||
|
"kdc.example.com:8888"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_address_ip_no_port() {
|
||||||
|
assert_eq!(resolve_address("10.0.0.1"), "10.0.0.1:88");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_address_ip_with_port() {
|
||||||
|
assert_eq!(resolve_address("10.0.0.1:88"), "10.0.0.1:88");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UDP transport tests ────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn udp_send_receive() {
|
||||||
|
// Set up a mock KDC that echoes the request back.
|
||||||
|
let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let server_addr = server.local_addr().unwrap();
|
||||||
|
|
||||||
|
let server_task = tokio::spawn(async move {
|
||||||
|
let mut buf = vec![0u8; UDP_MAX_SIZE];
|
||||||
|
let (n, src) = server.recv_from(&mut buf).await.unwrap();
|
||||||
|
// Echo back the message.
|
||||||
|
server.send_to(&buf[..n], src).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let message = b"test-kerberos-message";
|
||||||
|
let result = send_udp(&server_addr.to_string(), message, Duration::from_secs(5)).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"UDP send/receive failed: {:?}",
|
||||||
|
result.err()
|
||||||
|
);
|
||||||
|
assert_eq!(result.unwrap(), message);
|
||||||
|
|
||||||
|
server_task.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn udp_timeout_on_no_response() {
|
||||||
|
// Bind a server socket but never read from it.
|
||||||
|
let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let server_addr = server.local_addr().unwrap();
|
||||||
|
|
||||||
|
// Use very short timeout and only 1 retry attempt to keep test fast.
|
||||||
|
// We can't change MAX_RETRIES, but we use a very short timeout so
|
||||||
|
// all 3 retries finish quickly.
|
||||||
|
let result = send_udp(
|
||||||
|
&server_addr.to_string(),
|
||||||
|
b"hello",
|
||||||
|
Duration::from_millis(50),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(
|
||||||
|
matches!(result.as_ref().unwrap_err(), Error::Timeout),
|
||||||
|
"expected Timeout, got: {:?}",
|
||||||
|
result.unwrap_err()
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TCP transport tests ────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tcp_send_receive() {
|
||||||
|
// Set up a mock KDC that reads a length-prefixed message and
|
||||||
|
// sends back a length-prefixed response.
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
|
||||||
|
let server_task = tokio::spawn(async move {
|
||||||
|
let (mut stream, _) = listener.accept().await.unwrap();
|
||||||
|
|
||||||
|
// Read 4-byte length prefix.
|
||||||
|
let mut len_buf = [0u8; 4];
|
||||||
|
stream.read_exact(&mut len_buf).await.unwrap();
|
||||||
|
let msg_len = u32::from_be_bytes(len_buf) as usize;
|
||||||
|
|
||||||
|
// Read the message body.
|
||||||
|
let mut msg = vec![0u8; msg_len];
|
||||||
|
stream.read_exact(&mut msg).await.unwrap();
|
||||||
|
|
||||||
|
// Echo back with length prefix.
|
||||||
|
let response = b"kdc-response";
|
||||||
|
let resp_len = (response.len() as u32).to_be_bytes();
|
||||||
|
stream.write_all(&resp_len).await.unwrap();
|
||||||
|
stream.write_all(response).await.unwrap();
|
||||||
|
stream.flush().await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = send_tcp(&addr.to_string(), b"test-request", Duration::from_secs(5)).await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"TCP send/receive failed: {:?}",
|
||||||
|
result.err()
|
||||||
|
);
|
||||||
|
assert_eq!(result.unwrap(), b"kdc-response");
|
||||||
|
|
||||||
|
server_task.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tcp_timeout_on_no_response() {
|
||||||
|
// Set up a server that accepts but never responds.
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
|
||||||
|
let server_task = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.unwrap();
|
||||||
|
// Hold the connection open but never respond.
|
||||||
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||||
|
drop(stream);
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = send_tcp_once(&addr.to_string(), b"hello", Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
matches!(err, Error::Timeout),
|
||||||
|
"expected Timeout, got: {err}"
|
||||||
|
);
|
||||||
|
|
||||||
|
server_task.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tcp_truncated_response() {
|
||||||
|
// Server sends a length prefix saying 100 bytes, then disconnects.
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
|
||||||
|
let server_task = tokio::spawn(async move {
|
||||||
|
let (mut stream, _) = listener.accept().await.unwrap();
|
||||||
|
|
||||||
|
// Read the request (don't care about contents).
|
||||||
|
let mut len_buf = [0u8; 4];
|
||||||
|
let _ = stream.read_exact(&mut len_buf).await;
|
||||||
|
let msg_len = u32::from_be_bytes(len_buf) as usize;
|
||||||
|
let mut discard = vec![0u8; msg_len];
|
||||||
|
let _ = stream.read_exact(&mut discard).await;
|
||||||
|
|
||||||
|
// Send response with length 100 but only 5 bytes of data, then close.
|
||||||
|
let resp_len = 100u32.to_be_bytes();
|
||||||
|
stream.write_all(&resp_len).await.unwrap();
|
||||||
|
stream
|
||||||
|
.write_all(&[0x01, 0x02, 0x03, 0x04, 0x05])
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
stream.shutdown().await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = send_tcp_once(&addr.to_string(), b"hello", Duration::from_secs(5)).await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
assert!(
|
||||||
|
matches!(err, Error::Disconnected),
|
||||||
|
"expected Disconnected for truncated response, got: {err}"
|
||||||
|
);
|
||||||
|
|
||||||
|
server_task.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tcp_oversized_length_rejected() {
|
||||||
|
// Server sends a length prefix larger than MAX_KDC_FRAME_SIZE.
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap();
|
||||||
|
|
||||||
|
let server_task = tokio::spawn(async move {
|
||||||
|
let (mut stream, _) = listener.accept().await.unwrap();
|
||||||
|
|
||||||
|
// Read request.
|
||||||
|
let mut len_buf = [0u8; 4];
|
||||||
|
let _ = stream.read_exact(&mut len_buf).await;
|
||||||
|
let msg_len = u32::from_be_bytes(len_buf) as usize;
|
||||||
|
let mut discard = vec![0u8; msg_len];
|
||||||
|
let _ = stream.read_exact(&mut discard).await;
|
||||||
|
|
||||||
|
// Send absurdly large length.
|
||||||
|
let resp_len = (MAX_KDC_FRAME_SIZE as u32 + 1).to_be_bytes();
|
||||||
|
stream.write_all(&resp_len).await.unwrap();
|
||||||
|
stream.flush().await.unwrap();
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = send_tcp_once(&addr.to_string(), b"hello", Duration::from_secs(5)).await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_str = result.unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
err_str.contains("exceeds maximum"),
|
||||||
|
"expected 'exceeds maximum' error, got: {err_str}"
|
||||||
|
);
|
||||||
|
|
||||||
|
server_task.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── send_to_kdc tests ──────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn send_to_kdc_udp_success() {
|
||||||
|
// Set up a UDP mock KDC.
|
||||||
|
let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let server_addr = server.local_addr().unwrap();
|
||||||
|
|
||||||
|
let server_task = tokio::spawn(async move {
|
||||||
|
let mut buf = vec![0u8; UDP_MAX_SIZE];
|
||||||
|
let (n, src) = server.recv_from(&mut buf).await.unwrap();
|
||||||
|
// Respond with a fake AS-REP (not a KRB-ERROR).
|
||||||
|
let response = b"\x6b\x05\x30\x03\x02\x01\x05"; // Fake AS-REP-like
|
||||||
|
server.send_to(response, src).await.unwrap();
|
||||||
|
drop(buf[..n].to_vec()); // acknowledge we received
|
||||||
|
});
|
||||||
|
|
||||||
|
let config = KdcConfig {
|
||||||
|
address: server_addr.to_string(),
|
||||||
|
timeout: Duration::from_secs(5),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = send_to_kdc(&config, b"as-req").await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(result.unwrap(), b"\x6b\x05\x30\x03\x02\x01\x05");
|
||||||
|
|
||||||
|
server_task.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn send_to_kdc_udp_too_big_falls_back_to_tcp() {
|
||||||
|
// Set up a UDP server that returns KRB_ERR_RESPONSE_TOO_BIG
|
||||||
|
// and a TCP server that returns a real response. The fallback
|
||||||
|
// path uses one `KdcConfig.address`, so both servers must share
|
||||||
|
// a port.
|
||||||
|
//
|
||||||
|
// Bind TCP first (more restrictive) and then UDP to its port.
|
||||||
|
// On Windows Server, the OS port allocator can hand out an
|
||||||
|
// ephemeral port that's in an excluded range for the other
|
||||||
|
// protocol (WSAEACCES / 10013). Retry a few times if so;
|
||||||
|
// a fresh `:0` lottery picks a different port each attempt.
|
||||||
|
let (udp_server, tcp_listener) = {
|
||||||
|
let mut last_err: Option<std::io::Error> = None;
|
||||||
|
let mut bound = None;
|
||||||
|
for _ in 0..10 {
|
||||||
|
let tcp = match TcpListener::bind("127.0.0.1:0").await {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
last_err = Some(e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let port = tcp.local_addr().unwrap().port();
|
||||||
|
match UdpSocket::bind(format!("127.0.0.1:{port}")).await {
|
||||||
|
Ok(udp) => {
|
||||||
|
bound = Some((udp, tcp));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
last_err = Some(e);
|
||||||
|
// TCP listener drops here; try a new port.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bound.unwrap_or_else(|| {
|
||||||
|
panic!("could not co-bind UDP+TCP on a shared loopback port in 10 attempts: {last_err:?}")
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let udp_addr = udp_server.local_addr().unwrap();
|
||||||
|
|
||||||
|
let udp_task = tokio::spawn(async move {
|
||||||
|
let mut buf = vec![0u8; UDP_MAX_SIZE];
|
||||||
|
let (_, src) = udp_server.recv_from(&mut buf).await.unwrap();
|
||||||
|
let error = build_krb_error(KRB_ERR_RESPONSE_TOO_BIG);
|
||||||
|
udp_server.send_to(&error, src).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let tcp_task = tokio::spawn(async move {
|
||||||
|
let (mut stream, _) = tcp_listener.accept().await.unwrap();
|
||||||
|
// Read request.
|
||||||
|
let mut len_buf = [0u8; 4];
|
||||||
|
stream.read_exact(&mut len_buf).await.unwrap();
|
||||||
|
let msg_len = u32::from_be_bytes(len_buf) as usize;
|
||||||
|
let mut msg = vec![0u8; msg_len];
|
||||||
|
stream.read_exact(&mut msg).await.unwrap();
|
||||||
|
|
||||||
|
// Send TCP response.
|
||||||
|
let response = b"tcp-kdc-response";
|
||||||
|
let resp_len = (response.len() as u32).to_be_bytes();
|
||||||
|
stream.write_all(&resp_len).await.unwrap();
|
||||||
|
stream.write_all(response).await.unwrap();
|
||||||
|
stream.flush().await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let config = KdcConfig {
|
||||||
|
address: udp_addr.to_string(),
|
||||||
|
timeout: Duration::from_secs(5),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = send_to_kdc(&config, b"as-req-large").await;
|
||||||
|
assert!(result.is_ok(), "send_to_kdc failed: {:?}", result.err());
|
||||||
|
assert_eq!(result.unwrap(), b"tcp-kdc-response");
|
||||||
|
|
||||||
|
udp_task.await.unwrap();
|
||||||
|
tcp_task.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── discover_kdc tests ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn discover_kdc_returns_empty_placeholder() {
|
||||||
|
let result = discover_kdc("EXAMPLE.COM").await;
|
||||||
|
assert!(result.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
1631
vendor/smb2/src/auth/kerberos/messages.rs
vendored
Normal file
1631
vendor/smb2/src/auth/kerberos/messages.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user