1use rand::random;
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10use smolder_proto::rpc::SyntaxId;
11use smolder_proto::smb::compression::{CompressionAlgorithm, CompressionCapabilityFlags};
12use smolder_proto::smb::smb2::{
13 CloseRequest, CompressionCapabilities, CreateDisposition, CreateOptions, CreateRequest,
14 Dialect, DirectoryInformationEntry, DispositionInformation, EchoResponse, FileAttributes,
15 FileBasicInformation, FileId, FileInfoClass, FileStandardInformation, FlushRequest,
16 GlobalCapabilities, QueryDirectoryFlags, QueryDirectoryRequest, QueryInfoRequest, ReadRequest,
17 RenameInformation, SessionId, SetInfoRequest, ShareAccess, SigningMode, TreeConnectRequest,
18 TreeId, WriteRequest,
19};
20
21use crate::auth::NtlmCredentials;
22#[cfg(feature = "kerberos-api")]
23use crate::auth::{KerberosCredentials, KerberosTarget};
24use crate::client::{
25 Authenticated, Connection, DurableHandle, DurableOpenOptions, ResilientHandle, TreeConnected,
26};
27use crate::error::CoreError;
28use crate::lsarpc::LsarpcClient;
29use crate::pipe::{connect_session, NamedPipe, PipeAccess, SmbSessionConfig};
30#[cfg(feature = "quic")]
31use crate::pipe::{connect_session_quic, connect_tree_quic};
32use crate::rpc::PipeRpcClient;
33use crate::samr::SamrClient;
34use crate::srvsvc::SrvsvcClient;
35#[cfg(feature = "quic")]
36use crate::transport::QuicTransport;
37use crate::transport::{SmbTransport, TokioTcpTransport, TransportProtocol, TransportTarget};
38const MAX_IO_CHUNK_SIZE: usize = u16::MAX as usize;
39const FILE_READ_DATA: u32 = 0x0000_0001;
40const FILE_WRITE_DATA: u32 = 0x0000_0002;
41const FILE_APPEND_DATA: u32 = 0x0000_0004;
42const FILE_READ_EA: u32 = 0x0000_0008;
43const FILE_WRITE_EA: u32 = 0x0000_0010;
44const FILE_READ_ATTRIBUTES: u32 = 0x0000_0080;
45const FILE_WRITE_ATTRIBUTES: u32 = 0x0000_0100;
46const FILE_LIST_DIRECTORY: u32 = 0x0000_0001;
47const DELETE: u32 = 0x0001_0000;
48const READ_CONTROL: u32 = 0x0002_0000;
49const SYNCHRONIZE: u32 = 0x0010_0000;
50const WINDOWS_TICK: u64 = 10_000_000;
51const SEC_TO_UNIX_EPOCH: u64 = 11_644_473_600;
52const DIRECTORY_QUERY_BUFFER_SIZE: u32 = 64 * 1024;
53
54#[derive(Debug, Clone)]
55enum BuilderAuth {
56 Ntlm(NtlmCredentials),
57 #[cfg(feature = "kerberos-api")]
58 Kerberos {
59 credentials: KerberosCredentials,
60 target: KerberosTarget,
61 },
62}
63
64#[derive(Debug, Clone)]
66pub struct ClientBuilder {
67 target: TransportTarget,
68 auth: Option<BuilderAuth>,
69 signing_mode: SigningMode,
70 capabilities: GlobalCapabilities,
71 dialects: Vec<Dialect>,
72 client_guid: [u8; 16],
73 compression: Option<CompressionCapabilities>,
74}
75
76impl ClientBuilder {
77 #[must_use]
79 pub fn new(server: impl Into<String>) -> Self {
80 Self {
81 target: TransportTarget::tcp(server),
82 auth: None,
83 signing_mode: SigningMode::ENABLED,
84 capabilities: GlobalCapabilities::LARGE_MTU
85 | GlobalCapabilities::LEASING
86 | GlobalCapabilities::ENCRYPTION,
87 dialects: vec![Dialect::Smb210, Dialect::Smb302, Dialect::Smb311],
88 client_guid: random(),
89 compression: None,
90 }
91 }
92
93 #[must_use]
95 pub fn with_port(mut self, port: u16) -> Self {
96 self.target = self.target.with_port(port);
97 self
98 }
99
100 #[must_use]
102 pub fn with_transport_target(mut self, target: TransportTarget) -> Self {
103 self.target = target;
104 self
105 }
106
107 #[must_use]
109 pub fn with_connect_host(mut self, connect_host: impl Into<String>) -> Self {
110 self.target = self.target.with_connect_host(connect_host);
111 self
112 }
113
114 #[must_use]
116 pub fn with_tls_server_name(mut self, tls_server_name: impl Into<String>) -> Self {
117 self.target = self.target.with_tls_server_name(tls_server_name);
118 self
119 }
120
121 #[must_use]
123 pub fn with_signing_mode(mut self, signing_mode: SigningMode) -> Self {
124 self.signing_mode = signing_mode;
125 self
126 }
127
128 #[must_use]
130 pub fn with_capabilities(mut self, capabilities: GlobalCapabilities) -> Self {
131 self.capabilities = capabilities;
132 self
133 }
134
135 #[must_use]
137 pub fn with_dialects(mut self, dialects: Vec<Dialect>) -> Self {
138 self.dialects = dialects;
139 self
140 }
141
142 #[must_use]
144 pub fn with_client_guid(mut self, client_guid: [u8; 16]) -> Self {
145 self.client_guid = client_guid;
146 self
147 }
148
149 #[must_use]
151 pub fn with_compression_capabilities(mut self, compression: CompressionCapabilities) -> Self {
152 self.compression = Some(compression);
153 self
154 }
155
156 #[must_use]
158 pub fn with_compression_algorithms(
159 mut self,
160 compression_algorithms: Vec<CompressionAlgorithm>,
161 ) -> Self {
162 self.compression = Some(CompressionCapabilities {
163 compression_algorithms,
164 flags: CompressionCapabilityFlags::empty(),
165 });
166 self
167 }
168
169 #[must_use]
171 pub fn with_ntlm_credentials(mut self, credentials: NtlmCredentials) -> Self {
172 self.auth = Some(BuilderAuth::Ntlm(credentials));
173 self
174 }
175
176 #[cfg(feature = "kerberos-api")]
178 #[cfg_attr(
179 docsrs,
180 doc(cfg(any(feature = "kerberos", feature = "kerberos-gssapi")))
181 )]
182 #[must_use]
183 pub fn with_kerberos_credentials(
184 mut self,
185 credentials: KerberosCredentials,
186 target: KerberosTarget,
187 ) -> Self {
188 self.auth = Some(BuilderAuth::Kerberos {
189 credentials,
190 target,
191 });
192 self
193 }
194
195 pub fn build(self) -> Result<Client, CoreError> {
197 let auth = self.auth.ok_or(CoreError::InvalidInput(
198 "client builder requires NTLM or Kerberos credentials",
199 ))?;
200
201 let config = match auth {
202 BuilderAuth::Ntlm(credentials) => {
203 SmbSessionConfig::new(self.target.server().to_owned(), credentials)
204 }
205 #[cfg(feature = "kerberos-api")]
206 BuilderAuth::Kerberos {
207 credentials,
208 target,
209 } => SmbSessionConfig::kerberos(self.target.server().to_owned(), credentials, target),
210 }
211 .with_transport_target(self.target)
212 .with_signing_mode(self.signing_mode)
213 .with_capabilities(self.capabilities)
214 .with_dialects(self.dialects)
215 .with_client_guid(self.client_guid);
216 let config = if let Some(compression) = self.compression {
217 config.with_compression_capabilities(compression)
218 } else {
219 config
220 };
221
222 Ok(Client::from_session_config(config))
223 }
224}
225
226#[derive(Debug, Clone)]
228pub struct Client {
229 config: SmbSessionConfig,
230}
231
232impl Client {
233 #[must_use]
235 pub fn builder(server: impl Into<String>) -> ClientBuilder {
236 ClientBuilder::new(server)
237 }
238
239 #[must_use]
241 pub fn from_session_config(config: SmbSessionConfig) -> Self {
242 Self { config }
243 }
244
245 #[must_use]
247 pub fn session_config(&self) -> &SmbSessionConfig {
248 &self.config
249 }
250
251 #[must_use]
253 pub fn into_session_config(self) -> SmbSessionConfig {
254 self.config
255 }
256
257 #[must_use]
259 pub fn server(&self) -> &str {
260 self.config.server()
261 }
262
263 #[must_use]
265 pub fn port(&self) -> u16 {
266 self.config.port()
267 }
268
269 #[must_use]
271 pub fn connect_host(&self) -> &str {
272 self.config.connect_host()
273 }
274
275 #[must_use]
277 pub fn tls_server_name(&self) -> &str {
278 self.config.tls_server_name()
279 }
280
281 #[must_use]
283 pub fn transport_target(&self) -> &TransportTarget {
284 self.config.transport_target()
285 }
286
287 #[must_use]
289 pub fn transport_protocol(&self) -> TransportProtocol {
290 self.config.transport_protocol()
291 }
292
293 pub async fn connect(&self) -> Result<Session, CoreError> {
295 let connection = connect_session(&self.config).await?;
296 Ok(Session {
297 server: self.config.server().to_owned(),
298 connection,
299 })
300 }
301
302 #[cfg(feature = "quic")]
304 #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
305 pub async fn connect_quic(&self) -> Result<Session<QuicTransport>, CoreError> {
306 if self.config.transport_protocol() != TransportProtocol::Quic {
307 return Err(CoreError::InvalidInput(
308 "client is not configured for an SMB over QUIC transport target",
309 ));
310 }
311 let connection = connect_session_quic(&self.config).await?;
312 Ok(Session {
313 server: self.config.server().to_owned(),
314 connection,
315 })
316 }
317
318 pub async fn connect_share(&self, share: &str) -> Result<Share, CoreError> {
320 self.connect().await?.connect_share(share).await
321 }
322
323 #[cfg(feature = "quic")]
325 #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
326 pub async fn connect_share_quic(&self, share: &str) -> Result<Share<QuicTransport>, CoreError> {
327 if self.config.transport_protocol() != TransportProtocol::Quic {
328 return Err(CoreError::InvalidInput(
329 "client is not configured for an SMB over QUIC transport target",
330 ));
331 }
332 let connection = connect_tree_quic(&self.config, share).await?;
333 Ok(Share {
334 server: self.config.server().to_owned(),
335 name: normalize_share_name(share)?,
336 connection,
337 })
338 }
339
340 pub async fn connect_ipc(&self) -> Result<Share, CoreError> {
342 self.connect_share("IPC$").await
343 }
344
345 #[cfg(feature = "quic")]
347 #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
348 pub async fn connect_ipc_quic(&self) -> Result<Share<QuicTransport>, CoreError> {
349 self.connect_share_quic("IPC$").await
350 }
351
352 pub async fn connect_lsarpc(&self) -> Result<LsarpcClient, CoreError> {
354 self.connect().await?.connect_lsarpc().await
355 }
356
357 #[cfg(feature = "quic")]
359 #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
360 pub async fn connect_lsarpc_quic(&self) -> Result<LsarpcClient<QuicTransport>, CoreError> {
361 self.connect_quic().await?.connect_lsarpc().await
362 }
363
364 pub async fn connect_srvsvc(&self) -> Result<SrvsvcClient, CoreError> {
366 self.connect().await?.connect_srvsvc().await
367 }
368
369 #[cfg(feature = "quic")]
371 #[cfg_attr(docsrs, doc(cfg(feature = "quic")))]
372 pub async fn connect_srvsvc_quic(&self) -> Result<SrvsvcClient<QuicTransport>, CoreError> {
373 self.connect_quic().await?.connect_srvsvc().await
374 }
375}
376
377#[derive(Debug)]
379pub struct Session<T = TokioTcpTransport> {
380 server: String,
381 connection: Connection<T, Authenticated>,
382}
383
384impl<T> Session<T>
385where
386 T: SmbTransport + Send,
387{
388 #[must_use]
390 pub fn server(&self) -> &str {
391 &self.server
392 }
393
394 #[must_use]
396 pub fn session_id(&self) -> SessionId {
397 self.connection.session_id()
398 }
399
400 #[must_use]
402 pub fn session_key(&self) -> Option<&[u8]> {
403 self.connection.session_key()
404 }
405
406 #[must_use]
408 pub fn connection(&self) -> &Connection<T, Authenticated> {
409 &self.connection
410 }
411
412 #[must_use]
414 pub fn connection_mut(&mut self) -> &mut Connection<T, Authenticated> {
415 &mut self.connection
416 }
417
418 #[must_use]
420 pub fn into_connection(self) -> Connection<T, Authenticated> {
421 self.connection
422 }
423
424 pub async fn echo(&mut self) -> Result<EchoResponse, CoreError> {
426 self.connection.echo().await
427 }
428
429 pub async fn connect_share(self, share: &str) -> Result<Share<T>, CoreError> {
431 let normalized_share = normalize_share_name(share)?;
432 let unc = format!(r"\\{}\{}", self.server, normalized_share);
433 let connection = self
434 .connection
435 .tree_connect(&TreeConnectRequest::from_unc(&unc))
436 .await?;
437 Ok(Share {
438 server: self.server,
439 name: normalized_share,
440 connection,
441 })
442 }
443
444 pub async fn connect_ipc(self) -> Result<Share<T>, CoreError> {
446 self.connect_share("IPC$").await
447 }
448
449 pub async fn connect_pipe(
451 self,
452 pipe_name: &str,
453 access: PipeAccess,
454 ) -> Result<NamedPipe<T>, CoreError> {
455 self.connect_ipc().await?.open_pipe(pipe_name, access).await
456 }
457
458 pub async fn connect_rpc_pipe(
460 self,
461 pipe_name: &str,
462 access: PipeAccess,
463 ) -> Result<PipeRpcClient<T>, CoreError> {
464 let pipe = self.connect_pipe(pipe_name, access).await?;
465 Ok(PipeRpcClient::new(pipe))
466 }
467
468 pub async fn bind_rpc(
470 self,
471 pipe_name: &str,
472 context_id: u16,
473 abstract_syntax: SyntaxId,
474 ) -> Result<PipeRpcClient<T>, CoreError> {
475 let mut rpc = self
476 .connect_rpc_pipe(pipe_name, PipeAccess::ReadWrite)
477 .await?;
478 rpc.bind_context(context_id, abstract_syntax).await?;
479 Ok(rpc)
480 }
481
482 pub async fn connect_srvsvc(self) -> Result<SrvsvcClient<T>, CoreError> {
484 let rpc = self
485 .connect_rpc_pipe("srvsvc", PipeAccess::ReadWrite)
486 .await?;
487 SrvsvcClient::bind(rpc).await
488 }
489
490 pub async fn connect_lsarpc(self) -> Result<LsarpcClient<T>, CoreError> {
492 let rpc = self
493 .connect_rpc_pipe("lsarpc", PipeAccess::ReadWrite)
494 .await?;
495 LsarpcClient::bind(rpc).await
496 }
497
498 pub async fn connect_samr_pipe(self, pipe_name: &str) -> Result<SamrClient<T>, CoreError> {
500 let rpc = self
501 .connect_rpc_pipe(pipe_name, PipeAccess::ReadWrite)
502 .await?;
503 SamrClient::bind(rpc).await
504 }
505
506 pub async fn connect_samr(self) -> Result<SamrClient<T>, CoreError> {
508 self.connect_samr_pipe("lsarpc").await
509 }
510
511 pub async fn logoff(self) -> Result<(), CoreError> {
513 let _ = self.connection.logoff().await?;
514 Ok(())
515 }
516}
517
518#[derive(Debug)]
520pub struct Share<T = TokioTcpTransport> {
521 server: String,
522 name: String,
523 connection: Connection<T, TreeConnected>,
524}
525
526impl<T> Share<T>
527where
528 T: SmbTransport + Send,
529{
530 #[must_use]
532 pub fn server(&self) -> &str {
533 &self.server
534 }
535
536 #[must_use]
538 pub fn name(&self) -> &str {
539 &self.name
540 }
541
542 #[must_use]
544 pub fn session_id(&self) -> SessionId {
545 self.connection.session_id()
546 }
547
548 #[must_use]
550 pub fn tree_id(&self) -> TreeId {
551 self.connection.tree_id()
552 }
553
554 #[must_use]
556 pub fn session_key(&self) -> Option<&[u8]> {
557 self.connection.session_key()
558 }
559
560 #[must_use]
562 pub fn connection(&self) -> &Connection<T, TreeConnected> {
563 &self.connection
564 }
565
566 #[must_use]
568 pub fn connection_mut(&mut self) -> &mut Connection<T, TreeConnected> {
569 &mut self.connection
570 }
571
572 #[must_use]
574 pub fn into_connection(self) -> Connection<T, TreeConnected> {
575 self.connection
576 }
577
578 pub async fn open(mut self, path: &str, options: OpenOptions) -> Result<File<T>, CoreError> {
580 let normalized_path = normalize_share_path(path)?;
581 let dialect = self.connection.state().negotiated.dialect_revision;
582 let create_request = options.to_create_request(&normalized_path)?;
583 let (durable_handle, file_id) = if let Some(durable) = options.durable_options(dialect) {
584 let durable_handle = self
585 .connection
586 .create_durable(&create_request, durable)
587 .await?;
588 (Some(durable_handle.clone()), durable_handle.file_id())
589 } else {
590 let response = self.connection.create(&create_request).await?;
591 (None, response.file_id)
592 };
593
594 let resilient_handle = if let Some(timeout) = options.resilient_timeout {
595 Some(self.connection.request_resiliency(file_id, timeout).await?)
596 } else {
597 None
598 };
599
600 Ok(File {
601 share: self,
602 path: normalized_path,
603 file_id,
604 durable_handle,
605 resilient_handle,
606 })
607 }
608
609 pub async fn open_pipe(
611 self,
612 pipe_name: &str,
613 access: PipeAccess,
614 ) -> Result<NamedPipe<T>, CoreError> {
615 let normalized_pipe = normalize_pipe_name(pipe_name)?;
616 NamedPipe::open(self.connection, &normalized_pipe, access).await
617 }
618
619 pub async fn connect_rpc_pipe(
621 self,
622 pipe_name: &str,
623 access: PipeAccess,
624 ) -> Result<PipeRpcClient<T>, CoreError> {
625 let pipe = self.open_pipe(pipe_name, access).await?;
626 Ok(PipeRpcClient::new(pipe))
627 }
628
629 pub async fn bind_rpc(
631 self,
632 pipe_name: &str,
633 context_id: u16,
634 abstract_syntax: SyntaxId,
635 ) -> Result<PipeRpcClient<T>, CoreError> {
636 let mut rpc = self
637 .connect_rpc_pipe(pipe_name, PipeAccess::ReadWrite)
638 .await?;
639 rpc.bind_context(context_id, abstract_syntax).await?;
640 Ok(rpc)
641 }
642
643 pub async fn connect_srvsvc(self) -> Result<SrvsvcClient<T>, CoreError> {
645 let rpc = self
646 .connect_rpc_pipe("srvsvc", PipeAccess::ReadWrite)
647 .await?;
648 SrvsvcClient::bind(rpc).await
649 }
650
651 pub async fn connect_lsarpc(self) -> Result<LsarpcClient<T>, CoreError> {
653 let rpc = self
654 .connect_rpc_pipe("lsarpc", PipeAccess::ReadWrite)
655 .await?;
656 LsarpcClient::bind(rpc).await
657 }
658
659 pub async fn connect_samr_pipe(self, pipe_name: &str) -> Result<SamrClient<T>, CoreError> {
661 let rpc = self
662 .connect_rpc_pipe(pipe_name, PipeAccess::ReadWrite)
663 .await?;
664 SamrClient::bind(rpc).await
665 }
666
667 pub async fn connect_samr(self) -> Result<SamrClient<T>, CoreError> {
669 self.connect_samr_pipe("lsarpc").await
670 }
671
672 pub async fn read(&mut self, path: &str) -> Result<Vec<u8>, CoreError> {
674 let normalized_path = normalize_share_path(path)?;
675 let create_request = OpenOptions::new()
676 .read(true)
677 .to_create_request(&normalized_path)?;
678 let response = self.connection.create(&create_request).await?;
679 let file_id = response.file_id;
680 let size = self.stat_by_id(file_id).await?.size;
681 let mut output = Vec::with_capacity(usize::try_from(size).unwrap_or(0));
682 let mut offset = 0u64;
683
684 while offset < size {
685 let remaining = size - offset;
686 let chunk_len = remaining.min(MAX_IO_CHUNK_SIZE as u64) as u32;
687 let response = self
688 .connection
689 .read(&ReadRequest::for_file(file_id, offset, chunk_len))
690 .await?;
691 if response.data.is_empty() {
692 break;
693 }
694 offset = offset.saturating_add(response.data.len() as u64);
695 output.extend_from_slice(&response.data);
696 }
697
698 self.connection
699 .close(&CloseRequest { flags: 0, file_id })
700 .await?;
701 Ok(output)
702 }
703
704 pub async fn get(&mut self, path: &str) -> Result<Vec<u8>, CoreError> {
709 self.read(path).await
710 }
711
712 pub async fn write(&mut self, path: &str, data: &[u8]) -> Result<(), CoreError> {
714 let normalized_path = normalize_share_path(path)?;
715 let create_request = OpenOptions::new()
716 .write(true)
717 .create(true)
718 .truncate(true)
719 .to_create_request(&normalized_path)?;
720 let response = self.connection.create(&create_request).await?;
721 let file_id = response.file_id;
722 let mut offset = 0u64;
723
724 while (offset as usize) < data.len() {
725 let chunk_end = (offset as usize + MAX_IO_CHUNK_SIZE).min(data.len());
726 self.connection
727 .write(&WriteRequest::for_file(
728 file_id,
729 offset,
730 data[offset as usize..chunk_end].to_vec(),
731 ))
732 .await?;
733 offset = chunk_end as u64;
734 }
735
736 self.connection
737 .flush(&FlushRequest::for_file(file_id))
738 .await?;
739 self.connection
740 .close(&CloseRequest { flags: 0, file_id })
741 .await?;
742 Ok(())
743 }
744
745 pub async fn put(&mut self, path: &str, data: &[u8]) -> Result<(), CoreError> {
750 self.write(path, data).await
751 }
752
753 pub async fn stat(&mut self, path: &str) -> Result<FileMetadata, CoreError> {
755 let normalized_path = normalize_share_path(path)?;
756 let mut create_request = CreateRequest::from_path(&normalized_path);
757 create_request.desired_access = FILE_READ_ATTRIBUTES | READ_CONTROL | SYNCHRONIZE;
758 create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
759 create_request.create_disposition = CreateDisposition::Open;
760 create_request.create_options = CreateOptions::NON_DIRECTORY_FILE;
761 let response = self.connection.create(&create_request).await?;
762 let file_id = response.file_id;
763 let metadata = self.stat_by_id(file_id).await?;
764 self.connection
765 .close(&CloseRequest { flags: 0, file_id })
766 .await?;
767 Ok(metadata)
768 }
769
770 pub async fn metadata(&mut self, path: &str) -> Result<FileMetadata, CoreError> {
775 self.stat(path).await
776 }
777
778 pub async fn list(&mut self, path: &str) -> Result<Vec<DirectoryEntry>, CoreError> {
783 let normalized_path = normalize_share_path(path)?;
784 let mut create_request = CreateRequest::from_path(&normalized_path);
785 create_request.desired_access = FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | SYNCHRONIZE;
786 create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
787 create_request.create_disposition = CreateDisposition::Open;
788 create_request.create_options = CreateOptions::DIRECTORY_FILE;
789 let response = self.connection.create(&create_request).await?;
790 let file_id = response.file_id;
791
792 let list_result = async {
793 let mut request =
794 QueryDirectoryRequest::for_pattern(file_id, "*", DIRECTORY_QUERY_BUFFER_SIZE);
795 let mut entries = Vec::new();
796
797 loop {
798 let response = self.connection.query_directory(&request).await?;
799 let batch = response
800 .directory_entries()
801 .map_err(CoreError::from)?
802 .into_iter()
803 .filter(|entry| entry.file_name != "." && entry.file_name != "..")
804 .map(directory_entry_from_info)
805 .collect::<Vec<_>>();
806 if batch.is_empty() {
807 break;
808 }
809 entries.extend(batch);
810 request.flags = QueryDirectoryFlags::empty();
811 request.file_name.clear();
812 }
813
814 Ok(entries)
815 }
816 .await;
817
818 let close_result = self
819 .connection
820 .close(&CloseRequest { flags: 0, file_id })
821 .await;
822 match (list_result, close_result) {
823 (Ok(entries), Ok(_)) => Ok(entries),
824 (Err(error), _) => Err(error),
825 (Ok(_), Err(error)) => Err(error),
826 }
827 }
828
829 pub async fn read_dir(&mut self, path: &str) -> Result<Vec<DirectoryEntry>, CoreError> {
834 self.list(path).await
835 }
836
837 pub async fn create_dir(&mut self, path: &str) -> Result<(), CoreError> {
839 let normalized_path = normalize_share_path(path)?;
840 let mut create_request = CreateRequest::from_path(&normalized_path);
841 create_request.desired_access = FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE;
842 create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
843 create_request.file_attributes = FileAttributes::DIRECTORY;
844 create_request.create_disposition = CreateDisposition::Create;
845 create_request.create_options = CreateOptions::DIRECTORY_FILE;
846 let response = self.connection.create(&create_request).await?;
847 let file_id = response.file_id;
848 self.connection
849 .close(&CloseRequest { flags: 0, file_id })
850 .await?;
851 Ok(())
852 }
853
854 pub async fn mkdir(&mut self, path: &str) -> Result<(), CoreError> {
859 self.create_dir(path).await
860 }
861
862 pub async fn rename(&mut self, source: &str, destination: &str) -> Result<(), CoreError> {
864 let normalized_source = normalize_share_path(source)?;
865 let normalized_destination = normalize_share_path(destination)?;
866
867 let mut create_request = CreateRequest::from_path(&normalized_source);
868 create_request.desired_access =
869 DELETE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE;
870 create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
871 create_request.create_disposition = CreateDisposition::Open;
872 let response = self.connection.create(&create_request).await?;
873 let file_id = response.file_id;
874
875 let rename_result = self
876 .connection
877 .set_info(&SetInfoRequest::for_file_info(
878 file_id,
879 FileInfoClass::RenameInformation,
880 RenameInformation::from_path(&normalized_destination, false).encode(),
881 ))
882 .await;
883 let close_result = self
884 .connection
885 .close(&CloseRequest { flags: 0, file_id })
886 .await;
887 match (rename_result, close_result) {
888 (Ok(_), Ok(_)) => Ok(()),
889 (Err(error), _) => Err(error),
890 (Ok(_), Err(error)) => Err(error),
891 }
892 }
893
894 pub async fn remove(&mut self, path: &str) -> Result<(), CoreError> {
896 let normalized_path = normalize_share_path(path)?;
897 let mut create_request = CreateRequest::from_path(&normalized_path);
898 create_request.desired_access =
899 DELETE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | READ_CONTROL | SYNCHRONIZE;
900 create_request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
901 create_request.create_disposition = CreateDisposition::Open;
902 create_request.create_options = CreateOptions::NON_DIRECTORY_FILE;
903 let response = self.connection.create(&create_request).await?;
904 let file_id = response.file_id;
905 self.connection
906 .set_info(&SetInfoRequest::for_file_info(
907 file_id,
908 FileInfoClass::DispositionInformation,
909 DispositionInformation {
910 delete_pending: true,
911 }
912 .encode(),
913 ))
914 .await?;
915 self.connection
916 .close(&CloseRequest { flags: 0, file_id })
917 .await?;
918 Ok(())
919 }
920
921 pub async fn open_reader(self, path: &str) -> Result<File<T>, CoreError> {
923 self.open(path, OpenOptions::new().read(true)).await
924 }
925
926 pub async fn open_writer(self, path: &str) -> Result<File<T>, CoreError> {
929 self.open(
930 path,
931 OpenOptions::new().write(true).create(true).truncate(true),
932 )
933 .await
934 }
935
936 pub async fn disconnect(self) -> Result<Session<T>, CoreError> {
938 let connection = self.connection.tree_disconnect().await?;
939 Ok(Session {
940 server: self.server,
941 connection,
942 })
943 }
944
945 pub async fn logoff(self) -> Result<(), CoreError> {
947 self.disconnect().await?.logoff().await
948 }
949
950 async fn stat_by_id(&mut self, file_id: FileId) -> Result<FileMetadata, CoreError> {
951 let basic = self
952 .connection
953 .query_info(&QueryInfoRequest::for_file_info(
954 file_id,
955 FileInfoClass::BasicInformation,
956 ))
957 .await?;
958 let basic = FileBasicInformation::decode(&basic.output_buffer).map_err(CoreError::from)?;
959
960 let standard = self
961 .connection
962 .query_info(&QueryInfoRequest::for_file_info(
963 file_id,
964 FileInfoClass::StandardInformation,
965 ))
966 .await?;
967 let standard =
968 FileStandardInformation::decode(&standard.output_buffer).map_err(CoreError::from)?;
969
970 Ok(metadata_from_info(basic, standard))
971 }
972}
973
974#[derive(Debug, Clone, PartialEq, Eq)]
976pub struct OpenOptions {
977 read: bool,
978 write: bool,
979 create: bool,
980 create_new: bool,
981 truncate: bool,
982 durable: Option<DurableOpenOptions>,
983 resilient_timeout: Option<u32>,
984}
985
986impl OpenOptions {
987 #[must_use]
989 pub fn new() -> Self {
990 Self::default()
991 }
992
993 #[must_use]
995 pub fn read(mut self, read: bool) -> Self {
996 self.read = read;
997 self
998 }
999
1000 #[must_use]
1002 pub fn write(mut self, write: bool) -> Self {
1003 self.write = write;
1004 self
1005 }
1006
1007 #[must_use]
1009 pub fn create(mut self, create: bool) -> Self {
1010 self.create = create;
1011 self
1012 }
1013
1014 #[must_use]
1016 pub fn create_new(mut self, create_new: bool) -> Self {
1017 self.create_new = create_new;
1018 self
1019 }
1020
1021 #[must_use]
1023 pub fn truncate(mut self, truncate: bool) -> Self {
1024 self.truncate = truncate;
1025 self
1026 }
1027
1028 #[must_use]
1030 pub fn durable(mut self, durable: DurableOpenOptions) -> Self {
1031 self.durable = Some(durable);
1032 self
1033 }
1034
1035 #[must_use]
1037 pub fn resilient(mut self, timeout: u32) -> Self {
1038 self.resilient_timeout = Some(timeout);
1039 self
1040 }
1041
1042 fn to_create_request(&self, path: &str) -> Result<CreateRequest, CoreError> {
1043 if !self.read && !self.write {
1044 return Err(CoreError::InvalidInput(
1045 "open options must request read and/or write access",
1046 ));
1047 }
1048 if (self.truncate || self.create || self.create_new) && !self.write {
1049 return Err(CoreError::InvalidInput(
1050 "create and truncate operations require write access",
1051 ));
1052 }
1053
1054 let mut request = CreateRequest::from_path(path);
1055 request.desired_access = desired_access_mask(self);
1056 request.share_access = ShareAccess::READ | ShareAccess::WRITE | ShareAccess::DELETE;
1057 request.file_attributes = FileAttributes::NORMAL;
1058 request.create_options = CreateOptions::NON_DIRECTORY_FILE;
1059 request.create_disposition = create_disposition(self);
1060 Ok(request)
1061 }
1062
1063 fn durable_options(&self, dialect: Dialect) -> Option<DurableOpenOptions> {
1064 self.durable.clone().map(|durable| {
1065 if dialect_supports_durable_v2(dialect) && durable.create_guid.is_none() {
1066 durable.with_create_guid(random())
1067 } else {
1068 durable
1069 }
1070 })
1071 }
1072}
1073
1074impl Default for OpenOptions {
1075 fn default() -> Self {
1076 Self {
1077 read: false,
1078 write: false,
1079 create: false,
1080 create_new: false,
1081 truncate: false,
1082 durable: None,
1083 resilient_timeout: None,
1084 }
1085 }
1086}
1087
1088#[derive(Debug, Clone, PartialEq, Eq)]
1090pub struct FileMetadata {
1091 pub size: u64,
1093 pub allocation_size: u64,
1095 pub attributes: FileAttributes,
1097 pub created: Option<SystemTime>,
1099 pub accessed: Option<SystemTime>,
1101 pub written: Option<SystemTime>,
1103 pub changed: Option<SystemTime>,
1105 pub delete_pending: bool,
1107}
1108
1109impl FileMetadata {
1110 #[must_use]
1112 pub fn is_directory(&self) -> bool {
1113 self.attributes.contains(FileAttributes::DIRECTORY)
1114 }
1115
1116 #[must_use]
1118 pub fn is_file(&self) -> bool {
1119 !self.is_directory()
1120 }
1121}
1122
1123#[derive(Debug, Clone, PartialEq, Eq)]
1125pub struct DirectoryEntry {
1126 pub name: String,
1128 pub file_index: u32,
1130 pub metadata: FileMetadata,
1132}
1133
1134impl DirectoryEntry {
1135 #[must_use]
1137 pub fn is_directory(&self) -> bool {
1138 self.metadata.is_directory()
1139 }
1140
1141 #[must_use]
1143 pub fn is_file(&self) -> bool {
1144 self.metadata.is_file()
1145 }
1146}
1147
1148#[derive(Debug)]
1150pub struct File<T = TokioTcpTransport> {
1151 share: Share<T>,
1152 path: String,
1153 file_id: FileId,
1154 durable_handle: Option<DurableHandle>,
1155 resilient_handle: Option<ResilientHandle>,
1156}
1157
1158impl<T> File<T>
1159where
1160 T: SmbTransport + Send,
1161{
1162 #[must_use]
1164 pub fn path(&self) -> &str {
1165 &self.path
1166 }
1167
1168 #[must_use]
1170 pub fn file_id(&self) -> FileId {
1171 self.file_id
1172 }
1173
1174 #[must_use]
1176 pub fn durable_handle(&self) -> Option<&DurableHandle> {
1177 self.durable_handle.as_ref()
1178 }
1179
1180 #[must_use]
1182 pub fn resilient_handle(&self) -> Option<ResilientHandle> {
1183 self.resilient_handle
1184 }
1185
1186 #[must_use]
1188 pub fn connection(&self) -> &Connection<T, TreeConnected> {
1189 self.share.connection()
1190 }
1191
1192 #[must_use]
1194 pub fn connection_mut(&mut self) -> &mut Connection<T, TreeConnected> {
1195 self.share.connection_mut()
1196 }
1197
1198 pub async fn read_all(&mut self) -> Result<Vec<u8>, CoreError> {
1200 let metadata = self.stat().await?;
1201 let mut output = Vec::with_capacity(usize::try_from(metadata.size).unwrap_or(0));
1202 let mut offset = 0u64;
1203
1204 while offset < metadata.size {
1205 let remaining = metadata.size - offset;
1206 let chunk_len = remaining.min(MAX_IO_CHUNK_SIZE as u64) as u32;
1207 let response = self
1208 .share
1209 .connection
1210 .read(&ReadRequest::for_file(self.file_id, offset, chunk_len))
1211 .await?;
1212 if response.data.is_empty() {
1213 break;
1214 }
1215 offset = offset.saturating_add(response.data.len() as u64);
1216 output.extend_from_slice(&response.data);
1217 }
1218
1219 Ok(output)
1220 }
1221
1222 pub async fn read_to_end(&mut self) -> Result<Vec<u8>, CoreError> {
1227 self.read_all().await
1228 }
1229
1230 pub async fn write_all(&mut self, data: &[u8]) -> Result<(), CoreError> {
1232 let mut offset = 0u64;
1233 while (offset as usize) < data.len() {
1234 let chunk_end = (offset as usize + MAX_IO_CHUNK_SIZE).min(data.len());
1235 self.share
1236 .connection
1237 .write(&WriteRequest::for_file(
1238 self.file_id,
1239 offset,
1240 data[offset as usize..chunk_end].to_vec(),
1241 ))
1242 .await?;
1243 offset = chunk_end as u64;
1244 }
1245 Ok(())
1246 }
1247
1248 pub async fn flush(&mut self) -> Result<(), CoreError> {
1250 let _ = self
1251 .share
1252 .connection
1253 .flush(&FlushRequest::for_file(self.file_id))
1254 .await?;
1255 Ok(())
1256 }
1257
1258 pub async fn sync_all(&mut self) -> Result<(), CoreError> {
1263 self.flush().await
1264 }
1265
1266 pub async fn stat(&mut self) -> Result<FileMetadata, CoreError> {
1268 self.share.stat_by_id(self.file_id).await
1269 }
1270
1271 pub async fn request_resiliency(&mut self, timeout: u32) -> Result<ResilientHandle, CoreError> {
1273 let resilient = self
1274 .share
1275 .connection
1276 .request_resiliency(self.file_id, timeout)
1277 .await?;
1278 self.resilient_handle = Some(resilient);
1279 if let Some(durable) = self.durable_handle.take() {
1280 self.durable_handle = Some(durable.with_resilient_timeout(timeout));
1281 }
1282 Ok(resilient)
1283 }
1284
1285 pub async fn close(mut self) -> Result<Share<T>, CoreError> {
1287 self.share
1288 .connection
1289 .close(&CloseRequest {
1290 flags: 0,
1291 file_id: self.file_id,
1292 })
1293 .await?;
1294 Ok(self.share)
1295 }
1296
1297 #[must_use]
1299 pub fn into_parts(
1300 self,
1301 ) -> (
1302 Share<T>,
1303 FileId,
1304 Option<DurableHandle>,
1305 Option<ResilientHandle>,
1306 ) {
1307 (
1308 self.share,
1309 self.file_id,
1310 self.durable_handle,
1311 self.resilient_handle,
1312 )
1313 }
1314}
1315
1316fn normalize_share_name(share: &str) -> Result<String, CoreError> {
1317 let share = share.trim_matches(['\\', '/']);
1318 if share.is_empty() {
1319 return Err(CoreError::PathInvalid("share name must not be empty"));
1320 }
1321 if share.contains(['\\', '/', '\0']) {
1322 return Err(CoreError::PathInvalid(
1323 "share name must not contain separators or NUL bytes",
1324 ));
1325 }
1326 Ok(share.to_owned())
1327}
1328
1329fn normalize_share_path(path: &str) -> Result<String, CoreError> {
1330 if path.contains('\0') {
1331 return Err(CoreError::PathInvalid("path must not contain NUL bytes"));
1332 }
1333 if matches!(path, "\\" | "/") {
1334 return Ok("\\".to_string());
1335 }
1336
1337 let normalized = path
1338 .split(['\\', '/'])
1339 .filter(|segment| !segment.is_empty())
1340 .collect::<Vec<_>>()
1341 .join("\\");
1342 if normalized.is_empty() {
1343 return Err(CoreError::PathInvalid("path must not be empty"));
1344 }
1345 Ok(normalized)
1346}
1347
1348fn normalize_pipe_name(pipe_name: &str) -> Result<String, CoreError> {
1349 if pipe_name.contains('\0') {
1350 return Err(CoreError::PathInvalid(
1351 "pipe name must not contain NUL bytes",
1352 ));
1353 }
1354
1355 let normalized = pipe_name.trim().replace('/', "\\");
1356 let trimmed = normalized.trim_matches('\\');
1357 if trimmed.is_empty() {
1358 return Err(CoreError::PathInvalid("pipe name must not be empty"));
1359 }
1360
1361 let trimmed = if trimmed.len() > 5 && trimmed[..5].eq_ignore_ascii_case("pipe\\") {
1362 trimmed[5..].trim_start_matches('\\')
1363 } else {
1364 trimmed
1365 };
1366 if trimmed.is_empty() {
1367 return Err(CoreError::PathInvalid("pipe name must not be empty"));
1368 }
1369
1370 Ok(trimmed.to_owned())
1371}
1372
1373fn desired_access_mask(options: &OpenOptions) -> u32 {
1374 let mut desired_access = READ_CONTROL | SYNCHRONIZE;
1375 if options.read {
1376 desired_access |= FILE_READ_DATA | FILE_READ_EA | FILE_READ_ATTRIBUTES;
1377 }
1378 if options.write {
1379 desired_access |=
1380 FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES;
1381 }
1382 desired_access
1383}
1384
1385fn create_disposition(options: &OpenOptions) -> CreateDisposition {
1386 if options.create_new {
1387 CreateDisposition::Create
1388 } else if options.create && options.truncate {
1389 CreateDisposition::OverwriteIf
1390 } else if options.create {
1391 CreateDisposition::OpenIf
1392 } else if options.truncate {
1393 CreateDisposition::Overwrite
1394 } else {
1395 CreateDisposition::Open
1396 }
1397}
1398
1399fn dialect_supports_durable_v2(dialect: Dialect) -> bool {
1400 matches!(dialect, Dialect::Smb300 | Dialect::Smb302 | Dialect::Smb311)
1401}
1402
1403fn metadata_from_info(
1404 basic: FileBasicInformation,
1405 standard: FileStandardInformation,
1406) -> FileMetadata {
1407 let mut attributes = basic.file_attributes;
1408 if standard.directory {
1409 attributes |= FileAttributes::DIRECTORY;
1410 }
1411
1412 FileMetadata {
1413 size: standard.end_of_file,
1414 allocation_size: standard.allocation_size,
1415 attributes,
1416 created: system_time_from_windows_ticks(basic.creation_time),
1417 accessed: system_time_from_windows_ticks(basic.last_access_time),
1418 written: system_time_from_windows_ticks(basic.last_write_time),
1419 changed: system_time_from_windows_ticks(basic.change_time),
1420 delete_pending: standard.delete_pending,
1421 }
1422}
1423
1424fn directory_entry_from_info(entry: DirectoryInformationEntry) -> DirectoryEntry {
1425 DirectoryEntry {
1426 name: entry.file_name,
1427 file_index: entry.file_index,
1428 metadata: FileMetadata {
1429 size: entry.end_of_file,
1430 allocation_size: entry.allocation_size,
1431 attributes: entry.file_attributes,
1432 created: system_time_from_windows_ticks(entry.creation_time),
1433 accessed: system_time_from_windows_ticks(entry.last_access_time),
1434 written: system_time_from_windows_ticks(entry.last_write_time),
1435 changed: system_time_from_windows_ticks(entry.change_time),
1436 delete_pending: false,
1437 },
1438 }
1439}
1440
1441fn system_time_from_windows_ticks(value: u64) -> Option<SystemTime> {
1442 if value == 0 {
1443 return None;
1444 }
1445
1446 let unix_ticks = value.checked_sub(SEC_TO_UNIX_EPOCH * WINDOWS_TICK)?;
1447 Some(UNIX_EPOCH + Duration::from_nanos(unix_ticks.saturating_mul(100)))
1448}
1449
1450#[cfg(test)]
1451mod tests {
1452 use std::collections::VecDeque;
1453
1454 use async_trait::async_trait;
1455 use smolder_proto::smb::netbios::SessionMessage;
1456 use smolder_proto::smb::smb2::{
1457 CloseResponse, Command, CreateDisposition, CreateResponse, Dialect, FileAttributes, FileId,
1458 FlushResponse, GlobalCapabilities, Header, MessageId, NegotiateRequest, NegotiateResponse,
1459 OplockLevel, QueryDirectoryResponse, QueryInfoResponse, SessionFlags, SessionSetupResponse,
1460 ShareFlags, ShareType, SigningMode, TreeCapabilities, TreeConnectRequest,
1461 TreeConnectResponse, TreeId, WriteResponse,
1462 };
1463 use smolder_proto::smb::status::NtStatus;
1464
1465 use crate::auth::NtlmAuthenticator;
1466 use crate::auth::NtlmCredentials;
1467 #[cfg(feature = "kerberos-api")]
1468 use crate::auth::{KerberosCredentials, KerberosTarget};
1469 use crate::client::Connection;
1470 use crate::transport::Transport;
1471 use crate::transport::{TransportProtocol, TransportTarget};
1472
1473 use super::{
1474 normalize_pipe_name, normalize_share_name, normalize_share_path, Client, ClientBuilder,
1475 FileMetadata, OpenOptions, Share,
1476 };
1477
1478 #[derive(Debug)]
1479 struct ScriptedTransport {
1480 reads: VecDeque<Vec<u8>>,
1481 writes: Vec<Vec<u8>>,
1482 }
1483
1484 impl ScriptedTransport {
1485 fn new(reads: Vec<Vec<u8>>) -> Self {
1486 Self {
1487 reads: reads.into(),
1488 writes: Vec::new(),
1489 }
1490 }
1491 }
1492
1493 #[async_trait]
1494 impl Transport for ScriptedTransport {
1495 async fn send(&mut self, frame: &[u8]) -> std::io::Result<()> {
1496 self.writes.push(frame.to_vec());
1497 Ok(())
1498 }
1499
1500 async fn recv(&mut self) -> std::io::Result<Vec<u8>> {
1501 self.reads.pop_front().ok_or_else(|| {
1502 std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "no scripted response")
1503 })
1504 }
1505 }
1506
1507 fn response_frame(
1508 command: Command,
1509 status: u32,
1510 message_id: u64,
1511 session_id: u64,
1512 tree_id: u32,
1513 body: Vec<u8>,
1514 ) -> Vec<u8> {
1515 let mut header = Header::new(command, MessageId(message_id));
1516 header.status = status;
1517 header.credit_request_response = 1;
1518 header.session_id = smolder_proto::smb::smb2::SessionId(session_id);
1519 header.tree_id = TreeId(tree_id);
1520
1521 let mut packet = header.encode();
1522 packet.extend_from_slice(&body);
1523 SessionMessage::new(packet)
1524 .encode()
1525 .expect("response should frame")
1526 }
1527
1528 fn directory_entries_buffer(entries: &[(u32, FileAttributes, u64, &str)]) -> Vec<u8> {
1529 let mut buffer = Vec::new();
1530
1531 for (index, (file_index, attributes, size, name)) in entries.iter().enumerate() {
1532 let name_bytes = name
1533 .encode_utf16()
1534 .flat_map(|unit| unit.to_le_bytes())
1535 .collect::<Vec<_>>();
1536 let entry_len = (64 + name_bytes.len() + 7) & !7;
1537 let next_entry_offset = if index + 1 == entries.len() {
1538 0
1539 } else {
1540 entry_len as u32
1541 };
1542
1543 buffer.extend_from_slice(&next_entry_offset.to_le_bytes());
1544 buffer.extend_from_slice(&file_index.to_le_bytes());
1545 buffer.extend_from_slice(&1_u64.to_le_bytes());
1546 buffer.extend_from_slice(&2_u64.to_le_bytes());
1547 buffer.extend_from_slice(&3_u64.to_le_bytes());
1548 buffer.extend_from_slice(&4_u64.to_le_bytes());
1549 buffer.extend_from_slice(&size.to_le_bytes());
1550 buffer.extend_from_slice(&size.to_le_bytes());
1551 buffer.extend_from_slice(&attributes.bits().to_le_bytes());
1552 buffer.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
1553 buffer.extend_from_slice(&name_bytes);
1554 let padding = entry_len - 64 - name_bytes.len();
1555 buffer.resize(buffer.len() + padding, 0);
1556 }
1557
1558 buffer
1559 }
1560
1561 async fn build_share(reads: Vec<Vec<u8>>) -> Share<ScriptedTransport> {
1562 let negotiate_response = NegotiateResponse {
1563 security_mode: SigningMode::ENABLED,
1564 dialect_revision: Dialect::Smb302,
1565 negotiate_contexts: Vec::new(),
1566 server_guid: *b"server-guid-0001",
1567 capabilities: GlobalCapabilities::LARGE_MTU,
1568 max_transact_size: 65_536,
1569 max_read_size: 65_536,
1570 max_write_size: 65_536,
1571 system_time: 1,
1572 server_start_time: 1,
1573 security_buffer: Vec::new(),
1574 };
1575 let session_response = SessionSetupResponse {
1576 session_flags: SessionFlags::empty(),
1577 security_buffer: Vec::new(),
1578 };
1579 let tree_response = TreeConnectResponse {
1580 share_type: ShareType::Disk,
1581 share_flags: ShareFlags::empty(),
1582 capabilities: TreeCapabilities::empty(),
1583 maximal_access: 0x0012_019f,
1584 };
1585
1586 let mut scripted_reads = vec![
1587 response_frame(
1588 Command::Negotiate,
1589 NtStatus::SUCCESS.to_u32(),
1590 0,
1591 0,
1592 0,
1593 negotiate_response.encode(),
1594 ),
1595 response_frame(
1596 Command::SessionSetup,
1597 NtStatus::SUCCESS.to_u32(),
1598 1,
1599 11,
1600 0,
1601 session_response.encode(),
1602 ),
1603 response_frame(
1604 Command::TreeConnect,
1605 NtStatus::SUCCESS.to_u32(),
1606 2,
1607 11,
1608 7,
1609 tree_response.encode(),
1610 ),
1611 ];
1612 scripted_reads.extend(reads);
1613
1614 let transport = ScriptedTransport::new(scripted_reads);
1615 let negotiate_request = NegotiateRequest {
1616 security_mode: SigningMode::ENABLED,
1617 capabilities: GlobalCapabilities::LARGE_MTU,
1618 client_guid: *b"client-guid-0001",
1619 dialects: vec![Dialect::Smb210, Dialect::Smb302],
1620 negotiate_contexts: Vec::new(),
1621 };
1622 let connection = Connection::new(transport)
1623 .negotiate(&negotiate_request)
1624 .await
1625 .expect("negotiate should succeed");
1626 let mut auth = NtlmAuthenticator::new(NtlmCredentials::new("user", "pass"));
1627 let connection = connection
1628 .authenticate(&mut auth)
1629 .await
1630 .expect("authenticate should succeed");
1631 let connection = connection
1632 .tree_connect(&TreeConnectRequest::from_unc(r"\\server\share"))
1633 .await
1634 .expect("tree connect should succeed");
1635
1636 Share {
1637 server: "server".to_owned(),
1638 name: "share".to_owned(),
1639 connection,
1640 }
1641 }
1642
1643 #[test]
1644 fn builder_requires_credentials() {
1645 let error = Client::builder("server")
1646 .build()
1647 .expect_err("builder should reject missing credentials");
1648 assert!(matches!(
1649 error,
1650 crate::error::CoreError::InvalidInput(
1651 "client builder requires NTLM or Kerberos credentials"
1652 )
1653 ));
1654 }
1655
1656 #[test]
1657 fn ntlm_builder_populates_session_config() {
1658 let client = ClientBuilder::new("server")
1659 .with_port(1445)
1660 .with_signing_mode(SigningMode::REQUIRED)
1661 .with_capabilities(GlobalCapabilities::ENCRYPTION)
1662 .with_dialects(vec![Dialect::Smb302])
1663 .with_client_guid(*b"0123456789abcdef")
1664 .with_ntlm_credentials(NtlmCredentials::new("user", "pass"))
1665 .build()
1666 .expect("builder should produce a client");
1667
1668 let config = client.session_config();
1669 assert_eq!(client.server(), "server");
1670 assert_eq!(client.port(), 1445);
1671 assert_eq!(config.server(), "server");
1672 assert_eq!(config.port(), 1445);
1673 assert_eq!(config.signing_mode(), SigningMode::REQUIRED);
1674 assert_eq!(config.capabilities(), GlobalCapabilities::ENCRYPTION);
1675 assert_eq!(config.dialects(), &[Dialect::Smb302]);
1676 assert_eq!(config.client_guid(), b"0123456789abcdef");
1677 }
1678
1679 #[test]
1680 fn builder_can_override_transport_target() {
1681 let client = ClientBuilder::new("server")
1682 .with_transport_target(
1683 TransportTarget::quic("edge.lab.example")
1684 .with_connect_host("127.0.0.1")
1685 .with_tls_server_name("gateway.lab.example")
1686 .with_port(8443),
1687 )
1688 .with_ntlm_credentials(NtlmCredentials::new("user", "pass"))
1689 .build()
1690 .expect("builder should produce a client");
1691
1692 assert_eq!(client.server(), "edge.lab.example");
1693 assert_eq!(client.connect_host(), "127.0.0.1");
1694 assert_eq!(client.tls_server_name(), "gateway.lab.example");
1695 assert_eq!(client.port(), 8443);
1696 assert_eq!(client.transport_protocol(), TransportProtocol::Quic);
1697 assert_eq!(
1698 client.transport_target(),
1699 &TransportTarget::quic("edge.lab.example")
1700 .with_connect_host("127.0.0.1")
1701 .with_tls_server_name("gateway.lab.example")
1702 .with_port(8443)
1703 );
1704 }
1705
1706 #[test]
1707 fn builder_can_override_connect_host_without_replacing_target() {
1708 let client = ClientBuilder::new("files.lab.example")
1709 .with_connect_host("127.0.0.1")
1710 .with_ntlm_credentials(NtlmCredentials::new("user", "pass"))
1711 .build()
1712 .expect("builder should produce a client");
1713
1714 assert_eq!(client.server(), "files.lab.example");
1715 assert_eq!(client.connect_host(), "127.0.0.1");
1716 assert_eq!(client.transport_target().server(), "files.lab.example");
1717 assert_eq!(client.transport_protocol(), TransportProtocol::Tcp);
1718 }
1719
1720 #[cfg(feature = "kerberos-api")]
1721 #[test]
1722 fn kerberos_builder_produces_client() {
1723 let credentials = {
1724 #[cfg(feature = "kerberos-sspi")]
1725 {
1726 KerberosCredentials::new("user@LAB.EXAMPLE", "pass")
1727 }
1728 #[cfg(all(unix, feature = "kerberos-gssapi", not(feature = "kerberos-sspi")))]
1729 {
1730 KerberosCredentials::from_ticket_cache("user@LAB.EXAMPLE")
1731 }
1732 };
1733 let target = KerberosTarget::for_smb_host("server.lab.example");
1734 let client = ClientBuilder::new("server.lab.example")
1735 .with_kerberos_credentials(credentials, target)
1736 .build()
1737 .expect("builder should produce a client");
1738 assert_eq!(client.server(), "server.lab.example");
1739 assert_eq!(client.port(), 445);
1740 }
1741
1742 #[test]
1743 fn normalize_share_name_rejects_invalid_values() {
1744 assert_eq!(
1745 normalize_share_name(r"\\IPC$\\").expect("share should normalize"),
1746 "IPC$"
1747 );
1748 assert!(normalize_share_name("").is_err());
1749 assert!(normalize_share_name("share/path").is_err());
1750 }
1751
1752 #[test]
1753 fn open_options_map_to_expected_create_request() {
1754 let request = OpenOptions::new()
1755 .write(true)
1756 .create(true)
1757 .truncate(true)
1758 .to_create_request("docs\\report.txt")
1759 .expect("request should build");
1760 assert_eq!(request.create_disposition, CreateDisposition::OverwriteIf);
1761 }
1762
1763 #[test]
1764 fn normalize_share_path_rejects_invalid_values() {
1765 assert_eq!(
1766 normalize_share_path(r"/docs//nested\file.txt/").expect("path should normalize"),
1767 "docs\\nested\\file.txt"
1768 );
1769 assert!(normalize_share_path("").is_err());
1770 assert!(normalize_share_path("\0bad").is_err());
1771 }
1772
1773 #[test]
1774 fn normalize_pipe_name_rejects_invalid_values() {
1775 assert_eq!(
1776 normalize_pipe_name("srvsvc").expect("pipe should normalize"),
1777 "srvsvc"
1778 );
1779 assert_eq!(
1780 normalize_pipe_name(r"\\PIPE\\srvsvc").expect("pipe should normalize"),
1781 "srvsvc"
1782 );
1783 assert!(normalize_pipe_name("").is_err());
1784 assert!(normalize_pipe_name("\0bad").is_err());
1785 }
1786
1787 #[tokio::test]
1788 async fn share_read_queries_metadata_and_reads_contents() {
1789 let create_response = CreateResponse {
1790 oplock_level: OplockLevel::None,
1791 file_attributes: FileAttributes::ARCHIVE,
1792 allocation_size: 5,
1793 end_of_file: 5,
1794 file_id: FileId {
1795 persistent: 1,
1796 volatile: 2,
1797 },
1798 create_contexts: Vec::new(),
1799 };
1800 let basic = QueryInfoResponse {
1801 output_buffer: {
1802 let mut buffer = Vec::new();
1803 buffer.extend_from_slice(&1u64.to_le_bytes());
1804 buffer.extend_from_slice(&2u64.to_le_bytes());
1805 buffer.extend_from_slice(&3u64.to_le_bytes());
1806 buffer.extend_from_slice(&4u64.to_le_bytes());
1807 buffer.extend_from_slice(&FileAttributes::ARCHIVE.bits().to_le_bytes());
1808 buffer.extend_from_slice(&0u32.to_le_bytes());
1809 buffer
1810 },
1811 };
1812 let standard = QueryInfoResponse {
1813 output_buffer: {
1814 let mut buffer = Vec::new();
1815 buffer.extend_from_slice(&5u64.to_le_bytes());
1816 buffer.extend_from_slice(&5u64.to_le_bytes());
1817 buffer.extend_from_slice(&1u32.to_le_bytes());
1818 buffer.push(0);
1819 buffer.push(0);
1820 buffer.extend_from_slice(&0u16.to_le_bytes());
1821 buffer
1822 },
1823 };
1824 let read_response = smolder_proto::smb::smb2::ReadResponse {
1825 data_remaining: 0,
1826 flags: smolder_proto::smb::smb2::ReadResponseFlags::empty(),
1827 data: b"hello".to_vec(),
1828 };
1829
1830 let mut share = build_share(vec![
1831 response_frame(
1832 Command::Create,
1833 NtStatus::SUCCESS.to_u32(),
1834 3,
1835 11,
1836 7,
1837 create_response.encode(),
1838 ),
1839 response_frame(
1840 Command::QueryInfo,
1841 NtStatus::SUCCESS.to_u32(),
1842 4,
1843 11,
1844 7,
1845 basic.encode(),
1846 ),
1847 response_frame(
1848 Command::QueryInfo,
1849 NtStatus::SUCCESS.to_u32(),
1850 5,
1851 11,
1852 7,
1853 standard.encode(),
1854 ),
1855 response_frame(
1856 Command::Read,
1857 NtStatus::SUCCESS.to_u32(),
1858 6,
1859 11,
1860 7,
1861 read_response.encode(),
1862 ),
1863 response_frame(
1864 Command::Close,
1865 NtStatus::SUCCESS.to_u32(),
1866 7,
1867 11,
1868 7,
1869 CloseResponse {
1870 flags: 0,
1871 allocation_size: 5,
1872 end_of_file: 5,
1873 file_attributes: FileAttributes::ARCHIVE,
1874 }
1875 .encode(),
1876 ),
1877 ])
1878 .await;
1879
1880 let data = share.read("notes.txt").await.expect("read should succeed");
1881 assert_eq!(data, b"hello");
1882 }
1883
1884 #[tokio::test]
1885 async fn share_get_alias_reads_contents() {
1886 let create_response = CreateResponse {
1887 oplock_level: OplockLevel::None,
1888 file_attributes: FileAttributes::ARCHIVE,
1889 allocation_size: 5,
1890 end_of_file: 5,
1891 file_id: FileId {
1892 persistent: 1,
1893 volatile: 2,
1894 },
1895 create_contexts: Vec::new(),
1896 };
1897 let basic = QueryInfoResponse {
1898 output_buffer: {
1899 let mut buffer = Vec::new();
1900 buffer.extend_from_slice(&1u64.to_le_bytes());
1901 buffer.extend_from_slice(&2u64.to_le_bytes());
1902 buffer.extend_from_slice(&3u64.to_le_bytes());
1903 buffer.extend_from_slice(&4u64.to_le_bytes());
1904 buffer.extend_from_slice(&FileAttributes::ARCHIVE.bits().to_le_bytes());
1905 buffer.extend_from_slice(&0u32.to_le_bytes());
1906 buffer
1907 },
1908 };
1909 let standard = QueryInfoResponse {
1910 output_buffer: {
1911 let mut buffer = Vec::new();
1912 buffer.extend_from_slice(&5u64.to_le_bytes());
1913 buffer.extend_from_slice(&5u64.to_le_bytes());
1914 buffer.extend_from_slice(&1u32.to_le_bytes());
1915 buffer.push(0);
1916 buffer.push(0);
1917 buffer.extend_from_slice(&0u16.to_le_bytes());
1918 buffer
1919 },
1920 };
1921 let read_response = smolder_proto::smb::smb2::ReadResponse {
1922 data_remaining: 0,
1923 flags: smolder_proto::smb::smb2::ReadResponseFlags::empty(),
1924 data: b"hello".to_vec(),
1925 };
1926
1927 let mut share = build_share(vec![
1928 response_frame(
1929 Command::Create,
1930 NtStatus::SUCCESS.to_u32(),
1931 3,
1932 11,
1933 7,
1934 create_response.encode(),
1935 ),
1936 response_frame(
1937 Command::QueryInfo,
1938 NtStatus::SUCCESS.to_u32(),
1939 4,
1940 11,
1941 7,
1942 basic.encode(),
1943 ),
1944 response_frame(
1945 Command::QueryInfo,
1946 NtStatus::SUCCESS.to_u32(),
1947 5,
1948 11,
1949 7,
1950 standard.encode(),
1951 ),
1952 response_frame(
1953 Command::Read,
1954 NtStatus::SUCCESS.to_u32(),
1955 6,
1956 11,
1957 7,
1958 read_response.encode(),
1959 ),
1960 response_frame(
1961 Command::Close,
1962 NtStatus::SUCCESS.to_u32(),
1963 7,
1964 11,
1965 7,
1966 CloseResponse {
1967 flags: 0,
1968 allocation_size: 5,
1969 end_of_file: 5,
1970 file_attributes: FileAttributes::ARCHIVE,
1971 }
1972 .encode(),
1973 ),
1974 ])
1975 .await;
1976
1977 let data = share.get("notes.txt").await.expect("get should succeed");
1978 assert_eq!(data, b"hello");
1979 }
1980
1981 #[tokio::test]
1982 async fn share_put_alias_writes_contents() {
1983 let create_response = CreateResponse {
1984 oplock_level: OplockLevel::None,
1985 file_attributes: FileAttributes::ARCHIVE,
1986 allocation_size: 5,
1987 end_of_file: 5,
1988 file_id: FileId {
1989 persistent: 1,
1990 volatile: 2,
1991 },
1992 create_contexts: Vec::new(),
1993 };
1994
1995 let mut share = build_share(vec![
1996 response_frame(
1997 Command::Create,
1998 NtStatus::SUCCESS.to_u32(),
1999 3,
2000 11,
2001 7,
2002 create_response.encode(),
2003 ),
2004 response_frame(
2005 Command::Write,
2006 NtStatus::SUCCESS.to_u32(),
2007 4,
2008 11,
2009 7,
2010 WriteResponse { count: 5 }.encode(),
2011 ),
2012 response_frame(
2013 Command::Flush,
2014 NtStatus::SUCCESS.to_u32(),
2015 5,
2016 11,
2017 7,
2018 FlushResponse.encode(),
2019 ),
2020 response_frame(
2021 Command::Close,
2022 NtStatus::SUCCESS.to_u32(),
2023 6,
2024 11,
2025 7,
2026 CloseResponse {
2027 flags: 0,
2028 allocation_size: 5,
2029 end_of_file: 5,
2030 file_attributes: FileAttributes::ARCHIVE,
2031 }
2032 .encode(),
2033 ),
2034 ])
2035 .await;
2036
2037 share
2038 .put("notes.txt", b"hello")
2039 .await
2040 .expect("put should succeed");
2041 }
2042
2043 #[tokio::test]
2044 async fn share_stat_decodes_basic_and_standard_info() {
2045 let create_response = CreateResponse {
2046 oplock_level: OplockLevel::None,
2047 file_attributes: FileAttributes::ARCHIVE,
2048 allocation_size: 7,
2049 end_of_file: 5,
2050 file_id: FileId {
2051 persistent: 1,
2052 volatile: 2,
2053 },
2054 create_contexts: Vec::new(),
2055 };
2056 let basic = QueryInfoResponse {
2057 output_buffer: {
2058 let mut buffer = Vec::new();
2059 buffer.extend_from_slice(&1u64.to_le_bytes());
2060 buffer.extend_from_slice(&2u64.to_le_bytes());
2061 buffer.extend_from_slice(&3u64.to_le_bytes());
2062 buffer.extend_from_slice(&4u64.to_le_bytes());
2063 buffer.extend_from_slice(&FileAttributes::ARCHIVE.bits().to_le_bytes());
2064 buffer.extend_from_slice(&0u32.to_le_bytes());
2065 buffer
2066 },
2067 };
2068 let standard = QueryInfoResponse {
2069 output_buffer: {
2070 let mut buffer = Vec::new();
2071 buffer.extend_from_slice(&7u64.to_le_bytes());
2072 buffer.extend_from_slice(&5u64.to_le_bytes());
2073 buffer.extend_from_slice(&1u32.to_le_bytes());
2074 buffer.push(1);
2075 buffer.push(0);
2076 buffer.extend_from_slice(&0u16.to_le_bytes());
2077 buffer
2078 },
2079 };
2080
2081 let mut share = build_share(vec![
2082 response_frame(
2083 Command::Create,
2084 NtStatus::SUCCESS.to_u32(),
2085 3,
2086 11,
2087 7,
2088 create_response.encode(),
2089 ),
2090 response_frame(
2091 Command::QueryInfo,
2092 NtStatus::SUCCESS.to_u32(),
2093 4,
2094 11,
2095 7,
2096 basic.encode(),
2097 ),
2098 response_frame(
2099 Command::QueryInfo,
2100 NtStatus::SUCCESS.to_u32(),
2101 5,
2102 11,
2103 7,
2104 standard.encode(),
2105 ),
2106 response_frame(
2107 Command::Close,
2108 NtStatus::SUCCESS.to_u32(),
2109 6,
2110 11,
2111 7,
2112 CloseResponse {
2113 flags: 0,
2114 allocation_size: 7,
2115 end_of_file: 5,
2116 file_attributes: FileAttributes::ARCHIVE,
2117 }
2118 .encode(),
2119 ),
2120 ])
2121 .await;
2122
2123 let metadata: FileMetadata = share.stat("notes.txt").await.expect("stat should succeed");
2124 assert_eq!(metadata.size, 5);
2125 assert_eq!(metadata.allocation_size, 7);
2126 assert!(metadata.delete_pending);
2127 assert!(metadata.is_file());
2128 }
2129
2130 #[tokio::test]
2131 async fn share_list_decodes_directory_entries_and_filters_dot_entries() {
2132 let create_response = CreateResponse {
2133 oplock_level: OplockLevel::None,
2134 file_attributes: FileAttributes::DIRECTORY,
2135 allocation_size: 0,
2136 end_of_file: 0,
2137 file_id: FileId {
2138 persistent: 10,
2139 volatile: 20,
2140 },
2141 create_contexts: Vec::new(),
2142 };
2143 let first_page = QueryDirectoryResponse {
2144 output_buffer: directory_entries_buffer(&[
2145 (1, FileAttributes::DIRECTORY, 0, "."),
2146 (2, FileAttributes::ARCHIVE, 5, "notes.txt"),
2147 ]),
2148 };
2149 let second_page = QueryDirectoryResponse {
2150 output_buffer: directory_entries_buffer(&[
2151 (3, FileAttributes::DIRECTORY, 0, ".."),
2152 (4, FileAttributes::DIRECTORY, 0, "nested"),
2153 ]),
2154 };
2155
2156 let mut share = build_share(vec![
2157 response_frame(
2158 Command::Create,
2159 NtStatus::SUCCESS.to_u32(),
2160 3,
2161 11,
2162 7,
2163 create_response.encode(),
2164 ),
2165 response_frame(
2166 Command::QueryDirectory,
2167 NtStatus::SUCCESS.to_u32(),
2168 4,
2169 11,
2170 7,
2171 first_page.encode(),
2172 ),
2173 response_frame(
2174 Command::QueryDirectory,
2175 NtStatus::SUCCESS.to_u32(),
2176 5,
2177 11,
2178 7,
2179 second_page.encode(),
2180 ),
2181 response_frame(
2182 Command::QueryDirectory,
2183 NtStatus::NO_MORE_FILES.to_u32(),
2184 6,
2185 11,
2186 7,
2187 Vec::new(),
2188 ),
2189 response_frame(
2190 Command::Close,
2191 NtStatus::SUCCESS.to_u32(),
2192 7,
2193 11,
2194 7,
2195 CloseResponse {
2196 flags: 0,
2197 allocation_size: 0,
2198 end_of_file: 0,
2199 file_attributes: FileAttributes::DIRECTORY,
2200 }
2201 .encode(),
2202 ),
2203 ])
2204 .await;
2205
2206 let entries = share.list("\\").await.expect("list should succeed");
2207 assert_eq!(entries.len(), 2);
2208 assert_eq!(entries[0].name, "notes.txt");
2209 assert!(entries[0].is_file());
2210 assert_eq!(entries[0].metadata.size, 5);
2211 assert_eq!(entries[1].name, "nested");
2212 assert!(entries[1].is_directory());
2213 }
2214
2215 #[tokio::test]
2216 async fn share_create_dir_creates_and_closes_directory_handle() {
2217 let create_response = CreateResponse {
2218 oplock_level: OplockLevel::None,
2219 file_attributes: FileAttributes::DIRECTORY,
2220 allocation_size: 0,
2221 end_of_file: 0,
2222 file_id: FileId {
2223 persistent: 30,
2224 volatile: 40,
2225 },
2226 create_contexts: Vec::new(),
2227 };
2228
2229 let mut share = build_share(vec![
2230 response_frame(
2231 Command::Create,
2232 NtStatus::SUCCESS.to_u32(),
2233 3,
2234 11,
2235 7,
2236 create_response.encode(),
2237 ),
2238 response_frame(
2239 Command::Close,
2240 NtStatus::SUCCESS.to_u32(),
2241 4,
2242 11,
2243 7,
2244 CloseResponse {
2245 flags: 0,
2246 allocation_size: 0,
2247 end_of_file: 0,
2248 file_attributes: FileAttributes::DIRECTORY,
2249 }
2250 .encode(),
2251 ),
2252 ])
2253 .await;
2254
2255 share
2256 .create_dir("nested")
2257 .await
2258 .expect("create_dir should succeed");
2259 }
2260
2261 #[tokio::test]
2262 async fn share_rename_sets_rename_information_and_closes_handle() {
2263 let create_response = CreateResponse {
2264 oplock_level: OplockLevel::None,
2265 file_attributes: FileAttributes::ARCHIVE,
2266 allocation_size: 5,
2267 end_of_file: 5,
2268 file_id: FileId {
2269 persistent: 50,
2270 volatile: 60,
2271 },
2272 create_contexts: Vec::new(),
2273 };
2274
2275 let mut share = build_share(vec![
2276 response_frame(
2277 Command::Create,
2278 NtStatus::SUCCESS.to_u32(),
2279 3,
2280 11,
2281 7,
2282 create_response.encode(),
2283 ),
2284 response_frame(
2285 Command::SetInfo,
2286 NtStatus::SUCCESS.to_u32(),
2287 4,
2288 11,
2289 7,
2290 smolder_proto::smb::smb2::SetInfoResponse.encode(),
2291 ),
2292 response_frame(
2293 Command::Close,
2294 NtStatus::SUCCESS.to_u32(),
2295 5,
2296 11,
2297 7,
2298 CloseResponse {
2299 flags: 0,
2300 allocation_size: 5,
2301 end_of_file: 5,
2302 file_attributes: FileAttributes::ARCHIVE,
2303 }
2304 .encode(),
2305 ),
2306 ])
2307 .await;
2308
2309 share
2310 .rename("notes.txt", "archive\\notes.txt")
2311 .await
2312 .expect("rename should succeed");
2313 }
2314}