279 lines
8.9 KiB
Rust
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))
|
|
}
|
|
}
|