now_proto_pdu/exec/
shell.rs

1use alloc::borrow::Cow;
2
3use bitflags::bitflags;
4use ironrdp_core::{
5    cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned,
6    ReadCursor, WriteCursor,
7};
8
9use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowVarStr};
10
11bitflags! {
12    /// NOW-PROTO: NOW_EXEC_SHELL_MSG msgFlags field.
13    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14    struct NowExecShellFlags: u16 {
15        /// Set if parameters shell contains non-default value.
16        ///
17        /// NOW-PROTO: NOW_EXEC_FLAG_SHELL_SHELL_SET
18        const PARAMETERS_SET = 0x0001;
19
20        /// Set if directory field contains non-default value.
21        ///
22        /// NOW-PROTO: NOW_EXEC_FLAG_SHELL_DIRECTORY_SET
23        const DIRECTORY_SET = 0x0002;
24
25        /// Enable stdio (stdout, stderr, stdin) redirection.
26        ///
27        /// NOW-PROTO: NOW_EXEC_FLAG_SHELL_IO_REDIRECTION
28        const IO_REDIRECTION = 0x1000;
29
30        /// Detached mode: the shell is started without tracking execution or sending back output.
31        ///
32        /// NOW-PROTO: NOW_EXEC_FLAG_SHELL_DETACHED
33        const DETACHED = 0x8000;
34    }
35}
36
37/// The NOW_EXEC_SHELL_MSG message is used to execute a remote shell command.
38///
39/// NOW-PROTO: NOW_EXEC_SHELL_MSG
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct NowExecShellMsg<'a> {
42    flags: NowExecShellFlags,
43    session_id: u32,
44    command: NowVarStr<'a>,
45    shell: NowVarStr<'a>,
46    directory: NowVarStr<'a>,
47}
48
49impl_pdu_borrowing!(NowExecShellMsg<'_>, OwnedNowExecShellMsg);
50
51impl IntoOwned for NowExecShellMsg<'_> {
52    type Owned = OwnedNowExecShellMsg;
53
54    fn into_owned(self) -> Self::Owned {
55        OwnedNowExecShellMsg {
56            flags: self.flags,
57            session_id: self.session_id,
58            command: self.command.into_owned(),
59            shell: self.shell.into_owned(),
60            directory: self.directory.into_owned(),
61        }
62    }
63}
64
65impl<'a> NowExecShellMsg<'a> {
66    const NAME: &'static str = "NOW_EXEC_SHELL_MSG";
67    const FIXED_PART_SIZE: usize = 4;
68
69    pub fn new(session_id: u32, command: impl Into<Cow<'a, str>>) -> EncodeResult<Self> {
70        let msg = Self {
71            flags: NowExecShellFlags::empty(),
72            session_id,
73            command: NowVarStr::new(command)?,
74            shell: NowVarStr::default(),
75            directory: NowVarStr::default(),
76        };
77
78        msg.ensure_message_size()?;
79
80        Ok(msg)
81    }
82
83    pub fn with_shell(mut self, shell: impl Into<Cow<'a, str>>) -> EncodeResult<Self> {
84        self.flags |= NowExecShellFlags::PARAMETERS_SET;
85        self.shell = NowVarStr::new(shell)?;
86
87        self.ensure_message_size()?;
88
89        Ok(self)
90    }
91
92    pub fn with_directory(mut self, directory: impl Into<Cow<'a, str>>) -> EncodeResult<Self> {
93        self.flags |= NowExecShellFlags::DIRECTORY_SET;
94        self.directory = NowVarStr::new(directory)?;
95
96        self.ensure_message_size()?;
97
98        Ok(self)
99    }
100
101    fn ensure_message_size(&self) -> EncodeResult<()> {
102        ensure_now_message_size!(
103            Self::FIXED_PART_SIZE,
104            self.command.size(),
105            self.shell.size(),
106            self.directory.size()
107        );
108
109        Ok(())
110    }
111
112    pub fn session_id(&self) -> u32 {
113        self.session_id
114    }
115
116    pub fn command(&self) -> &str {
117        &self.command
118    }
119
120    pub fn shell(&self) -> Option<&str> {
121        if self.flags.contains(NowExecShellFlags::PARAMETERS_SET) {
122            Some(&self.shell)
123        } else {
124            None
125        }
126    }
127
128    pub fn directory(&self) -> Option<&str> {
129        if self.flags.contains(NowExecShellFlags::DIRECTORY_SET) {
130            Some(&self.directory)
131        } else {
132            None
133        }
134    }
135
136    #[must_use]
137    pub fn with_io_redirection(mut self) -> Self {
138        self.flags |= NowExecShellFlags::IO_REDIRECTION;
139        self
140    }
141
142    pub fn is_with_io_redirection(&self) -> bool {
143        self.flags.contains(NowExecShellFlags::IO_REDIRECTION)
144    }
145
146    #[must_use]
147    pub fn with_detached(mut self) -> Self {
148        self.flags |= NowExecShellFlags::DETACHED;
149        self
150    }
151
152    pub fn is_detached(&self) -> bool {
153        self.flags.contains(NowExecShellFlags::DETACHED)
154    }
155
156    // LINTS: Overall message size is validated in the constructor/decode method
157    #[allow(clippy::arithmetic_side_effects)]
158    fn body_size(&self) -> usize {
159        Self::FIXED_PART_SIZE + self.command.size() + self.shell.size() + self.directory.size()
160    }
161
162    pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult<Self> {
163        ensure_fixed_part_size!(in: src);
164
165        let flags = NowExecShellFlags::from_bits_retain(header.flags);
166        let session_id = src.read_u32();
167        let command = NowVarStr::decode(src)?;
168        let shell = NowVarStr::decode(src)?;
169        let directory = NowVarStr::decode(src)?;
170
171        let msg = Self {
172            flags,
173            session_id,
174            command,
175            shell,
176            directory,
177        };
178
179        Ok(msg)
180    }
181}
182
183impl Encode for NowExecShellMsg<'_> {
184    fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
185        let header = NowHeader {
186            size: cast_length!("size", self.body_size())?,
187            class: NowMessageClass::EXEC,
188            kind: NowExecMsgKind::SHELL.0,
189            flags: self.flags.bits(),
190        };
191
192        header.encode(dst)?;
193
194        ensure_fixed_part_size!(in: dst);
195        dst.write_u32(self.session_id);
196        self.command.encode(dst)?;
197        self.shell.encode(dst)?;
198        self.directory.encode(dst)?;
199
200        Ok(())
201    }
202
203    fn name(&self) -> &'static str {
204        Self::NAME
205    }
206
207    // LINTS: See body_size()
208    #[allow(clippy::arithmetic_side_effects)]
209    fn size(&self) -> usize {
210        NowHeader::FIXED_PART_SIZE + self.body_size()
211    }
212}
213
214impl<'de> Decode<'de> for NowExecShellMsg<'de> {
215    fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
216        let header = NowHeader::decode(src)?;
217
218        match (header.class, NowExecMsgKind(header.kind)) {
219            (NowMessageClass::EXEC, NowExecMsgKind::SHELL) => Self::decode_from_body(header, src),
220            _ => Err(invalid_field_err!("type", "invalid message type")),
221        }
222    }
223}
224
225impl<'a> From<NowExecShellMsg<'a>> for NowMessage<'a> {
226    fn from(msg: NowExecShellMsg<'a>) -> Self {
227        NowMessage::Exec(NowExecMessage::Shell(msg))
228    }
229}