Skip to main content

rustsmb/io/
request.rs

1//! The command table (single source of truth) and the commands migrated onto the
2//! typestate pipeline so far. Phase 0 wires only ECHO end to end to prove the
3//! machinery; later phases add one line per command here.
4
5use metrics::{counter, gauge};
6use smb_server_proto::types::Status;
7use smb_server_proto_smb2::commands::{
8    self as c, ChangeNotifyReq, CloseReq, CreateReq, FlushReq, IoctlReq, LockReq, QueryDirReq,
9    QueryInfoReq, ReadReq, SetInfoReq, WriteReq,
10};
11use smb_server_proto_smb2::session_setup::cmd;
12
13use super::context::{Command, IoContext, Outcome, Resources};
14use super::origin::{Bare, Solicited};
15use super::state::Accepted;
16use super::wire::Decode;
17
18/// SMB2 ECHO request ([MS-SMB2] §2.2.28): no meaningful body.
19#[derive(Debug)]
20pub struct EchoReq;
21impl Decode for EchoReq {
22    const COMMAND: u16 = cmd::ECHO;
23    fn decode(_frame: &[u8]) -> Result<Self, Status> {
24        Ok(EchoReq)
25    }
26}
27
28/// SMB2 SESSION_SETUP request ([MS-SMB2] §2.2.5): the NTLM/SPNEGO blob is parsed
29/// from the frame by the authenticator, so the marker itself decodes nothing.
30#[derive(Debug)]
31pub struct SessionSetupReq;
32impl Decode for SessionSetupReq {
33    const COMMAND: u16 = cmd::SESSION_SETUP;
34    fn decode(_frame: &[u8]) -> Result<Self, Status> {
35        Ok(SessionSetupReq)
36    }
37}
38
39/// SMB2 NEGOTIATE request ([MS-SMB2] §2.2.3): dialects and negotiate contexts are
40/// parsed from the frame by the negotiator, so the marker itself decodes nothing.
41#[derive(Debug)]
42pub struct NegotiateReq;
43impl Decode for NegotiateReq {
44    const COMMAND: u16 = cmd::NEGOTIATE;
45    fn decode(_frame: &[u8]) -> Result<Self, Status> {
46        Ok(NegotiateReq)
47    }
48}
49
50/// SMB2 TREE_DISCONNECT request ([MS-SMB2] §2.2.11): no fields we act on.
51#[derive(Debug)]
52pub struct TreeDisconnectReq;
53impl Decode for TreeDisconnectReq {
54    const COMMAND: u16 = cmd::TREE_DISCONNECT;
55    fn decode(_frame: &[u8]) -> Result<Self, Status> {
56        Ok(TreeDisconnectReq)
57    }
58}
59
60/// SMB2 TREE_CONNECT request ([MS-SMB2] §2.2.9): the share path is parsed from
61/// the frame by the handler, so the marker itself decodes nothing.
62#[derive(Debug)]
63pub struct TreeConnectReq;
64impl Decode for TreeConnectReq {
65    const COMMAND: u16 = cmd::TREE_CONNECT;
66    fn decode(_frame: &[u8]) -> Result<Self, Status> {
67        Ok(TreeConnectReq)
68    }
69}
70
71/// SMB2 LOGOFF request ([MS-SMB2] §2.2.7): no fields we act on.
72#[derive(Debug)]
73pub struct LogoffReq;
74impl Decode for LogoffReq {
75    const COMMAND: u16 = cmd::LOGOFF;
76    fn decode(_frame: &[u8]) -> Result<Self, Status> {
77        Ok(LogoffReq)
78    }
79}
80
81/// SMB2 CANCEL request ([MS-SMB2] §2.2.30): correlated by MessageId/AsyncId from
82/// the header, so the body carries nothing we decode.
83#[derive(Debug)]
84pub struct CancelReq;
85impl Decode for CancelReq {
86    const COMMAND: u16 = cmd::CANCEL;
87    fn decode(_frame: &[u8]) -> Result<Self, Status> {
88        Ok(CancelReq)
89    }
90}
91
92/// SMB2 OPLOCK_BREAK acknowledgement ([MS-SMB2] §2.2.24): command 18 carries an
93/// oplock (StructureSize 24) or lease (StructureSize 36) ack, dispatched on the
94/// size in the handler, so the marker itself decodes nothing.
95#[derive(Debug)]
96pub struct OplockBreakReq;
97impl Decode for OplockBreakReq {
98    const COMMAND: u16 = cmd::OPLOCK_BREAK;
99    fn decode(_frame: &[u8]) -> Result<Self, Status> {
100        Ok(OplockBreakReq)
101    }
102}
103
104/// ECHO handler: a keep-alive, always answered SUCCESS ([MS-SMB2] §3.3.5.17).
105pub struct EchoCmd;
106impl Command for EchoCmd {
107    type Request = EchoReq;
108    async fn serve(ctx: IoContext<Accepted, Bare>, _req: EchoReq, _res: &mut Resources<'_>) -> Outcome {
109        Outcome::Final(ctx.respond(Status::SUCCESS, c::build_echo_resp()))
110    }
111}
112
113/// FLUSH handler ([MS-SMB2] §3.3.5.11): flush the open's backing file.
114pub struct FlushCmd;
115impl Command for FlushCmd {
116    type Request = FlushReq;
117    async fn serve(ctx: IoContext<Accepted, Bare>, req: FlushReq, res: &mut Resources<'_>) -> Outcome {
118        let tid = ctx.reply.tree_id;
119        let Some(vfs) = crate::smb2::share_vfs(res.server, res.conn, tid) else {
120            return Outcome::Final(ctx.respond(Status::INVALID_HANDLE, Vec::new()));
121        };
122        if let Some(mut open) = res.conn.handle_take(&req.file_id.0) {
123            let r = vfs.flush(&mut open).await;
124            res.conn.handle_insert(req.file_id.0, open);
125            if let Err(e) = r {
126                return Outcome::Final(ctx.respond(crate::smb2::vfs_err(e), Vec::new()));
127            }
128        }
129        Outcome::Final(ctx.respond(Status::SUCCESS, c::build_flush_resp()))
130    }
131}
132
133/// TREE_DISCONNECT handler ([MS-SMB2] §3.3.5.8): idempotent tree teardown.
134pub struct TreeDisconnectCmd;
135impl Command for TreeDisconnectCmd {
136    type Request = TreeDisconnectReq;
137    async fn serve(ctx: IoContext<Accepted, Bare>, _req: TreeDisconnectReq, res: &mut Resources<'_>) -> Outcome {
138        res.conn.tree_remove(ctx.reply.tree_id);
139        Outcome::Final(ctx.respond(Status::SUCCESS, c::build_tree_disconnect_resp()))
140    }
141}
142
143/// LOGOFF handler ([MS-SMB2] §3.3.5.6): release the session's resources but keep
144/// the session id and keys so this reply still frames/seals correctly.
145pub struct LogoffCmd;
146impl Command for LogoffCmd {
147    type Request = LogoffReq;
148    async fn serve(ctx: IoContext<Accepted, Bare>, _req: LogoffReq, res: &mut Resources<'_>) -> Outcome {
149        let sid = res.conn.session_id;
150        crate::smb2::close_all_handles(res.conn);
151        res.server.locks.release_session(sid);
152        res.server.share_modes.close_session(sid);
153        res.server.oplocks.release_session(sid);
154        res.server.leases.release_session(sid);
155        res.server.app_instances.close_session(sid);
156        res.server.sessions.remove(sid);
157        crate::session_scope::remove(sid);
158        res.conn.searches.clear();
159        if res.conn.authenticated {
160            gauge!("smb_sessions_active").decrement(1.0);
161        }
162        res.conn.authenticated = false;
163        counter!("smb_logoffs_total").increment(1);
164        Outcome::Final(ctx.respond(Status::SUCCESS, c::build_logoff_resp()))
165    }
166}
167
168/// READ handler ([MS-SMB2] §3.3.5.12): a pipe read on IPC$, else a file read.
169pub struct ReadCmd;
170impl Command for ReadCmd {
171    type Request = ReadReq;
172    async fn serve(ctx: IoContext<Accepted, Bare>, _req: ReadReq, res: &mut Resources<'_>) -> Outcome {
173        if let Some(body) = crate::smb2::pipe_read(res.conn, res.frame) {
174            return Outcome::Final(ctx.respond(Status::SUCCESS, body));
175        }
176        let Some(vfs) = crate::smb2::share_vfs(res.server, res.conn, ctx.reply.tree_id) else {
177            return Outcome::Final(ctx.respond(Status::INVALID_HANDLE, Vec::new()));
178        };
179        match crate::smb2::read(res.conn, vfs, res.server, res.frame).await {
180            Ok(body) => Outcome::Final(ctx.respond(Status::SUCCESS, body)),
181            Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
182        }
183    }
184}
185
186/// WRITE handler ([MS-SMB2] §3.3.5.13): a pipe write on IPC$, else a file write.
187pub struct WriteCmd;
188impl Command for WriteCmd {
189    type Request = WriteReq;
190    async fn serve(ctx: IoContext<Accepted, Bare>, _req: WriteReq, res: &mut Resources<'_>) -> Outcome {
191        if let Some(body) = crate::smb2::pipe_write(res.server, res.conn, res.frame) {
192            return Outcome::Final(ctx.respond(Status::SUCCESS, body));
193        }
194        let Some(vfs) = crate::smb2::share_vfs(res.server, res.conn, ctx.reply.tree_id) else {
195            return Outcome::Final(ctx.respond(Status::INVALID_HANDLE, Vec::new()));
196        };
197        match crate::smb2::write(res.conn, vfs, res.server, res.frame).await {
198            Ok(body) => Outcome::Final(ctx.respond(Status::SUCCESS, body)),
199            Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
200        }
201    }
202}
203
204/// CLOSE handler ([MS-SMB2] §3.3.5.10): closes a pipe handle, else a file handle
205/// and tears down that open's notifies, byte-range locks, share mode, oplock,
206/// lease, and durable-handle state.
207pub struct CloseCmd;
208impl Command for CloseCmd {
209    type Request = CloseReq;
210    async fn serve(ctx: IoContext<Accepted, Bare>, _req: CloseReq, res: &mut Resources<'_>) -> Outcome {
211        if crate::smb2::pipe_close(res.conn, res.frame) {
212            let body = c::build_close_resp([0u64; 4], 0, 0, 0);
213            return Outcome::Final(ctx.respond(Status::SUCCESS, body));
214        }
215        let Some(vfs) = crate::smb2::share_vfs(res.server, res.conn, ctx.reply.tree_id) else {
216            return Outcome::Final(ctx.respond(Status::INVALID_HANDLE, Vec::new()));
217        };
218        let close_path = CloseReq::parse(res.frame)
219            .and_then(|r| res.conn.with_handle(&r.file_id.0, |h| (r.file_id.0, h.path.clone(), h.delete_on_close)));
220        match crate::smb2::close(res.conn, vfs, res.frame).await {
221            Ok(body) => {
222                if let Some((fid, path, deleted)) = close_path {
223                    complete_pending_notifies(res.conn, fid);
224                    res.server.locks.release_owner((res.conn.session_id, fid));
225                    res.server.share_modes.close(&path, (res.conn.session_id, fid));
226                    res.server.oplocks.release(&path, (res.conn.session_id, fid));
227                    res.server.leases.release(&path, (res.conn.session_id, fid));
228                    res.server.app_instances.close((res.conn.session_id, fid));
229                    res.conn.durable.remove(&fid);
230                    let _ = res.server.durables.remove(&fid).await;
231                    // Deleting a child revokes READ caching on a directory lease
232                    // held on the parent ([MS-SMB2] §3.3.1.4).
233                    if deleted {
234                        crate::smb2::break_dir_lease(
235                            res.server,
236                            res.conn,
237                            crate::smb2::parent_dir(&path),
238                            fid,
239                            c::lease::RH,
240                        );
241                    }
242                }
243                Outcome::Final(ctx.respond(Status::SUCCESS, body))
244            }
245            Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
246        }
247    }
248}
249
250/// Complete any pending CHANGE_NOTIFY on this handle with STATUS_NOTIFY_CLEANUP
251/// ([MS-SMB2] §3.3.5.10) when its open is closed.
252fn complete_pending_notifies(conn: &mut crate::smb2::Smb2Conn, fid: [u8; 16]) {
253    let Some(ids) = conn.async_by_file.remove(&fid) else {
254        return;
255    };
256    for aid in ids {
257        conn.async_msgids.retain(|_, v| *v != aid);
258        if let Some(tx) = conn.async_cancels.remove(&aid) {
259            let _ = tx.send(Status::NOTIFY_CLEANUP);
260        }
261    }
262}
263
264/// QUERY_DIRECTORY handler ([MS-SMB2] §3.3.5.18): enumerates a directory handle.
265pub struct QueryDirCmd;
266impl Command for QueryDirCmd {
267    type Request = QueryDirReq;
268    async fn serve(ctx: IoContext<Accepted, Bare>, _req: QueryDirReq, res: &mut Resources<'_>) -> Outcome {
269        let Some(vfs) = crate::smb2::share_vfs(res.server, res.conn, ctx.reply.tree_id) else {
270            return Outcome::Final(ctx.respond(Status::INVALID_HANDLE, Vec::new()));
271        };
272        match crate::smb2::query_directory(res.conn, vfs, res.frame).await {
273            Ok(Some(buffer)) => Outcome::Final(ctx.respond(Status::SUCCESS, c::build_info_resp(&buffer))),
274            Ok(None) => Outcome::Final(ctx.respond(Status::NO_MORE_FILES, Vec::new())),
275            Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
276        }
277    }
278}
279
280/// QUERY_INFO handler ([MS-SMB2] §3.3.5.20): file/filesystem/security info.
281pub struct QueryInfoCmd;
282impl Command for QueryInfoCmd {
283    type Request = QueryInfoReq;
284    async fn serve(ctx: IoContext<Accepted, Bare>, _req: QueryInfoReq, res: &mut Resources<'_>) -> Outcome {
285        let Some(vfs) = crate::smb2::share_vfs(res.server, res.conn, ctx.reply.tree_id) else {
286            return Outcome::Final(ctx.respond(Status::INVALID_HANDLE, Vec::new()));
287        };
288        match crate::smb2::query_info(res.conn, vfs, res.frame).await {
289            Ok(Some(buffer)) => Outcome::Final(ctx.respond(Status::SUCCESS, c::build_info_resp(&buffer))),
290            Ok(None) => Outcome::Final(ctx.respond(Status::NOT_IMPLEMENTED, Vec::new())),
291            Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
292        }
293    }
294}
295
296/// SET_INFO handler ([MS-SMB2] §3.3.5.21): applies file/filesystem info changes.
297pub struct SetInfoCmd;
298impl Command for SetInfoCmd {
299    type Request = SetInfoReq;
300    async fn serve(ctx: IoContext<Accepted, Bare>, _req: SetInfoReq, res: &mut Resources<'_>) -> Outcome {
301        let Some(vfs) = crate::smb2::share_vfs(res.server, res.conn, ctx.reply.tree_id) else {
302            return Outcome::Final(ctx.respond(Status::INVALID_HANDLE, Vec::new()));
303        };
304        // A rename of a child changes directory contents: capture the pre-rename
305        // path so the parent directory lease's READ caching can be revoked
306        // ([MS-SMB2] §3.3.1.4).
307        let rename = SetInfoReq::parse(res.frame)
308            .filter(|r| r.info_type == c::info_type::FILE && r.class == smb_server_proto_smb2::info::file_class::RENAME)
309            .and_then(|r| res.conn.with_handle(&r.file_id.0, |h| (r.file_id.0, h.path.clone(), h.is_dir)));
310        // Renaming a directory with open handles anywhere in its subtree first
311        // revokes HANDLE caching on the affected directory leases, then fails
312        // with STATUS_ACCESS_DENIED because the subtree is in use ([MS-SMB2]
313        // §3.3.1.4, [MS-FSA] §2.1.5.14.2).
314        if let Some((fid, old_path, true)) = &rename {
315            let owner = (res.conn.session_id, *fid);
316            crate::smb2::break_subtree_lease_wait(
317                res.server,
318                res.conn,
319                old_path,
320                *fid,
321                c::lease::HANDLE_CACHING,
322            )
323            .await;
324            if res.server.share_modes.has_open_under(old_path, owner) {
325                return Outcome::Final(ctx.respond(Status::ACCESS_DENIED, Vec::new()));
326            }
327        }
328        let rename_old = rename.map(|(fid, path, _)| (fid, path));
329        match crate::smb2::set_info(res.conn, vfs, res.frame).await {
330            Ok(()) => {
331                if let Some((fid, old_path)) = rename_old {
332                    crate::smb2::break_dir_lease(
333                        res.server,
334                        res.conn,
335                        crate::smb2::parent_dir(&old_path),
336                        fid,
337                        c::lease::RH,
338                    );
339                    if let Some(new_path) = res.conn.with_handle(&fid, |h| h.path.clone()) {
340                        crate::smb2::break_dir_lease(
341                            res.server,
342                            res.conn,
343                            crate::smb2::parent_dir(&new_path),
344                            fid,
345                            c::lease::RH,
346                        );
347                    }
348                }
349                Outcome::Final(ctx.respond(Status::SUCCESS, c::build_set_info_resp()))
350            }
351            Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
352        }
353    }
354}
355
356/// CREATE handler ([MS-SMB2] §3.3.5.9): opens a named pipe on IPC$, else a file
357/// or directory. A symlink in the path yields a Symbolic Link Error Response
358/// ([MS-SMB2] §2.2.2.2.1); the heavy share-mode/oplock/lease/durable logic lives
359/// in the existing create().
360pub struct CreateCmd;
361impl Command for CreateCmd {
362    type Request = CreateReq;
363    async fn serve(ctx: IoContext<Accepted, Bare>, _req: CreateReq, res: &mut Resources<'_>) -> Outcome {
364        if crate::smb2::share_is_ipc(res.server, res.conn, ctx.reply.tree_id) {
365            return match crate::smb2::pipe_create(res.conn, res.frame) {
366                Ok(body) => Outcome::Final(ctx.respond(Status::SUCCESS, body)),
367                Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
368            };
369        }
370        let Some(vfs) = crate::smb2::share_vfs(res.server, res.conn, ctx.reply.tree_id) else {
371            return Outcome::Final(ctx.respond(Status::INVALID_HANDLE, Vec::new()));
372        };
373        let signed = smb_server_proto_smb2::Header2::parse(res.frame)
374            .map(|h| h.is_signed())
375            .unwrap_or(false);
376        let is_ca = crate::smb2::share_is_ca(res.server, res.conn, ctx.reply.tree_id);
377        match crate::smb2::create(res.conn, vfs, res.server, signed, is_ca, res.frame).await {
378            Ok(body) => Outcome::Final(ctx.respond(Status::SUCCESS, body)),
379            Err(status) if status == Status::STOPPED_ON_SYMLINK => {
380                let body = res.conn.symlink_error.take().unwrap_or_else(crate::smb2::error_resp);
381                Outcome::Final(ctx.respond(status, body))
382            }
383            Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
384        }
385    }
386}
387
388/// IOCTL handler ([MS-SMB2] §3.3.5.15): FSCTL dispatch (validate-negotiate,
389/// resiliency, pipe transact, server-side copy, zero-data, interface info, ...).
390/// A downgrade-detected VALIDATE_NEGOTIATE_INFO terminates the connection and
391/// yields no reply.
392pub struct IoctlCmd;
393impl Command for IoctlCmd {
394    type Request = IoctlReq;
395    async fn serve(ctx: IoContext<Accepted, Bare>, _req: IoctlReq, res: &mut Resources<'_>) -> Outcome {
396        match crate::smb2::ioctl(res.conn, res.server, ctx.reply.tree_id, res.frame).await {
397            crate::smb2::IoctlReply::Reply(status, body) => Outcome::Final(ctx.respond(status, body)),
398            crate::smb2::IoctlReply::Silent => Outcome::Silent,
399        }
400    }
401}
402
403/// LOCK handler ([MS-SMB2] §3.3.5.14): a byte-range lock/unlock. A conflicting
404/// blocking lock defers as [`Outcome::Interim`]; the parked waiter completes it
405/// on the outbound queue when the range frees.
406pub struct LockCmd;
407impl Command for LockCmd {
408    type Request = LockReq;
409    async fn serve(ctx: IoContext<Accepted, Bare>, _req: LockReq, res: &mut Resources<'_>) -> Outcome {
410        let Some(hdr) = smb_server_proto_smb2::Header2::parse(res.frame) else {
411            return Outcome::Final(ctx.respond(Status::INVALID_PARAMETER, Vec::new()));
412        };
413        match crate::smb2::begin_lock(res.conn, res.server, &hdr, res.frame) {
414            crate::smb2::AsyncStart::Reply(status, body) => Outcome::Final(ctx.respond(status, body)),
415            crate::smb2::AsyncStart::Pending(async_id) => {
416                let parked = ctx.defer(async_id);
417                let interim = parked.interim(Status::PENDING, crate::smb2::error_resp());
418                Outcome::Interim { parked, interim }
419            }
420        }
421    }
422}
423
424/// CHANGE_NOTIFY handler ([MS-SMB2] §3.3.5.19): registers a directory watch and
425/// defers as [`Outcome::Interim`]; the background watcher completes it when the
426/// first change (or cancellation) arrives.
427pub struct ChangeNotifyCmd;
428impl Command for ChangeNotifyCmd {
429    type Request = ChangeNotifyReq;
430    async fn serve(ctx: IoContext<Accepted, Bare>, _req: ChangeNotifyReq, res: &mut Resources<'_>) -> Outcome {
431        let Some(hdr) = smb_server_proto_smb2::Header2::parse(res.frame) else {
432            return Outcome::Final(ctx.respond(Status::INVALID_PARAMETER, Vec::new()));
433        };
434        match crate::smb2::begin_change_notify(res.conn, &hdr, res.frame) {
435            crate::smb2::AsyncStart::Reply(status, body) => Outcome::Final(ctx.respond(status, body)),
436            crate::smb2::AsyncStart::Pending(async_id) => {
437                let parked = ctx.defer(async_id);
438                let interim = parked.interim(Status::PENDING, crate::smb2::error_resp());
439                Outcome::Interim { parked, interim }
440            }
441        }
442    }
443}
444
445/// CANCEL handler ([MS-SMB2] §3.3.5.16): signals the targeted pending op to
446/// complete with STATUS_CANCELLED. CANCEL itself yields no reply.
447pub struct CancelCmd;
448impl Command for CancelCmd {
449    type Request = CancelReq;
450    async fn serve(_ctx: IoContext<Accepted, Bare>, _req: CancelReq, res: &mut Resources<'_>) -> Outcome {
451        if let Some(hdr) = smb_server_proto_smb2::Header2::parse(res.frame) {
452            crate::smb2::cancel(res.conn, &hdr, res.frame);
453        }
454        Outcome::Silent
455    }
456}
457
458/// OPLOCK_BREAK handler ([MS-SMB2] §3.3.5.22): acknowledges an oplock or lease
459/// break. Command 18 dispatches on StructureSize — 36 is a lease-break ack
460/// (record the settled state), otherwise an oplock-break ack (echo the level).
461pub struct OplockBreakCmd;
462impl Command for OplockBreakCmd {
463    type Request = OplockBreakReq;
464    async fn serve(ctx: IoContext<Accepted, Bare>, _req: OplockBreakReq, res: &mut Resources<'_>) -> Outcome {
465        let frame = res.frame;
466        let hdr_len = smb_server_proto_smb2::consts::hdr::LEN;
467        let structure_size = u16::from_le_bytes([
468            *frame.get(hdr_len).unwrap_or(&0),
469            *frame.get(hdr_len + 1).unwrap_or(&0),
470        ]);
471        if structure_size == c::LeaseBreakAck::STRUCTURE_SIZE {
472            return match c::LeaseBreakAck::parse(frame) {
473                Some(ack) => {
474                    use crate::state::LeaseAckError;
475                    match res.server.leases.acknowledge(res.conn.client_guid, ack.key, ack.state) {
476                        Ok(state) => {
477                            res.server.leases.signal_ack(ack.key);
478                            Outcome::Final(
479                                ctx.respond(Status::SUCCESS, c::build_lease_break_resp(ack.key, state)),
480                            )
481                        }
482                        Err(LeaseAckError::NotFound) => {
483                            Outcome::Final(ctx.respond(Status::OBJECT_NAME_NOT_FOUND, Vec::new()))
484                        }
485                        Err(LeaseAckError::NotBreaking) => {
486                            Outcome::Final(ctx.respond(Status::UNSUCCESSFUL, Vec::new()))
487                        }
488                        Err(LeaseAckError::StateNotAccepted) => {
489                            Outcome::Final(ctx.respond(Status::REQUEST_NOT_ACCEPTED, Vec::new()))
490                        }
491                    }
492                }
493                None => Outcome::Final(ctx.respond(Status::INVALID_PARAMETER, Vec::new())),
494            };
495        }
496        match c::OplockBreakAck::parse(frame) {
497            // The holder acknowledges a break; we already downgraded on our
498            // side, so echo the settled level ([MS-SMB2] §3.3.5.22.2).
499            Some(ack) => Outcome::Final(ctx.respond(Status::SUCCESS, c::build_oplock_break_resp(ack.file_id, ack.level))),
500            None => Outcome::Final(ctx.respond(Status::INVALID_PARAMETER, Vec::new())),
501        }
502    }
503}
504
505/// NEGOTIATE handler ([MS-SMB2] §3.3.5.3-4): negotiates the dialect and
506/// cipher/compression. A repeat NEGOTIATE on an already-negotiated connection
507/// terminates it ([MS-SMB2] §3.3.5.4), yielding no reply.
508pub struct NegotiateCmd;
509impl Command for NegotiateCmd {
510    type Request = NegotiateReq;
511    async fn serve(ctx: IoContext<Accepted, Bare>, _req: NegotiateReq, res: &mut Resources<'_>) -> Outcome {
512        let Some(hdr) = smb_server_proto_smb2::Header2::parse(res.frame) else {
513            return Outcome::Final(ctx.respond(Status::INVALID_PARAMETER, Vec::new()));
514        };
515        match crate::smb2::negotiate(res.conn, res.server, &hdr, res.frame) {
516            crate::smb2::NegotiateReply::Reply(status, body) => Outcome::Final(ctx.respond(status, body)),
517            crate::smb2::NegotiateReply::Silent => Outcome::Silent,
518        }
519    }
520}
521
522/// SESSION_SETUP handler ([MS-SMB2] §3.3.5.5): runs one NTLM/SPNEGO leg via the
523/// existing authenticator. Kept thin so the shared framing tail still folds the
524/// pre-auth hash, derives the signing key and registers the session in one place.
525pub struct SessionSetupCmd;
526impl Command for SessionSetupCmd {
527    type Request = SessionSetupReq;
528    async fn serve(ctx: IoContext<Accepted, Bare>, _req: SessionSetupReq, res: &mut Resources<'_>) -> Outcome {
529        match crate::smb2::session_setup(res.server, res.conn, res.frame) {
530            Ok((status, body)) => Outcome::Final(ctx.respond(status, body)),
531            Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
532        }
533    }
534}
535
536/// TREE_CONNECT handler ([MS-SMB2] §3.3.5.7): resolves the share, installs a
537/// fresh TreeId (surfaced to the reply header via conn.resp_tree_id), and
538/// answers with the share's type.
539pub struct TreeConnectCmd;
540impl Command for TreeConnectCmd {
541    type Request = TreeConnectReq;
542    async fn serve(ctx: IoContext<Accepted, Bare>, _req: TreeConnectReq, res: &mut Resources<'_>) -> Outcome {
543        match crate::smb2::tree_connect(res.server, res.conn, res.frame) {
544            Ok((name, share_type, encrypt, compress, ca)) => {
545                let new_tid = crate::smb2::next_tree_id();
546                res.conn.tree_insert(new_tid, name);
547                res.conn.resp_tree_id = Some(new_tid);
548                let mut share_flags = 0;
549                let mut capabilities = 0;
550                if encrypt {
551                    res.conn.seal_current = true;
552                    share_flags |= c::SHAREFLAG_ENCRYPT_DATA;
553                }
554                if compress {
555                    share_flags |= c::SHAREFLAG_COMPRESS_DATA;
556                }
557                if ca {
558                    share_flags |= c::SHAREFLAG_CONTINUOUSLY_AVAILABLE;
559                    capabilities |= c::SHARE_CAP_CONTINUOUS_AVAILABILITY;
560                }
561                counter!("smb_tcons_total").increment(1);
562                gauge!("smb_trees_active").increment(1.0);
563                Outcome::Final(ctx.respond(Status::SUCCESS, c::build_tree_connect_resp(share_type, share_flags, capabilities)))
564            }
565            Err(status) => Outcome::Final(ctx.respond(status, Vec::new())),
566        }
567    }
568}
569
570// The command table (single source of truth). A row makes a command decodable;
571// a row in `smb_dispatch!` (plus a Command impl) makes it handled.
572smb_request_table! {
573    Negotiate      = cmd::NEGOTIATE       => NegotiateReq;
574    Echo           = cmd::ECHO            => EchoReq;
575    SessionSetup   = cmd::SESSION_SETUP   => SessionSetupReq;
576    TreeConnect    = cmd::TREE_CONNECT    => TreeConnectReq;
577    TreeDisconnect = cmd::TREE_DISCONNECT => TreeDisconnectReq;
578    Logoff         = cmd::LOGOFF          => LogoffReq;
579    Create         = cmd::CREATE          => CreateReq;
580    Close          = cmd::CLOSE           => CloseReq;
581    Flush          = cmd::FLUSH           => FlushReq;
582    Read           = cmd::READ            => ReadReq;
583    Write          = cmd::WRITE           => WriteReq;
584    Lock           = cmd::LOCK            => LockReq;
585    Ioctl          = cmd::IOCTL           => IoctlReq;
586    QueryDir       = cmd::QUERY_DIRECTORY => QueryDirReq;
587    ChangeNotify   = cmd::CHANGE_NOTIFY   => ChangeNotifyReq;
588    QueryInfo      = cmd::QUERY_INFO      => QueryInfoReq;
589    SetInfo        = cmd::SET_INFO        => SetInfoReq;
590    Cancel         = cmd::CANCEL          => CancelReq;
591    OplockBreak    = cmd::OPLOCK_BREAK    => OplockBreakReq;
592}
593
594smb_dispatch! {
595    Negotiate      => NegotiateCmd;
596    Echo           => EchoCmd;
597    SessionSetup   => SessionSetupCmd;
598    TreeConnect    => TreeConnectCmd;
599    Flush          => FlushCmd;
600    TreeDisconnect => TreeDisconnectCmd;
601    Logoff         => LogoffCmd;
602    Read           => ReadCmd;
603    Write          => WriteCmd;
604    Close          => CloseCmd;
605    QueryDir       => QueryDirCmd;
606    QueryInfo      => QueryInfoCmd;
607    SetInfo        => SetInfoCmd;
608    Ioctl          => IoctlCmd;
609    Create         => CreateCmd;
610    Lock           => LockCmd;
611    ChangeNotify   => ChangeNotifyCmd;
612    Cancel         => CancelCmd;
613    OplockBreak    => OplockBreakCmd;
614}
615
616#[cfg(test)]
617mod tests {
618    use super::*;
619    use crate::io::wire::{ReplyHeader, SealIntent};
620
621    fn reply(command: u16, message_id: u64) -> ReplyHeader {
622        ReplyHeader {
623            command,
624            message_id,
625            session_id: 1,
626            tree_id: 1,
627            credits: 1,
628            seal: SealIntent::default(),
629        }
630    }
631
632    #[test]
633    fn accept_split_respond_moves_through_states() {
634        // The pure typestate path: accept a request, split it out for a handler,
635        // and produce a response — no server resources needed. Handlers that use
636        // resources are covered by the MS suite / conformance integration.
637        let req = SmbRequest::parse(cmd::ECHO, &[]).expect("decode echo");
638        assert_eq!(req.command(), cmd::ECHO);
639        let ctx = IoContext::accept(reply(cmd::ECHO, 7), req);
640        let (ctx, req) = ctx.split();
641        assert!(matches!(req, SmbRequest::Echo(_)));
642        let resp = ctx.respond(Status::SUCCESS, vec![1, 2, 3]);
643        assert_eq!(resp.status, Status::SUCCESS);
644        assert_eq!(resp.reply.message_id, 7);
645        assert_eq!(resp.body, vec![1, 2, 3]);
646    }
647
648    #[test]
649    fn unknown_command_is_not_implemented() {
650        assert_eq!(SmbRequest::parse(0xBEEF, &[]).unwrap_err(), Status::NOT_IMPLEMENTED);
651    }
652
653    #[test]
654    fn every_migrated_command_routes_to_its_decoder() {
655        // A command in the table routes to a decoder: on an empty frame the body
656        // decoders reject with INVALID_PARAMETER; the body-less commands accept.
657        for &code in &[
658            cmd::CREATE, cmd::CLOSE, cmd::FLUSH, cmd::READ, cmd::WRITE, cmd::LOCK,
659            cmd::IOCTL, cmd::QUERY_DIRECTORY, cmd::CHANGE_NOTIFY, cmd::QUERY_INFO, cmd::SET_INFO,
660        ] {
661            assert_eq!(
662                SmbRequest::parse(code, &[]).unwrap_err(),
663                Status::INVALID_PARAMETER,
664                "command {code} should route to a decoder, not fall through",
665            );
666        }
667        for &code in &[cmd::ECHO, cmd::TREE_DISCONNECT, cmd::LOGOFF] {
668            assert!(SmbRequest::parse(code, &[]).is_ok());
669        }
670    }
671
672    #[test]
673    fn decode_command_codes_match_the_table() {
674        assert_eq!(<CreateReq as Decode>::COMMAND, cmd::CREATE);
675        assert_eq!(<ReadReq as Decode>::COMMAND, cmd::READ);
676        assert_eq!(<WriteReq as Decode>::COMMAND, cmd::WRITE);
677        assert_eq!(<LockReq as Decode>::COMMAND, cmd::LOCK);
678        assert_eq!(<IoctlReq as Decode>::COMMAND, cmd::IOCTL);
679        assert_eq!(<SetInfoReq as Decode>::COMMAND, cmd::SET_INFO);
680    }
681}