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:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user