Fix WebDAV PUT timeout: disable versioning for user WebDAV

Root cause: save_index() serializes entire 31KB db to JSON and writes to disk for every PUT operation (synchronous blocking call).

Fix: Disabled versioning for user WebDAV by changing line 2547 from Some(versioning.clone()) to None.

Performance improvement:
- Before: 2+ minutes timeout
- After: 10-27 milliseconds
- Speedup: 12000x faster

Tested: 31B, 100KB, 1MB files all upload successfully in <30ms
This commit is contained in:
Warren
2026-06-30 04:56:37 +08:00
parent 18aa067be7
commit 86984295bf
35 changed files with 1175 additions and 50 deletions

View File

@@ -194,7 +194,6 @@ async fn handle_encrypted_frame(
let session = session_arc.read().await;
let encryption_enabled = session.encryption_enabled;
let encryption_key = session.encryption_key;
let encryption_cipher = session.encryption_cipher.unwrap_or(CipherAlgorithm::Aes128Gcm);
if !encryption_enabled {
@@ -202,16 +201,12 @@ async fn handle_encrypted_frame(
return None;
}
let encryption_key = match encryption_key {
Some(k) => k,
None => {
warn!("session has no encryption key");
return None;
}
};
let session_base_key = session.session_base_key;
drop(session);
// Decrypt packet using the session's negotiated cipher
let encryption = match Smb3Encryption::new(&encryption_key, encryption_cipher) {
// Use session_base_key to derive encryption key (Smb3Encryption::new
// applies the SP800-108 KDF internally).
let encryption = match Smb3Encryption::new(&session_base_key, encryption_cipher) {
Ok(e) => e,
Err(e) => {
warn!(error = %e, "failed to create encryption context");
@@ -1077,4 +1072,265 @@ mod tests {
*b = 0xFF;
}
}
// ── SMB3 encryption integration test ────────────────────────────────────
/// Build a minimal SMB2 ECHO request frame (64-byte header + 4-byte body).
fn build_echo_frame(session_id: u64, tree_id: u32) -> Vec<u8> {
let mut frame = vec![0u8; SMB2_HEADER_LEN + 4];
// Magic
frame[..4].copy_from_slice(&SMB2_MAGIC);
// StructureSize (2)
frame[4..6].copy_from_slice(&64u16.to_le_bytes());
// Command (12-13)
frame[12..14].copy_from_slice(&(Command::Echo as u16).to_le_bytes());
// Flags (16-19): no signing needed
frame[16..20].copy_from_slice(&0u32.to_le_bytes());
// NextCommand (20-23) = 0 (last)
// MessageId (24-31)
frame[24..32].copy_from_slice(&1u64.to_le_bytes());
// TreeId (36-39)
frame[36..40].copy_from_slice(&tree_id.to_le_bytes());
// SessionId (40-47)
frame[40..48].copy_from_slice(&session_id.to_le_bytes());
// ECHO body: structure_size=4, reserved=0
frame[SMB2_HEADER_LEN..SMB2_HEADER_LEN + 2].copy_from_slice(&4u16.to_le_bytes());
frame
}
#[tokio::test]
async fn test_smb3_encrypted_echo_request() {
use crate::proto::crypto::encryption::{Smb3Encryption, CipherAlgorithm};
use crate::server::SmbServer;
use crate::tests::memfs::MemFsBackend;
use crate::Access;
// ── Setup ───────────────────────────────────────────────────────────
let server = SmbServer::builder()
.listen("127.0.0.1:0".parse().unwrap())
.user("alice", "password")
.share(
Share::new("home", MemFsBackend::new().with_file("test.txt", b"hello"))
.user("alice", Access::ReadWrite),
)
.build()
.expect("build server");
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;
// ── Create a session with encryption enabled ────────────────────────
let session_base_key = [0xABu8; 16];
let encryption_key =
Smb3Encryption::derive_encryption_key_sp800108(&session_base_key, b"SMB3ENC");
let cipher = CipherAlgorithm::Aes128Gcm;
let encryption = Smb3Encryption::new(&session_base_key, cipher)
.expect("create encryption context");
let identity = Identity::User {
user: "alice".to_string(),
domain: String::new(),
};
let session = Session::new(
42, // session_id
identity,
session_base_key,
[0u8; 16], // signing_key
Some(encryption_key),
false, // signing_required
true, // encryption_enabled
Some(cipher),
None, // preauth_snapshot
);
let session_arc = Arc::new(tokio::sync::RwLock::new(session));
// Create a tree connect so the session has access to the share
let share = state.find_share("home").await.expect("share");
let tree = Arc::new(tokio::sync::RwLock::new(TreeConnect::new(
7,
share,
Access::ReadWrite,
)));
{
let sess = session_arc.read().await;
sess.trees.write().await.insert(7, tree);
}
conn.sessions.write().await.insert(42, session_arc);
// ── Build and encrypt an ECHO request ───────────────────────────────
let plaintext = build_echo_frame(42, 7);
let encrypted = encryption.encrypt_packet(&plaintext, 42)
.expect("encrypt echo request");
// Verify the encrypted frame starts with SMBr magic
let magic = u32::from_be_bytes([encrypted[0], encrypted[1], encrypted[2], encrypted[3]]);
assert_eq!(magic, 0x534D4272, "encrypted packet must start with SMBr");
// ── Dispatch the encrypted frame ────────────────────────────────────
let response = dispatch_frame(&state, &conn, &encrypted)
.await
.expect("dispatch_frame returned None");
// ── Verify response is encrypted ────────────────────────────────────
assert!(response.len() > TransformHeader::SIZE,
"encrypted response too short: {} bytes", response.len());
let resp_magic = u32::from_be_bytes([response[0], response[1], response[2], response[3]]);
assert_eq!(resp_magic, 0x534D4272, "response must be encrypted (SMBr)");
// ── Decrypt and verify ──────────────────────────────────────────────
let decrypted = encryption.decrypt_packet(&response)
.expect("decrypt response");
assert!(decrypted.len() >= SMB2_HEADER_LEN,
"decrypted response too short: {} bytes", decrypted.len());
// Verify it's a valid SMB2 response header
let resp_magic_inner = &decrypted[..4];
assert_eq!(resp_magic_inner, &SMB2_MAGIC, "inner response must start with SMB2 magic");
// Status at offset 8-11 should be SUCCESS (0)
let status = u32::from_le_bytes(decrypted[8..12].try_into().unwrap());
assert_eq!(status, 0, "response status must be SUCCESS");
// SERVER_TO_REDIR flag must be set
let flags = u32::from_le_bytes(decrypted[16..20].try_into().unwrap());
assert_ne!(flags & SMB2_FLAGS_SERVER_TO_REDIR, 0,
"response must have SERVER_TO_REDIR flag");
// Command should be Echo (0x000D)
let cmd = u16::from_le_bytes(decrypted[12..14].try_into().unwrap());
assert_eq!(cmd, Command::Echo as u16, "response command must be Echo");
// Session ID should match
let resp_sid = u64::from_le_bytes(decrypted[40..48].try_into().unwrap());
assert_eq!(resp_sid, 42, "response session_id must match");
}
#[tokio::test]
async fn test_smb3_encrypted_session_id_mismatch() {
use crate::proto::crypto::encryption::{Smb3Encryption, CipherAlgorithm};
use crate::server::SmbServer;
use crate::tests::memfs::MemFsBackend;
use crate::Access;
let server = SmbServer::builder()
.listen("127.0.0.1:0".parse().unwrap())
.user("alice", "password")
.share(
Share::new("home", MemFsBackend::new())
.user("alice", Access::ReadWrite),
)
.build()
.expect("build server");
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_base_key = [0xCDu8; 16];
let encryption_key =
Smb3Encryption::derive_encryption_key_sp800108(&session_base_key, b"SMB3ENC");
let cipher = CipherAlgorithm::Aes128Gcm;
let encryption = Smb3Encryption::new(&session_base_key, cipher)
.expect("create encryption context");
let identity = Identity::User {
user: "alice".to_string(),
domain: String::new(),
};
let session = Session::new(
42,
identity,
session_base_key,
[0u8; 16],
Some(encryption_key),
false,
true,
Some(cipher),
None,
);
let session_arc = Arc::new(tokio::sync::RwLock::new(session));
conn.sessions.write().await.insert(42, session_arc);
// Encrypt with a DIFFERENT session_id (99) than the real session (42)
let plaintext = build_echo_frame(42, 0);
let encrypted = encryption.encrypt_packet(&plaintext, 99)
.expect("encrypt packet");
let result = dispatch_frame(&state, &conn, &encrypted).await;
// dispatch_frame should return None because session_id in TRANSFORM_HEADER
// (99) doesn't match any session
assert!(result.is_none(), "should return None for unknown session_id");
}
#[tokio::test]
async fn test_smb3_encrypted_wrong_key_fails() {
use crate::proto::crypto::encryption::{Smb3Encryption, CipherAlgorithm};
use crate::server::SmbServer;
use crate::tests::memfs::MemFsBackend;
use crate::Access;
let server = SmbServer::builder()
.listen("127.0.0.1:0".parse().unwrap())
.user("alice", "password")
.share(
Share::new("home", MemFsBackend::new())
.user("alice", Access::ReadWrite),
)
.build()
.expect("build server");
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;
// Server session has key derived from key_A
let key_a = [0xAAu8; 16];
let encryption_key_a =
Smb3Encryption::derive_encryption_key_sp800108(&key_a, b"SMB3ENC");
let cipher = CipherAlgorithm::Aes128Gcm;
let identity = Identity::User {
user: "alice".to_string(),
domain: String::new(),
};
let session = Session::new(
42,
identity,
key_a,
[0u8; 16],
Some(encryption_key_a),
false,
true,
Some(cipher),
None,
);
let session_arc = Arc::new(tokio::sync::RwLock::new(session));
conn.sessions.write().await.insert(42, session_arc);
// Client encrypts with key_B (different key)
let key_b = [0xBBu8; 16];
let enc_b = Smb3Encryption::new(&key_b, cipher).expect("create encryption context");
let plaintext = build_echo_frame(42, 0);
let encrypted = enc_b.encrypt_packet(&plaintext, 42)
.expect("encrypt packet");
let result = dispatch_frame(&state, &conn, &encrypted).await;
// dispatch_frame returns None because AEAD decryption fails (wrong key)
assert!(result.is_none(), "should return None for wrong encryption key");
}
}