189 lines
5.9 KiB
Rust
189 lines
5.9 KiB
Rust
//! 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;
|
||
|
||
const SMB2_SHAREFLAG_MANUAL_CACHING: u32 = 0x00000000;
|
||
const SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM: u32 = 0x00080000;
|
||
|
||
const SMB2_SHARE_CAP_DFS: u32 = 0x00000001;
|
||
|
||
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,
|
||
};
|
||
|
||
// Time Machine: set xattr on share root directory
|
||
if share.time_machine {
|
||
use crate::path::SmbPath;
|
||
let root_path = SmbPath::root();
|
||
|
||
// Generate UUID for this Time Machine backup
|
||
let uuid = uuid::Uuid::new_v4();
|
||
let uuid_bytes = uuid.as_bytes();
|
||
|
||
// Set com.apple.TimeMachine.SupportedFilesStoreUUID
|
||
share.backend.set_xattr(&root_path, "com.apple.TimeMachine.SupportedFilesStoreUUID", uuid_bytes).await.ok();
|
||
|
||
// Set com.apple.TimeMachine.SupportsThisDevice (1 = true)
|
||
share.backend.set_xattr(&root_path, "com.apple.TimeMachine.SupportsThisDevice", &[1]).await.ok();
|
||
|
||
// Set max_size if specified
|
||
if let Some(max_size) = share.time_machine_max_size {
|
||
let max_size_bytes = max_size.to_le_bytes();
|
||
share.backend.set_xattr(&root_path, "com.apple.TimeMachine.MaxSize", &max_size_bytes).await.ok();
|
||
}
|
||
|
||
tracing::info!(share = %share.name, uuid = %uuid, "Time Machine enabled");
|
||
}
|
||
|
||
let share_flags = if share.is_ipc {
|
||
0
|
||
} else {
|
||
SMB2_SHAREFLAG_MANUAL_CACHING | SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM
|
||
};
|
||
|
||
let capabilities = if share.is_ipc {
|
||
0
|
||
} else {
|
||
0 // 普通磁盘 share,不是 DFS share
|
||
};
|
||
|
||
let capabilities = if share.is_ipc {
|
||
0
|
||
} else {
|
||
SMB2_SHARE_CAP_DFS // Basic DFS support for Finder
|
||
};
|
||
|
||
let resp = TreeConnectResponse {
|
||
structure_size: 16,
|
||
share_type: if share.is_ipc {
|
||
SHARE_TYPE_PIPE
|
||
} else {
|
||
SHARE_TYPE_DISK
|
||
},
|
||
reserved: 0,
|
||
share_flags,
|
||
capabilities,
|
||
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(),
|
||
},
|
||
}
|
||
}
|