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