Files
markbase/vendor/smb-server/src/builder.rs

279 lines
8.9 KiB
Rust

//! 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>,
pub(crate) time_machine: bool,
pub(crate) time_machine_max_size: Option<u64>,
}
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(),
time_machine: false,
time_machine_max_size: None,
}
}
/// 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. Username is normalized to lowercase for SMB case-insensitive
/// matching.
pub fn user(mut self, name: impl Into<String>, access: Access) -> Self {
self.users.insert(name.into().to_lowercase(), access);
self
}
/// Enable Time Machine support for this share.
/// macOS clients will be able to use this share for backups.
pub fn time_machine(mut self) -> Self {
self.time_machine = true;
self
}
/// Set maximum backup size in GB for Time Machine.
pub fn time_machine_max_size(mut self, max_size_gb: u64) -> Self {
self.time_machine = true;
self.time_machine_max_size = Some(max_size_gb * 1024 * 1024 * 1024);
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().to_lowercase();
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, s.time_machine, s.time_machine_max_size,
));
}
// 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))
}
}