diff --git a/Cargo.lock b/Cargo.lock index 9a266bb..fb3a4ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,178 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dissimilar" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "expect-test" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0be0a561335815e06dab7c62e50353134c796e7a6155402a64bcff66b6a5e0" +dependencies = [ + "dissimilar", + "once_cell", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "ironrdp-core" version = "0.1.2" @@ -29,6 +189,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c42594b1f8ec9490dcbc33128fd4400235bab4942a59b99b36b0041259963492" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "now-proto-fuzzing" version = "0.0.0" @@ -39,11 +205,23 @@ version = "0.1.0" dependencies = [ "bitflags", "ironrdp-core", + "ironrdp-error", ] [[package]] name = "now-proto-testsuite" version = "0.0.0" +dependencies = [ + "expect-test", + "now-proto-pdu", + "rstest", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "pico-args" @@ -51,6 +229,177 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "xshell" version = "0.2.7" diff --git a/README.md b/README.md index 787fa6a..5ade808 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ -# now-proto +NOW-proto +========= +Devolutions::Now::Agent RDP virtual channel protocol libraries and clients. -Devolutions::Now::Agent RDP virtual channel +### Specification +Current protocol specification: [read](./doc/NOW-spec.md) + +### now-proto-pdu (rust) +This repository contains the [Rust implementation](./crates/now-proto-pdu/README.md) of the +NOW-proto protocol encoding/decoding library. + +### Updating protocol +In order to update the protocol, the following steps should be followed: +1. Update the protocol specification in `doc/NOW-spec.md`. + 1. Bump the protocol version number. +1. Update the Rust implementation of the protocol in `crates/now-proto-pdu`. + 1. Bump current protocol version in `NowProtoVersion::CURRENT` +1. Update the C# protocol implementation (WIP) +1. Update C# clients (WIP) \ No newline at end of file diff --git a/crates/now-proto-pdu/Cargo.toml b/crates/now-proto-pdu/Cargo.toml index 9a98e3d..e6e3068 100644 --- a/crates/now-proto-pdu/Cargo.toml +++ b/crates/now-proto-pdu/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true authors.workspace = true keywords.workspace = true categories.workspace = true +publish = true [lib] doctest = false @@ -20,7 +21,8 @@ workspace = true [dependencies] bitflags = "2" -ironrdp-core = { version = "0.1.2", features = ["alloc"] } +ironrdp-core = { version = "0.1", features = ["alloc"] } +ironrdp-error = { version = "0.1", features = ["alloc"] } [features] default = [] diff --git a/crates/now-proto-pdu/README.md b/crates/now-proto-pdu/README.md new file mode 100644 index 0000000..891b2ca --- /dev/null +++ b/crates/now-proto-pdu/README.md @@ -0,0 +1,34 @@ +NOW-proto PDU encoding/decoding library +======================================= + +This crate provides a Rust implementation of the NOW-proto protocol encoding/decoding library. + +## Library architecture details +- The library only provides encoding/decoding functions for the NOW-proto protocol, transport layer + and session processing logic should be implemented by the client/server. +- `#[no_std]` compatible. Requires `alloc`. +- PDUs could contain borrowed data by default to avoid unnecessary string/vec allocations when + parsing. The only exception is error types which are `'static` and may allocate if optional + message is set. +- PDUs are immutable by default. PDU constructors take only required fields, optional fields are + set using implicit builder pattern (consuming `.with_*` and `.set_*` methods). +- User-facing `bitfield` types should be avoied in the public API if incorrect usage could lead to + invalid PDUs. E.g. `ExecData`'s stdio stream flags are represented as a bitfield, but exactly + one of the flags should be set at a time. The public API should provide a safe way to set and + retrieve these flags. Channel capabilities flags on the other hand could all be set independently, + therefore it is safe to expose them in the public API. +- Primitive protocol types e.g `NOW_STRING` should not be exposed in the public API. +- Message validition should be always checked in the PDU constructor(s). If the message have + variable fields, it should be ensured that it could fit into the message body (`u32`). +- PDUs should NOT fail on deserialization if message body have more data to ensure backwards + compatibility with future protocol versions (e.g. new fields added to the end of the message in + the new protocol version). +- + + + +## Versioning +Crate version is not tied to the protocol version (e.g. Introduction of breaking changes in the +crate API does not necessarily mean a protocol version bump and vice versa). The currently +implemented protocol version is stored in [`NowProtoVersion::CURRENT`] and should be updated +accordingly when the protocol is updated. \ No newline at end of file diff --git a/crates/now-proto-pdu/src/channel/capset.rs b/crates/now-proto-pdu/src/channel/capset.rs new file mode 100644 index 0000000..f5ba9f7 --- /dev/null +++ b/crates/now-proto-pdu/src/channel/capset.rs @@ -0,0 +1,299 @@ +use core::time; + +use bitflags::bitflags; +use ironrdp_core::{ + ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, +}; + +use crate::{NowChannelMessage, NowChannelMsgKind, NowHeader, NowMessage, NowMessageClass}; + +bitflags! { + /// NOW-PROTO: NOW_CHANNEL_CAPSET_MSG flags field. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct NowChannelCapsetFlags: u16 { + /// Set if heartbeat specify channel heartbeat interval. + /// + /// NOW-PROTO: NOW_CHANNEL_SET_HEATBEAT + const SET_HEARTBEAT = 0x0001; + } +} + +bitflags! { + /// NOW-PROTO: NOW_CHANNEL_CAPSET_MSG systemCapset field. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct NowSystemCapsetFlags: u16 { + /// System shutdown command support. + /// + /// NOW-PROTO: NOW_CAP_SYSTEM_SHUTDOWN + const SHUTDOWN = 0x0001; + } +} + +bitflags! { + /// NOW-PROTO: NOW_CHANNEL_CAPSET_MSG sessionCapset field. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct NowSessionCapsetFlags: u16 { + /// Session lock command support. + /// + /// NOW-PROTO: NOW_CAP_SESSION_LOCK + const LOCK = 0x0001; + /// Session logoff command support. + /// + /// NOW-PROTO: NOW_CAP_SESSION_LOGOFF + const LOGOFF = 0x0002; + /// Message box command support. + /// + /// NOW-PROTO: NOW_CAP_SESSION_MSGBOX + const MSGBOX = 0x0004; + } +} + +bitflags! { + /// NOW-PROTO: NOW_CHANNEL_CAPSET_MSG execCapset field. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct NowExecCapsetFlags: u16 { + /// Generic "Run" execution style. + /// + /// NOW-PROTO: NOW_CAP_EXEC_STYLE_RUN + const STYLE_RUN = 0x0001; + /// CreateProcess() execution style. + /// + /// NOW-PROTO: NOW_CAP_EXEC_STYLE_PROCESS + const STYLE_PROCESS = 0x0002; + /// System shell (.sh) execution style. + /// + /// NOW-PROTO: NOW_CAP_EXEC_STYLE_SHELL + const STYLE_SHELL = 0x0004; + /// Windows batch file (.bat) execution style. + /// + /// NOW-PROTO: NOW_CAP_EXEC_STYLE_BATCH + const STYLE_BATCH = 0x00008; + /// Windows PowerShell (.ps1) execution style. + /// + /// NOW-PROTO: NOW_CAP_EXEC_STYLE_WINPS + const STYLE_WINPS = 0x0010; + /// PowerShell 7 (.ps1) execution style. + /// + /// NOW-PROTO: NOW_CAP_EXEC_STYLE_PWSH + const STYLE_PWSH = 0x0020; + } +} + +/// NOW-PROTO version representation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NowProtoVersion { + // IMPORTANT: Field ordering is important for `PartialOrd` and `Ord` derived implementations. + pub major: u16, + pub minor: u16, +} + +impl NowProtoVersion { + /// Represents the current version of the NOW protocol implemented by the library. + pub const CURRENT: Self = Self { major: 1, minor: 0 }; +} + +/// This message is first set by the client side, to advertise capabilities. +/// Received client message should be downgraded by the server (remove non-intersecting +/// capabilities) and sent back to the client at the start of DVC channel communications. +/// DVC channel should be closed if protocol versions are not compatible. +/// +/// `Default` implementation returns capabilities with empty capability sets and no heartbeat +/// interval set. Proto version is set to [`NowProtoVersion::CURRENT`] by default. +/// +/// NOW-PROTO: NOW_CHANNEL_CAPSET_MSG +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NowChannelCapsetMsg { + version: NowProtoVersion, + system_capset: NowSystemCapsetFlags, + session_capset: NowSessionCapsetFlags, + exec_capset: NowExecCapsetFlags, + heartbeat_interval: Option, +} + +impl Default for NowChannelCapsetMsg { + fn default() -> Self { + Self { + version: NowProtoVersion::CURRENT, + system_capset: NowSystemCapsetFlags::empty(), + session_capset: NowSessionCapsetFlags::empty(), + exec_capset: NowExecCapsetFlags::empty(), + heartbeat_interval: None, + } + } +} + +impl NowChannelCapsetMsg { + const NAME: &'static str = "NOW_CHANNEL_CAPSET_MSG"; + const FIXED_PART_SIZE: usize = 14; // NowProtoVersion(4) + u16(2) + u16(2) + u16(2) + u32(4) + + #[must_use] + pub fn with_system_capset(mut self, system_capset: NowSystemCapsetFlags) -> Self { + self.system_capset = system_capset; + self + } + + #[must_use] + pub fn with_session_capset(mut self, session_capset: NowSessionCapsetFlags) -> Self { + self.session_capset = session_capset; + self + } + + #[must_use] + pub fn with_exec_capset(mut self, exec_capset: NowExecCapsetFlags) -> Self { + self.exec_capset = exec_capset; + self + } + + pub fn with_heartbeat_interval(mut self, interval: time::Duration) -> EncodeResult { + // Sanity check: Limit heartbeat interval to 24 hours. + const MAX_HEARTBEAT_INTERVAL: time::Duration = time::Duration::from_secs(60 * 60 * 24); + + if interval > MAX_HEARTBEAT_INTERVAL { + return Err(invalid_field_err!("heartbeat_timeout", "too big heartbeat interval")); + } + + let interval = u32::try_from(interval.as_secs()).expect("heartbeat interval fits into u32"); + + self.heartbeat_interval = Some(interval); + Ok(self) + } + + pub fn system_capset(&self) -> NowSystemCapsetFlags { + self.system_capset + } + + pub fn session_capset(&self) -> NowSessionCapsetFlags { + self.session_capset + } + + pub fn exec_capset(&self) -> NowExecCapsetFlags { + self.exec_capset + } + + pub fn heartbeat_interval(&self) -> Option { + self.heartbeat_interval + .map(|interval| time::Duration::from_secs(u64::from(interval))) + } + + pub fn version(&self) -> NowProtoVersion { + self.version + } + + /// Downgrade capabilities to the minimum common capabilities between two peers. + /// + /// - Version is chosen as minimum between two peers. + /// - Capabilities are chosen as intersection between two peers. + /// - Heartbeat interval is chosen as minimum specified value between two peers. + #[must_use] + pub fn downgrade(&self, other: &Self) -> Self { + // Choose minimum version between two peers. + let version = self.version.min(other.version); + + let system_capset = self.system_capset & other.system_capset; + let session_capset = self.session_capset & other.session_capset; + let exec_capset = self.exec_capset & other.exec_capset; + + // Choose minimum specified heartbeat interval between two peers. + let heartbeat_interval = match (self.heartbeat_interval, other.heartbeat_interval) { + (Some(lhs), Some(rhs)) => Some(lhs.min(rhs)), + (Some(lhs), None) => Some(lhs), + (None, Some(rhs)) => Some(rhs), + (None, None) => None, + }; + + Self { + version, + system_capset, + session_capset, + exec_capset, + heartbeat_interval, + } + } + + pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let flags = NowChannelCapsetFlags::from_bits_retain(header.flags); + + let major_version = src.read_u16(); + let minor_version = src.read_u16(); + + let version = NowProtoVersion { + major: major_version, + minor: minor_version, + }; + + let system_capset = NowSystemCapsetFlags::from_bits_retain(src.read_u16()); + let session_capset = NowSessionCapsetFlags::from_bits_retain(src.read_u16()); + let exec_capset = NowExecCapsetFlags::from_bits_retain(src.read_u16()); + // Read heartbeat interval unconditionally even if `SET_HEARTBEAT` flags is not set. + let heartbeat_interval_value = src.read_u32(); + + let heartbeat_interval = flags + .contains(NowChannelCapsetFlags::SET_HEARTBEAT) + .then_some(heartbeat_interval_value); + + Ok(Self { + version, + system_capset, + session_capset, + exec_capset, + heartbeat_interval, + }) + } +} + +impl Encode for NowChannelCapsetMsg { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let flags = if self.heartbeat_interval.is_some() { + NowChannelCapsetFlags::SET_HEARTBEAT + } else { + NowChannelCapsetFlags::empty() + }; + + let header = NowHeader { + size: u32::try_from(Self::FIXED_PART_SIZE).expect("Capabilities have fixed size which fits into u32"), + class: NowMessageClass::CHANNEL, + kind: NowChannelMsgKind::CAPSET.0, + flags: flags.bits(), + }; + + header.encode(dst)?; + + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.version.major); + dst.write_u16(self.version.minor); + dst.write_u16(self.system_capset.bits()); + dst.write_u16(self.session_capset.bits()); + dst.write_u16(self.exec_capset.bits()); + dst.write_u32(self.heartbeat_interval.unwrap_or_default()); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + NowHeader::FIXED_PART_SIZE + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for NowChannelCapsetMsg { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + let header = NowHeader::decode(src)?; + + match (header.class, NowChannelMsgKind(header.kind)) { + (NowMessageClass::CHANNEL, NowChannelMsgKind::CAPSET) => Self::decode_from_body(header, src), + _ => Err(invalid_field_err!("type", "invalid message type")), + } + } +} + +impl From for NowMessage<'_> { + fn from(msg: NowChannelCapsetMsg) -> Self { + NowMessage::Channel(NowChannelMessage::Capset(msg)) + } +} diff --git a/crates/now-proto-pdu/src/channel/close.rs b/crates/now-proto-pdu/src/channel/close.rs new file mode 100644 index 0000000..742d8ae --- /dev/null +++ b/crates/now-proto-pdu/src/channel/close.rs @@ -0,0 +1,98 @@ +use ironrdp_core::{invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor}; + +use crate::{NowChannelMessage, NowChannelMsgKind, NowHeader, NowMessage, NowMessageClass, NowStatus, NowStatusError}; + +/// Channel close notice, could be sent by either parties at any moment of communication to +/// gracefully close DVC channel. +/// +/// NOW-PROTO: NOW_CHANNEL_CLOSE_MSG +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NowChannelCloseMsg<'a> { + status: NowStatus<'a>, +} + +impl_pdu_borrowing!(NowChannelCloseMsg<'_>, OwnedNowChannelCloseMsg); + +impl IntoOwned for NowChannelCloseMsg<'_> { + type Owned = OwnedNowChannelCloseMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowChannelCloseMsg { + status: self.status.into_owned(), + } + } +} + +impl Default for NowChannelCloseMsg<'_> { + fn default() -> Self { + let status = NowStatus::new_success(); + + Self { status } + } +} + +impl<'a> NowChannelCloseMsg<'a> { + const NAME: &'static str = "NOW_CHANNEL_CLOSE_MSG"; + + pub fn from_error(error: impl Into) -> EncodeResult { + let status = NowStatus::new_error(error); + + let msg = Self { status }; + + ensure_now_message_size!(msg.status.size()); + + Ok(msg) + } + + pub fn to_result(&self) -> Result<(), NowStatusError> { + self.status.to_result() + } + + pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { + let status = NowStatus::decode(src)?; + + Ok(Self { status }) + } +} + +impl Encode for NowChannelCloseMsg<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header = NowHeader { + size: self.status.size().try_into().expect("validated in constructor"), + class: NowMessageClass::CHANNEL, + kind: NowChannelMsgKind::CLOSE.0, + flags: 0, + }; + + header.encode(dst)?; + + self.status.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + NowHeader::FIXED_PART_SIZE + self.status.size() + } +} + +impl<'de> Decode<'de> for NowChannelCloseMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { + let header = NowHeader::decode(src)?; + + match (header.class, NowChannelMsgKind(header.kind)) { + (NowMessageClass::CHANNEL, NowChannelMsgKind::CLOSE) => Self::decode_from_body(header, src), + _ => Err(invalid_field_err!("type", "invalid message type")), + } + } +} + +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowChannelCloseMsg<'a>) -> Self { + NowMessage::Channel(NowChannelMessage::Close(msg)) + } +} diff --git a/crates/now-proto-pdu/src/channel/heartbeat.rs b/crates/now-proto-pdu/src/channel/heartbeat.rs new file mode 100644 index 0000000..18a0f96 --- /dev/null +++ b/crates/now-proto-pdu/src/channel/heartbeat.rs @@ -0,0 +1,54 @@ +use ironrdp_core::{invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +use crate::{NowChannelMessage, NowChannelMsgKind, NowHeader, NowMessage, NowMessageClass}; + +/// Periodic heartbeat message sent by the server. If the client does not receive this message +/// within the specified interval, it should consider the connection as lost. +/// +/// NOW-PROTO: NOW_CHANNEL_HEARTBEAT_MSG +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct NowChannelHeartbeatMsg {} + +impl NowChannelHeartbeatMsg { + const NAME: &'static str = "NOW_CHANNEL_HEARTBEAT_MSG"; +} + +impl Encode for NowChannelHeartbeatMsg { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header = NowHeader { + size: 0, + class: NowMessageClass::CHANNEL, + kind: NowChannelMsgKind::HEARTBEAT.0, + flags: 0, + }; + + header.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + NowHeader::FIXED_PART_SIZE + } +} + +impl Decode<'_> for NowChannelHeartbeatMsg { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + let header = NowHeader::decode(src)?; + + match (header.class, NowChannelMsgKind(header.kind)) { + (NowMessageClass::CHANNEL, NowChannelMsgKind::HEARTBEAT) => Ok(NowChannelHeartbeatMsg::default()), + _ => Err(invalid_field_err!("type", "invalid message type")), + } + } +} + +impl From for NowMessage<'_> { + fn from(msg: NowChannelHeartbeatMsg) -> Self { + NowMessage::Channel(NowChannelMessage::Heartbeat(msg)) + } +} diff --git a/crates/now-proto-pdu/src/channel/mod.rs b/crates/now-proto-pdu/src/channel/mod.rs new file mode 100644 index 0000000..5d8fe6c --- /dev/null +++ b/crates/now-proto-pdu/src/channel/mod.rs @@ -0,0 +1,83 @@ +mod capset; +mod close; +mod heartbeat; + +pub use capset::{ + NowChannelCapsetFlags, NowChannelCapsetMsg, NowExecCapsetFlags, NowProtoVersion, NowSessionCapsetFlags, + NowSystemCapsetFlags, +}; +pub use close::{NowChannelCloseMsg, OwnedNowChannelCloseMsg}; +pub use heartbeat::NowChannelHeartbeatMsg; +use ironrdp_core::{DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor}; + +use crate::NowHeader; + +// Wrapper for the `NOW_CHANNEL_MSG_CLASS_ID` message class. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NowChannelMessage<'a> { + Capset(NowChannelCapsetMsg), + Heartbeat(NowChannelHeartbeatMsg), + Close(NowChannelCloseMsg<'a>), +} + +pub type OwnedNowChannelMessage = NowChannelMessage<'static>; + +impl IntoOwned for NowChannelMessage<'_> { + type Owned = OwnedNowChannelMessage; + + fn into_owned(self) -> Self::Owned { + match self { + Self::Capset(msg) => OwnedNowChannelMessage::Capset(msg), + Self::Heartbeat(msg) => OwnedNowChannelMessage::Heartbeat(msg), + Self::Close(msg) => OwnedNowChannelMessage::Close(msg.into_owned()), + } + } +} + +impl<'a> NowChannelMessage<'a> { + const NAME: &'static str = "NOW_CHANNEL_MSG"; + + pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { + match NowChannelMsgKind(header.kind) { + NowChannelMsgKind::CAPSET => Ok(Self::Capset(NowChannelCapsetMsg::decode_from_body(header, src)?)), + NowChannelMsgKind::HEARTBEAT => Ok(Self::Heartbeat(NowChannelHeartbeatMsg::default())), + NowChannelMsgKind::CLOSE => Ok(Self::Close(NowChannelCloseMsg::decode_from_body(header, src)?)), + _ => Err(unsupported_message_err!(class: header.class.0, kind: header.kind)), + } + } +} + +impl Encode for NowChannelMessage<'_> { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + match self { + Self::Capset(msg) => msg.encode(dst), + Self::Heartbeat(msg) => msg.encode(dst), + Self::Close(msg) => msg.encode(dst), + } + } + + fn name(&self) -> &'static str { + Self::NAME + } + + fn size(&self) -> usize { + match self { + Self::Capset(msg) => msg.size(), + Self::Heartbeat(msg) => msg.size(), + Self::Close(msg) => msg.size(), + } + } +} + +/// NOW-PROTO: NOW_CHANNEL_*_ID +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NowChannelMsgKind(pub u8); + +impl NowChannelMsgKind { + /// NOW-PROTO: NOW_CHANNEL_CAPSET_MSG_ID + pub const CAPSET: Self = Self(0x01); + /// NOW-PROTO: NOW_CHANNEL_HEARTBEAT_MSG_ID + pub const HEARTBEAT: Self = Self(0x02); + /// NOW-PROTO: NOW_CHANNEL_CLOSE_MSG_ID + pub const CLOSE: Self = Self(0x03); +} diff --git a/crates/now-proto-pdu/src/core/buffer.rs b/crates/now-proto-pdu/src/core/buffer.rs index b0269c3..6244ff7 100644 --- a/crates/now-proto-pdu/src/core/buffer.rs +++ b/crates/now-proto-pdu/src/core/buffer.rs @@ -1,115 +1,34 @@ //! Buffer types for NOW protocol. -use alloc::vec::Vec; +use alloc::borrow::Cow; +use core::ops::Deref; use ironrdp_core::{ - cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, - ReadCursor, WriteCursor, + cast_length, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, + WriteCursor, }; use crate::VarU32; -/// String value up to 2^32 bytes long. +/// Buffer up to 2^30 bytes long (Length has compact variable length encoding). /// -/// NOW-PROTO: NOW_LRGBUF -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowLrgBuf(Vec); - -impl NowLrgBuf { - const NAME: &'static str = "NOW_LRGBUF"; - const FIXED_PART_SIZE: usize = 4; - - /// Create a new `NowLrgBuf` instance. Returns an error if the provided value is too large. - pub fn new(value: impl Into>) -> DecodeResult { - let value: Vec = value.into(); - - if value.len() > VarU32::MAX as usize { - return Err(invalid_field_err!("data", "data is too large for NOW_LRGBUF")); - } - - Self::ensure_message_size(value.len())?; - - Ok(NowLrgBuf(value)) - } - - /// Get the buffer value. - pub fn value(&self) -> &[u8] { - self.0.as_slice() - } - - fn ensure_message_size(buffer_size: usize) -> DecodeResult<()> { - if buffer_size > usize::try_from(VarU32::MAX).expect("BUG: too small usize") { - return Err(invalid_field_err!("data", "data is too large for NOW_LRGBUF")); - } - - if buffer_size > usize::MAX - Self::FIXED_PART_SIZE { - return Err(invalid_field_err!( - "data", - "data size is too large to fit in 32-bit usize" - )); - } - - Ok(()) - } -} - -impl Encode for NowLrgBuf { - fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { - let encoded_size = self.size(); - ensure_size!(in: dst, size: encoded_size); - - let len: u32 = self.0.len().try_into().expect("BUG: validated in constructor"); - - dst.write_u32(len); - dst.write_slice(self.0.as_slice()); - - Ok(()) - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn size(&self) -> usize { - // + - Self::FIXED_PART_SIZE - .checked_add(self.0.len()) - .expect("BUG: size overflow") - } -} - -impl Decode<'_> for NowLrgBuf { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { - ensure_fixed_part_size!(in: src); - - let len: usize = cast_length!("len", src.read_u32())?; - - Self::ensure_message_size(len)?; - - ensure_size!(in: src, size: len); - let bytes = src.read_slice(len); +/// NOW-PROTO: NOW_VARBUF +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct NowVarBuf<'a>(Cow<'a, [u8]>); - Ok(NowLrgBuf(bytes.to_vec())) - } -} +impl IntoOwned for NowVarBuf<'_> { + type Owned = NowVarBuf<'static>; -impl From for Vec { - fn from(buf: NowLrgBuf) -> Self { - buf.0 + fn into_owned(self) -> Self::Owned { + NowVarBuf(Cow::Owned(self.0.into_owned())) } } -/// Buffer up to 2^31 bytes long (Length has compact variable length encoding). -/// -/// NOW-PROTO: NOW_VARBUF -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowVarBuf(Vec); - -impl NowVarBuf { +impl<'a> NowVarBuf<'a> { const NAME: &'static str = "NOW_VARBUF"; /// Create a new `NowVarBuf` instance. Returns an error if the provided value is too large. - pub fn new(value: impl Into>) -> DecodeResult { + pub(crate) fn new(value: impl Into>) -> EncodeResult { let value = value.into(); let _: u32 = value @@ -121,14 +40,9 @@ impl NowVarBuf { Ok(NowVarBuf(value)) } - - /// Get the buffer value. - pub fn value(&self) -> &[u8] { - self.0.as_slice() - } } -impl Encode for NowVarBuf { +impl Encode for NowVarBuf<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let encoded_size = self.size(); ensure_size!(in: dst, size: encoded_size); @@ -136,7 +50,7 @@ impl Encode for NowVarBuf { let len: u32 = self.0.len().try_into().expect("BUG: validated in constructor"); VarU32::new(len)?.encode(dst)?; - dst.write_slice(self.0.as_slice()); + dst.write_slice(&self.0); Ok(()) } @@ -155,20 +69,22 @@ impl Encode for NowVarBuf { } } -impl Decode<'_> for NowVarBuf { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowVarBuf<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let len_u32 = VarU32::decode(src)?.value(); let len: usize = cast_length!("len", len_u32)?; ensure_size!(in: src, size: len); let bytes = src.read_slice(len); - Ok(NowVarBuf(bytes.to_vec())) + Ok(NowVarBuf(Cow::Borrowed(bytes))) } } -impl From for Vec { - fn from(buf: NowVarBuf) -> Self { - buf.0 +impl Deref for NowVarBuf<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 } } diff --git a/crates/now-proto-pdu/src/core/header.rs b/crates/now-proto-pdu/src/core/header.rs index 5d0fa59..5144820 100644 --- a/crates/now-proto-pdu/src/core/header.rs +++ b/crates/now-proto-pdu/src/core/header.rs @@ -4,6 +4,9 @@ use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeR pub struct NowMessageClass(pub u8); impl NowMessageClass { + /// NOW-PROTO: NOW_CHANNEL_MSG_CLASS_ID + pub const CHANNEL: Self = Self(0x10); + /// NOW-PROTO: NOW_SYSTEM_MSG_CLASS_ID pub const SYSTEM: Self = Self(0x11); diff --git a/crates/now-proto-pdu/src/core/mod.rs b/crates/now-proto-pdu/src/core/mod.rs index 1e0572e..9ac7ebd 100644 --- a/crates/now-proto-pdu/src/core/mod.rs +++ b/crates/now-proto-pdu/src/core/mod.rs @@ -1,4 +1,7 @@ //! This module contains `NOW-PROTO` core types definitions. +//! +//! Note that these types are not intended to be used directly by the user, and not exported in the +//! public API. mod buffer; mod header; @@ -6,8 +9,11 @@ mod number; mod status; mod string; -pub use buffer::{NowLrgBuf, NowVarBuf}; -pub use header::{NowHeader, NowMessageClass}; -pub use number::{VarI16, VarI32, VarI64, VarU16, VarU32, VarU64}; -pub use status::{NowSeverity, NowStatus, NowStatusCode}; -pub use string::{NowLrgStr, NowString128, NowString16, NowString256, NowString32, NowString64, NowVarStr}; +pub(crate) use buffer::NowVarBuf; +pub(crate) use header::{NowHeader, NowMessageClass}; +pub(crate) use number::VarU32; +pub(crate) use status::NowStatus; +// Only public-exported type is the status error, which should be available to the user for error +// handling. +pub use status::{NowProtoError, NowStatusError, NowStatusErrorKind}; +pub(crate) use string::NowVarStr; diff --git a/crates/now-proto-pdu/src/core/number.rs b/crates/now-proto-pdu/src/core/number.rs index 19fb8a8..fed6bc6 100644 --- a/crates/now-proto-pdu/src/core/number.rs +++ b/crates/now-proto-pdu/src/core/number.rs @@ -1,258 +1,8 @@ //! Variable-length number types. use ironrdp_core::{ - ensure_size, invalid_field_err, Decode, DecodeError, DecodeResult, Encode, EncodeError, EncodeResult, ReadCursor, - WriteCursor, + ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeError, EncodeResult, ReadCursor, WriteCursor, }; -/// Variable-length encoded u16. -/// Value range:`[0..0x7FFF]` -/// -/// NOW-PROTO: NOW_VARU16 -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct VarU16(u16); - -impl VarU16 { - pub const MIN: u16 = 0x0000; - pub const MAX: u16 = 0x7FFF; - - const NAME: &'static str = "NOW_VARU16"; - - pub fn new(value: u16) -> DecodeResult { - if value > Self::MAX { - return Err(invalid_field_err!("value", "too large number")); - } - - Ok(VarU16(value)) - } - - pub fn value(&self) -> u16 { - self.0 - } -} - -impl Encode for VarU16 { - fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { - let encoded_size = self.size(); - - ensure_size!(in: dst, size: encoded_size); - - // LINTS: encoded_size will always be 1 or 2, therefore following arithmetic is safe - #[allow(clippy::arithmetic_side_effects)] - let mut shift = (encoded_size - 1) * 8; - let mut bytes = [0u8; 2]; - - for byte in bytes.iter_mut().take(encoded_size) { - *byte = ((self.0 >> shift) & 0xFF).try_into().expect("always <= 0xFF"); - - // LINTS: as per code above, shift is always 8 or 16 - #[allow(clippy::arithmetic_side_effects)] - if shift != 0 { - shift -= 8; - } - } - - // LINTS: encoded_size is always >= 1 - #[allow(clippy::arithmetic_side_effects)] - let c: u8 = (encoded_size - 1).try_into().expect("always fits into u8"); - bytes[0] |= c << 7; - - dst.write_slice(&bytes[..encoded_size]); - - Ok(()) - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn size(&self) -> usize { - match self.0 { - 0x00..=0x7F => 1, - 0x80..=0x7FFF => 2, - _ => unreachable!("BUG: value is out of range!"), - } - } -} - -impl Decode<'_> for VarU16 { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { - // Ensure we have at least 1 byte available to determine the size of the value - ensure_size!(in: src, size: 1); - - let header = src.read_u8(); - let c: usize = ((header >> 7) & 0x01).into(); - - if c == 0 { - return Ok(VarU16((header & 0x7F).into())); - } - - ensure_size!(in: src, size: c); - let bytes = src.read_slice(c); - - let val1 = header & 0x7F; - // LINTS: c is always 1 or 2 - #[allow(clippy::arithmetic_side_effects)] - let mut shift = c * 8; - let mut num = u16::from(val1) << shift; - - // Read val2..valN - // LINTS: shift is always 8 or 16 - #[allow(clippy::arithmetic_side_effects)] - for val in bytes.iter().take(c) { - shift -= 8; - num |= (u16::from(*val)) << shift; - } - - Ok(VarU16(num)) - } -} - -impl From for u16 { - fn from(value: VarU16) -> Self { - value.value() - } -} - -impl TryFrom for VarU16 { - type Error = DecodeError; - - fn try_from(value: u16) -> Result { - Self::new(value) - } -} - -/// Variable-length encoded i16. -/// Value range:`[-0x3FFF..0x3FFF]` -/// -/// NOW-PROTO: NOW_VARI16 -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct VarI16(i16); - -impl VarI16 { - pub const MIN: i16 = -0x3FFF; - pub const MAX: i16 = 0x3FFF; - - const NAME: &'static str = "NOW_VARI16"; - - pub fn new(value: i16) -> DecodeResult { - if value.abs() > Self::MAX { - return Err(invalid_field_err!("value", "too large number")); - } - - Ok(VarI16(value)) - } - - pub fn value(&self) -> i16 { - self.0 - } -} - -impl Encode for VarI16 { - fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { - let encoded_size = self.size(); - - ensure_size!(in: dst, size: encoded_size); - - // LINTS: encoded_size will always be 1 or 2, therefore following arithmetic is safe - #[allow(clippy::arithmetic_side_effects)] - let mut shift = (encoded_size - 1) * 8; - let mut bytes = [0u8; 2]; - - let abs_value = self.0.unsigned_abs(); - - for byte in bytes.iter_mut().take(encoded_size) { - *byte = ((abs_value >> shift) & 0xFF).try_into().expect("always <= 0xFF"); - - // LINTS: as per code above, shift is always 8 or 16 - #[allow(clippy::arithmetic_side_effects)] - if shift != 0 { - shift -= 8; - } - } - - // LINTS: encoded_size is always >= 1 - #[allow(clippy::arithmetic_side_effects)] - let c: u8 = (encoded_size - 1).try_into().expect("always fits into u8"); - bytes[0] |= c << 7; - if self.0 < 0 { - // set sign bit - bytes[0] |= 0x40; - } - - dst.write_slice(&bytes[..encoded_size]); - - Ok(()) - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn size(&self) -> usize { - match self.0.unsigned_abs() { - 0..=0x3F => 1, - 0x40..=0x3FFF => 2, - _ => unreachable!("BUG: value is out of range!"), - } - } -} - -impl Decode<'_> for VarI16 { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { - // Ensure we have at least 1 byte available to determine the size of the value - ensure_size!(in: src, size: 1); - - let header = src.read_u8(); - let c: usize = ((header >> 7) & 0x01).into(); - let is_negative = (header & 0x40) != 0; - - if c == 0 { - let val = i16::from(header & 0x3F); - // LINTS: Variable integer range is always smaller than underlying type range, - // therefore negation is always safe - #[allow(clippy::arithmetic_side_effects)] - return Ok(VarI16(if is_negative { -val } else { val })); - } - - ensure_size!(in: src, size: c); - let bytes = src.read_slice(c); - - let val1 = header & 0x3F; - - // LINTS: c is always 1 or 2 - #[allow(clippy::arithmetic_side_effects)] - let mut shift = c * 8; - let mut num = i16::from(val1) << shift; - - // Read val2..valN - // LINTS: shift is always 8 or 16 - #[allow(clippy::arithmetic_side_effects)] - for val in bytes.iter().take(c) { - shift -= 8; - num |= (i16::from(*val)) << shift; - } - - // LINTS: Variable integer range is always smaller than underlying type range, - // therefore negation is always safe - #[allow(clippy::arithmetic_side_effects)] - Ok(VarI16(if is_negative { -num } else { num })) - } -} - -impl From for i16 { - fn from(value: VarI16) -> Self { - value.value() - } -} - -impl TryFrom for VarI16 { - type Error = DecodeError; - - fn try_from(value: i16) -> Result { - Self::new(value) - } -} - /// Variable-length encoded u32. /// Value range: `[0..0x3FFFFFFF]` /// @@ -372,395 +122,3 @@ impl TryFrom for VarU32 { Self::new(value) } } - -/// Variable-length encoded i32. -/// Value range: `[-0x1FFFFFFF..0x1FFFFFFF]` -/// -/// NOW-PROTO: NOW_VARI32 -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct VarI32(i32); - -impl VarI32 { - pub const MIN: i32 = -0x1FFFFFFF; - pub const MAX: i32 = 0x1FFFFFFF; - - const NAME: &'static str = "NOW_VARI32"; - - pub fn new(value: i32) -> DecodeResult { - if value.abs() > Self::MAX { - return Err(invalid_field_err!("value", "too large number")); - } - - Ok(VarI32(value)) - } - - pub fn value(&self) -> i32 { - self.0 - } -} - -impl Encode for VarI32 { - fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { - let encoded_size = self.size(); - - ensure_size!(in: dst, size: encoded_size); - - // LINTS: encoded_size will always be [1..4], therefore following arithmetic is safe - #[allow(clippy::arithmetic_side_effects)] - let mut shift = (encoded_size - 1) * 8; - let mut bytes = [0u8; 4]; - - let abs_value = self.0.unsigned_abs(); - - for byte in bytes.iter_mut().take(encoded_size) { - *byte = ((abs_value >> shift) & 0xFF).try_into().expect("always <= 0xFF"); - - // LINTS: as per code above, shift is always 8, 16, 24 - #[allow(clippy::arithmetic_side_effects)] - if shift != 0 { - shift -= 8; - } - } - - // LINTS: encoded_size is always >= 1 - #[allow(clippy::arithmetic_side_effects)] - let c: u8 = (encoded_size - 1).try_into().expect("always fits into u8"); - bytes[0] |= c << 6; - if self.0 < 0 { - // set sign bit - bytes[0] |= 0x20; - } - - dst.write_slice(&bytes[..encoded_size]); - - Ok(()) - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn size(&self) -> usize { - match self.0.unsigned_abs() { - 0..=0x1F => 1, - 0x20..=0x1FFF => 2, - 0x2000..=0x1FFFFF => 3, - 0x200000..=0x1FFFFFFF => 4, - _ => unreachable!("BUG: value is out of range!"), - } - } -} - -impl Decode<'_> for VarI32 { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { - // Ensure we have at least 1 byte available to determine the size of the value - ensure_size!(in: src, size: 1); - - let header = src.read_u8(); - let c: usize = ((header >> 6) & 0x03).into(); - let is_negative = (header & 0x20) != 0; - - if c == 0 { - let val = i32::from(header & 0x1F); - // LINTS: Variable integer range is always smaller than underlying type range, - // therefore negation is always safe - #[allow(clippy::arithmetic_side_effects)] - return Ok(VarI32(if is_negative { -val } else { val })); - } - - ensure_size!(in: src, size: c); - let bytes = src.read_slice(c); - - let val1 = header & 0x1F; - - // LINTS: c is always [1..4] - #[allow(clippy::arithmetic_side_effects)] - let mut shift = c * 8; - let mut num = i32::from(val1) << shift; - - // Read val2..valN - // LINTS: shift is always 8, 16, 24 - #[allow(clippy::arithmetic_side_effects)] - for val in bytes.iter().take(c) { - shift -= 8; - num |= (i32::from(*val)) << shift; - } - - // LINTS: Variable integer range is always smaller than underlying type range, - // therefore negation is always safe - #[allow(clippy::arithmetic_side_effects)] - Ok(VarI32(if is_negative { -num } else { num })) - } -} - -impl From for i32 { - fn from(value: VarI32) -> Self { - value.value() - } -} - -impl TryFrom for VarI32 { - type Error = DecodeError; - - fn try_from(value: i32) -> Result { - Self::new(value) - } -} - -/// Variable-length encoded u64. -/// Value range: `[0..0x1FFFFFFFFFFFFFFF]` -/// -/// NOW-PROTO: NOW_VARU64 -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct VarU64(u64); - -impl VarU64 { - pub const MIX: u64 = 0x0000000000000000; - pub const MAX: u64 = 0x1FFFFFFFFFFFFFFF; - - const NAME: &'static str = "NOW_VARU64"; - - pub fn new(value: u64) -> DecodeResult { - if value > Self::MAX { - return Err(invalid_field_err!("value", "too large number")); - } - - Ok(VarU64(value)) - } - - pub fn value(&self) -> u64 { - self.0 - } -} - -impl Encode for VarU64 { - fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { - let encoded_size = self.size(); - - ensure_size!(in: dst, size: encoded_size); - - // LINTS: encoded_size will always be [1..8], therefore following arithmetic is safe - #[allow(clippy::arithmetic_side_effects)] - let mut shift = (encoded_size - 1) * 8; - let mut bytes = [0u8; 8]; - - for byte in bytes.iter_mut().take(encoded_size) { - *byte = ((self.0 >> shift) & 0xFF).try_into().expect("always <= 0xFF"); - - // LINTS: as per code above, shift is always >= 8 - #[allow(clippy::arithmetic_side_effects)] - if shift != 0 { - shift -= 8; - } - } - - // LINTS: encoded_size is always >= 1 - #[allow(clippy::arithmetic_side_effects)] - let c: u8 = (encoded_size - 1).try_into().expect("always fits into u8"); - bytes[0] |= c << 5; - - dst.write_slice(&bytes[..encoded_size]); - - Ok(()) - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn size(&self) -> usize { - match self.0 { - 0x00..=0x1F => 1, - 0x20..=0x1FFF => 2, - 0x2000..=0x1FFFFF => 3, - 0x200000..=0x1FFFFFFF => 4, - 0x20000000..=0x1FFFFFFFFF => 5, - 0x2000000000..=0x1FFFFFFFFFFF => 6, - 0x200000000000..=0x1FFFFFFFFFFFFF => 7, - 0x20000000000000..=0x1FFFFFFFFFFFFFFF => 8, - _ => unreachable!("BUG: value is out of range!"), - } - } -} - -impl Decode<'_> for VarU64 { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { - // Ensure we have at least 1 byte available to determine the size of the value - ensure_size!(in: src, size: 1); - - let header = src.read_u8(); - let c: usize = ((header >> 5) & 0x07).into(); - - if c == 0 { - return Ok(VarU64((header & 0x1F).into())); - } - - ensure_size!(in: src, size: c); - let bytes = src.read_slice(c); - - let val1 = header & 0x1F; - // LINTS: c is always [1..8] - #[allow(clippy::arithmetic_side_effects)] - let mut shift = c * 8; - let mut num = u64::from(val1) << shift; - - // Read val2..valN - // LINTS: shift is always >= 8 - #[allow(clippy::arithmetic_side_effects)] - for val in bytes.iter().take(c) { - shift -= 8; - num |= (u64::from(*val)) << shift; - } - - Ok(VarU64(num)) - } -} - -impl From for u64 { - fn from(value: VarU64) -> Self { - value.value() - } -} - -impl TryFrom for VarU64 { - type Error = DecodeError; - - fn try_from(value: u64) -> Result { - Self::new(value) - } -} - -/// Variable-length encoded i64. -/// Value range: `[-0x0FFFFFFFFFFFFFFF..0x0FFFFFFFFFFFFFFF]` -/// -/// NOW-PROTO: NOW_VARI64 -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct VarI64(i64); - -impl VarI64 { - const NAME: &'static str = "NOW_VARI64"; - const MAX: i64 = 0x0FFFFFFFFFFFFFFF; - - pub fn new(value: i64) -> DecodeResult { - if value.abs() > Self::MAX { - return Err(invalid_field_err!("value", "too large number")); - } - - Ok(VarI64(value)) - } - - pub fn value(&self) -> i64 { - self.0 - } -} - -impl Encode for VarI64 { - fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { - let encoded_size = self.size(); - - ensure_size!(in: dst, size: encoded_size); - - // LINTS: encoded_size will always be [1..8], therefore following arithmetic is safe - #[allow(clippy::arithmetic_side_effects)] - let mut shift = (encoded_size - 1) * 8; - let mut bytes = [0u8; 8]; - - let abs_value = self.0.unsigned_abs(); - - for byte in bytes.iter_mut().take(encoded_size) { - *byte = ((abs_value >> shift) & 0xFF).try_into().expect("always <= 0xFF"); - - // LINTS: as per code above, shift is always >= 8 - #[allow(clippy::arithmetic_side_effects)] - if shift != 0 { - shift -= 8; - } - } - - // LINTS: encoded_size is always >= 1 - #[allow(clippy::arithmetic_side_effects)] - let c: u8 = (encoded_size - 1).try_into().expect("always fits into u8"); - bytes[0] |= c << 5; - if self.0 < 0 { - // set sign bit - bytes[0] |= 0x10; - } - - dst.write_slice(&bytes[..encoded_size]); - - Ok(()) - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn size(&self) -> usize { - match self.0.unsigned_abs() { - 0..=0x0F => 1, - 0x10..=0x0FFF => 2, - 0x1000..=0x0FFFFF => 3, - 0x100000..=0x0FFFFFFF => 4, - 0x10000000..=0x0FFFFFFFFF => 5, - 0x1000000000..=0x0FFFFFFFFFFF => 6, - 0x100000000000..=0x0FFFFFFFFFFFFF => 7, - 0x10000000000000..=0x0FFFFFFFFFFFFFFF => 8, - _ => unreachable!("BUG: value is out of range!"), - } - } -} - -impl Decode<'_> for VarI64 { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { - // Ensure we have at least 1 byte available to determine the size of the value - ensure_size!(in: src, size: 1); - - let header = src.read_u8(); - let c: usize = ((header >> 5) & 0x07).into(); - let is_negative = (header & 0x10) != 0; - - if c == 0 { - let val = i64::from(header & 0x0F); - // LINTS: Variable integer range is always smaller than underlying type range, - // therefore negation is always safe - #[allow(clippy::arithmetic_side_effects)] - return Ok(VarI64(if is_negative { -val } else { val })); - } - - ensure_size!(in: src, size: c); - let bytes = src.read_slice(c); - - let val1 = header & 0x0F; - // LINTS: c is always [1..8] - #[allow(clippy::arithmetic_side_effects)] - let mut shift = c * 8; - let mut num = i64::from(val1) << shift; - - // Read val2..valN - // LINTS: shift is always >= 8 - #[allow(clippy::arithmetic_side_effects)] - for val in bytes.iter().take(c) { - shift -= 8; - num |= (i64::from(*val)) << shift; - } - - // LINTS: Variable integer range is always smaller than underlying type range, - // therefore negation is always safe - #[allow(clippy::arithmetic_side_effects)] - Ok(VarI64(if is_negative { -num } else { num })) - } -} - -impl From for i64 { - fn from(value: VarI64) -> Self { - value.value() - } -} - -impl TryFrom for VarI64 { - type Error = DecodeError; - - fn try_from(value: i64) -> Result { - Self::new(value) - } -} diff --git a/crates/now-proto-pdu/src/core/status.rs b/crates/now-proto-pdu/src/core/status.rs index 8231719..cb2e142 100644 --- a/crates/now-proto-pdu/src/core/status.rs +++ b/crates/now-proto-pdu/src/core/status.rs @@ -1,113 +1,381 @@ +use alloc::borrow::Cow; +use alloc::fmt; +use core::ops::Deref; + +use bitflags::bitflags; use ironrdp_core::{ - ensure_fixed_part_size, invalid_field_err, Decode, DecodeError, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor, }; -/// Error or status severity. +use crate::NowVarStr; + +bitflags! { + /// NOW-PROTO: NOW_STATUS flags field. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct NowStatusFlags: u16 { + /// This flag set for all error statuses. If flag is not set, operation was successful. + /// + /// NOW-PROTO: NOW_STATUS_ERROR + const ERROR = 0x0001; + /// Set if `errorMessage` contains optional error message. + /// + /// NOW-PROTO: NOW_STATUS_ERROR_MESSAGE + const ERROR_MESSAGE = 0x0002; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct RawNowStatusKind(pub u16); + +impl RawNowStatusKind { + const GENERIC: Self = Self(0x0000); + const NOW: Self = Self(0x0001); + const WINAPI: Self = Self(0x0002); + const UNIX: Self = Self(0x0003); +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum NowSeverity { - /// Informative status +struct RawNowProtoError(pub u32); + +impl RawNowProtoError { + const IN_USE: Self = Self(0x0001); + const INVALID_REQUEST: Self = Self(0x0002); + const ABORTED: Self = Self(0x0003); + const NOT_FOUND: Self = Self(0x0004); + const ACCESS_DENIED: Self = Self(0x0005); + const INTERNAL: Self = Self(0x0006); + const NOT_IMPLEMENTED: Self = Self(0x0007); + const PROTOCOL_VERSION: Self = Self(0x0008); +} + +/// `code` field value of `NOW_STATUS` message if `kind` is `NOW_STATUS_ERROR_KIND_NOW`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NowProtoError { + /// Resource (e.g. exec session id is already in use). + /// + /// NOW-PROTO: NOW_CODE_IN_USE + InUse, + /// Sent request is invalid (e.g. invalid exec request params). /// - /// NOW-PROTO: NOW_SEVERITY_INFO - Info = 0, - /// Warning status + /// NOW-PROTO: NOW_CODE_INVALID_REQUEST + InvalidRequest, + /// Operation has been aborted on the server side. /// - /// NOW-PROTO: NOW_SEVERITY_WARN - Warn = 1, - /// Error status (recoverable) + /// NOW-PROTO: NOW_CODE_ABORTED + Aborted, + /// Resource not found. /// - /// NOW-PROTO: NOW_SEVERITY_ERROR - Error = 2, - /// Error status (non-recoverable) + /// NOW-PROTO: NOW_CODE_NOT_FOUND + NotFound, + /// Resource can't be accessed. /// - /// NOW-PROTO: NOW_SEVERITY_FATAL - Fatal = 3, + /// NOW-PROTO: NOW_CODE_ACCESS_DENIED + AccessDenied, + /// Internal error. + /// + /// NOW-PROTO: NOW_CODE_INTERNAL + Internal, + /// Operation is not implemented on current platform. + /// + /// NOW-PROTO: NOW_CODE_NOT_IMPLEMENTED + NotImplemented, + /// Incompatible protocol versions. + /// + /// NOW-PROTO: NOW_CODE_PROTOCOL_VERSION + ProtocolVersion, + /// Other error code. + Other(u32), } -impl TryFrom for NowSeverity { - type Error = DecodeError; +impl From for NowProtoError { + fn from(code: RawNowProtoError) -> Self { + match code { + RawNowProtoError::IN_USE => NowProtoError::InUse, + RawNowProtoError::INVALID_REQUEST => NowProtoError::InvalidRequest, + RawNowProtoError::ABORTED => NowProtoError::Aborted, + RawNowProtoError::NOT_FOUND => NowProtoError::NotFound, + RawNowProtoError::ACCESS_DENIED => NowProtoError::AccessDenied, + RawNowProtoError::INTERNAL => NowProtoError::Internal, + RawNowProtoError::NOT_IMPLEMENTED => NowProtoError::NotImplemented, + RawNowProtoError::PROTOCOL_VERSION => NowProtoError::ProtocolVersion, + RawNowProtoError(code) => NowProtoError::Other(code), + } + } +} - fn try_from(value: u8) -> DecodeResult { - match value { - 0 => Ok(Self::Info), - 1 => Ok(Self::Warn), - 2 => Ok(Self::Error), - 3 => Ok(Self::Fatal), - _ => Err(invalid_field_err!("severity", "invalid value")), +impl From for RawNowProtoError { + fn from(err: NowProtoError) -> Self { + match err { + NowProtoError::InUse => RawNowProtoError::IN_USE, + NowProtoError::InvalidRequest => RawNowProtoError::INVALID_REQUEST, + NowProtoError::Aborted => RawNowProtoError::ABORTED, + NowProtoError::NotFound => RawNowProtoError::NOT_FOUND, + NowProtoError::AccessDenied => RawNowProtoError::ACCESS_DENIED, + NowProtoError::Internal => RawNowProtoError::INTERNAL, + NowProtoError::NotImplemented => RawNowProtoError::NOT_IMPLEMENTED, + NowProtoError::ProtocolVersion => RawNowProtoError::PROTOCOL_VERSION, + NowProtoError::Other(code) => RawNowProtoError(code), } } } -/// Error or status code. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct NowStatusCode(pub u16); - -impl NowStatusCode { - /// NOW-PROTO: NOW_CODE_SUCCESS - pub const SUCCESS: Self = Self(0x0000); - /// NOW-PROTO: NOW_CODE_FAILURE - pub const FAILURE: Self = Self(0xFFFF); - /// NOW-PROTO: NOW_CODE_FILE_NOT_FOUND - pub const FILE_NOT_FOUND: Self = Self(0x0002); - /// NOW-PROTO: NOW_CODE_ACCESS_DENIED - pub const ACCESS_DENIED: Self = Self(0x0005); - /// NOW-PROTO: NOW_CODE_BAD_FORMAT - pub const BAD_FORMAT: Self = Self(0x000B); +impl fmt::Display for NowProtoError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NowProtoError::InUse => write!(f, "resource is already in use"), + NowProtoError::InvalidRequest => write!(f, "invalid request"), + NowProtoError::Aborted => write!(f, "operation has been aborted"), + NowProtoError::NotFound => write!(f, "resource not found"), + NowProtoError::AccessDenied => write!(f, "access denied"), + NowProtoError::Internal => write!(f, "internal error"), + NowProtoError::NotImplemented => write!(f, "operation is not implemented"), + NowProtoError::ProtocolVersion => write!(f, "incompatible protocol versions"), + NowProtoError::Other(code) => write!(f, "unknown error code {}", code), + } + } } -/// A status code, with a structure similar to HRESULT. + +/// Mapped NOW_STATUS error kinds with their respective error codes inside enum variants represented +/// as rust enum for convenient error handling. /// -/// NOW-PROTO: NOW_STATUS -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowStatus { - severity: NowSeverity, - kind: u8, - code: NowStatusCode, +/// Converted internally by the library to/from `kind` and `code` fields of `NOW_STATUS` message. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NowStatusErrorKind { + /// `code` value is undefined and could be ignored. + /// + /// NOW-PROTO: NOW_STATUS_ERROR_KIND_GENERIC + Generic(u32), + /// `code` contains NowProto-defined error code (see `NOW_STATUS_ERROR_KIND_NOW`). + /// + /// NOW-PROTO: NOW_STATUS_ERROR_KIND_NOW + Now(NowProtoError), + /// `code` field contains Windows error code. + /// + /// NOW-PROTO: NOW_STATUS_ERROR_KIND_WINAPI + WinApi(u32), + /// `code` field contains Unix error code. + /// + /// NOW-PROTO: NOW_STATUS_ERROR_KIND_UNIX + Unix(u32), + /// Unknown error kind. + Unknown { kind: u16, code: u32 }, } -impl NowStatus { - const NAME: &'static str = "NOW_STATUS"; - const FIXED_PART_SIZE: usize = 4; +impl From for NowStatusErrorKind { + fn from(err: NowProtoError) -> Self { + NowStatusErrorKind::Now(err) + } +} - pub fn new(severity: NowSeverity, code: NowStatusCode) -> Self { +impl fmt::Display for NowStatusErrorKind { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + NowStatusErrorKind::Generic(code) => write!(f, "generic error code {}", code), + NowStatusErrorKind::Now(error) => { + write!(f, "NOW-proto error: ")?; + error.fmt(f) + } + NowStatusErrorKind::WinApi(code) => write!(f, "WinAPI error code {}", code), + NowStatusErrorKind::Unix(code) => write!(f, "Unix error code {}", code), + NowStatusErrorKind::Unknown { kind, code } => { + write!(f, "unknown error: kind({}), code({})", kind, code) + } + } + } +} + +impl NowStatusErrorKind { + fn status_kind(&self) -> RawNowStatusKind { + match self { + NowStatusErrorKind::Generic(_) => RawNowStatusKind::GENERIC, + NowStatusErrorKind::Now(_) => RawNowStatusKind::NOW, + NowStatusErrorKind::WinApi(_) => RawNowStatusKind::WINAPI, + NowStatusErrorKind::Unix(_) => RawNowStatusKind::UNIX, + NowStatusErrorKind::Unknown { kind, .. } => RawNowStatusKind(*kind), + } + } + + fn status_code(&self) -> u32 { + match self { + NowStatusErrorKind::Generic(code) => *code, + NowStatusErrorKind::Now(error) => RawNowProtoError::from(*error).0, + NowStatusErrorKind::WinApi(code) => *code, + NowStatusErrorKind::Unix(code) => *code, + NowStatusErrorKind::Unknown { code, .. } => *code, + } + } + + fn from_parts(kind: u16, code: u32) -> Self { + match RawNowStatusKind(kind) { + RawNowStatusKind::GENERIC => NowStatusErrorKind::Generic(code), + RawNowStatusKind::NOW => NowStatusErrorKind::Now(RawNowProtoError(code).into()), + RawNowStatusKind::WINAPI => NowStatusErrorKind::WinApi(code), + RawNowStatusKind::UNIX => NowStatusErrorKind::Unix(code), + _ => NowStatusErrorKind::Unknown { kind, code }, + } + } +} + +/// Wrapper type around NOW_STATUS errors. Provides rust-friendly interface for error handling. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NowStatusError { + kind: NowStatusErrorKind, + message: NowVarStr<'static>, +} + +impl NowStatusError { + pub fn new_generic(code: u32) -> Self { Self { - severity, - kind: 0, - code, + kind: NowStatusErrorKind::Generic(code), + message: Default::default(), } } - pub fn with_kind(self, kind: u8) -> DecodeResult { - if kind > 0x0F { - return Err(invalid_field_err!("type", "status type is too large")); + pub fn new_proto(error: NowProtoError) -> Self { + Self { + kind: NowStatusErrorKind::Now(error), + message: Default::default(), } + } - Ok(Self { kind, ..self }) + pub fn new_winapi(code: u32) -> Self { + Self { + kind: NowStatusErrorKind::WinApi(code), + message: Default::default(), + } } - pub fn severity(&self) -> NowSeverity { - self.severity + pub fn new_unix(code: u32) -> Self { + Self { + kind: NowStatusErrorKind::Unix(code), + message: Default::default(), + } } - pub fn kind(&self) -> u8 { + pub fn kind(&self) -> NowStatusErrorKind { self.kind } - pub fn code(&self) -> NowStatusCode { - self.code + pub fn message(&self) -> &str { + &self.message + } + + /// Attach optional message to NOW_STATUS error. + pub fn with_message(self, message: impl Into>) -> EncodeResult { + Ok(Self { + kind: self.kind, + message: NowVarStr::new(message)?, + }) + } +} + +impl core::fmt::Display for NowStatusError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + fmt::Display::fmt(&self.kind, f)?; + + // Write optional message if provided. + if !self.message.is_empty() { + write!(f, " ({})", self.message.deref())?; + } + + Ok(()) + } +} + +impl From for NowStatusError { + fn from(kind: NowStatusErrorKind) -> Self { + Self { + kind, + message: Default::default(), + } + } +} + +impl From for NowStatusError { + fn from(err: NowProtoError) -> Self { + NowStatusErrorKind::from(err).into() } } -impl Encode for NowStatus { +impl core::error::Error for NowStatusError {} + +/// Channel or session operation status. +/// +/// NOW-PROTO: NOW_STATUS +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NowStatus<'a> { + flags: NowStatusFlags, + kind: RawNowStatusKind, + code: u32, + message: NowVarStr<'a>, +} + +impl IntoOwned for NowStatus<'_> { + type Owned = NowStatus<'static>; + + fn into_owned(self) -> Self::Owned { + Self::Owned { + flags: self.flags, + kind: self.kind, + code: self.code, + message: self.message.into_owned(), + } + } +} + +impl NowStatus<'_> { + const NAME: &'static str = "NOW_STATUS"; + const FIXED_PART_SIZE: usize = 8; + + /// Create a new success status. + pub(crate) fn new_success() -> Self { + Self { + flags: NowStatusFlags::empty(), + kind: RawNowStatusKind::GENERIC, + code: 0, + message: Default::default(), + } + } + + /// Create a new status with error. + pub(crate) fn new_error(error: impl Into) -> Self { + let error: NowStatusError = error.into(); + + let flags = if error.message.is_empty() { + NowStatusFlags::ERROR + } else { + NowStatusFlags::ERROR | NowStatusFlags::ERROR_MESSAGE + }; + + Self { + flags, + kind: error.kind.status_kind(), + code: error.kind.status_code(), + message: error.message, + } + } + + /// Convert status to result with 'static error. + pub(crate) fn to_result(&self) -> Result<(), NowStatusError> { + if !self.flags.contains(NowStatusFlags::ERROR) { + return Ok(()); + } + + Err(NowStatusError { + kind: NowStatusErrorKind::from_parts(self.kind.0, self.code), + message: self.message.clone().into_owned(), + }) + } +} + +impl Encode for NowStatus<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { ensure_fixed_part_size!(in: dst); + dst.write_u16(self.flags.bits()); + dst.write_u16(self.kind.0); + dst.write_u32(self.code); - // Y, Z, class fields are reserved and must be set to 0. - let header_byte = (self.severity as u8) << 6; - - dst.write_u8(header_byte); - dst.write_u8(self.kind); - dst.write_u16(self.code.0); + self.message.encode(dst)?; Ok(()) } @@ -117,23 +385,24 @@ impl Encode for NowStatus { } fn size(&self) -> usize { - Self::FIXED_PART_SIZE + Self::FIXED_PART_SIZE + self.message.size() } } -impl Decode<'_> for NowStatus { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowStatus<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { ensure_fixed_part_size!(in: src); + let flags = NowStatusFlags::from_bits_retain(src.read_u16()); + let kind = RawNowStatusKind(src.read_u16()); + let code = src.read_u32(); - let header_byte = src.read_u8(); - let severity = (header_byte >> 6) & 0x03; - let kind = src.read_u8(); - let code = src.read_u16(); + let message = NowVarStr::decode(src)?; Ok(NowStatus { - severity: NowSeverity::try_from(severity)?, + flags, kind, - code: NowStatusCode(code), + code, + message, }) } } diff --git a/crates/now-proto-pdu/src/core/string.rs b/crates/now-proto-pdu/src/core/string.rs index c2ac164..74e4f65 100644 --- a/crates/now-proto-pdu/src/core/string.rs +++ b/crates/now-proto-pdu/src/core/string.rs @@ -1,132 +1,35 @@ //! String types -use alloc::string::String; +use alloc::borrow::Cow; +use core::ops::Deref; +use core::str; use ironrdp_core::{ - cast_length, ensure_fixed_part_size, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, - ReadCursor, WriteCursor, + cast_length, ensure_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, + WriteCursor, }; use crate::VarU32; -/// String value up to 2^32 bytes long. +/// String value up to 2^30 bytes long (Length has compact variable length encoding). /// -/// NOW-PROTO: NOW_LRGSTR -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowLrgStr(String); - -impl NowLrgStr { - pub const MAX_SIZE: usize = u32::MAX as usize; - - const NAME: &'static str = "NOW_LRGSTR"; - const FIXED_PART_SIZE: usize = 4; - - /// Returns empty string. - pub fn empty() -> Self { - Self(String::new()) - } - - /// Creates new `NowLrgStr`. Returns error if string is too big for the protocol. - pub fn new(value: impl Into) -> DecodeResult { - let value: String = value.into(); - // IMPORTANT: we need to check for encoded UTF-8 size, not the string length. - - Self::ensure_message_size(value.as_bytes().len())?; - - Ok(NowLrgStr(value)) - } - - pub fn value(&self) -> &str { - &self.0 - } - - fn ensure_message_size(string_size: usize) -> DecodeResult<()> { - if string_size > usize::try_from(VarU32::MAX).expect("BUG: too small usize") { - return Err(invalid_field_err!("data", "data is too large for NOW_LRGSTR")); - } - - if string_size > usize::MAX - Self::FIXED_PART_SIZE - 1 { - return Err(invalid_field_err!( - "string", - "string size is too large to fit in 32-bit usize" - )); - } - - Ok(()) - } -} - -impl Encode for NowLrgStr { - fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { - let encoded_size = self.size(); - ensure_size!(in: dst, size: encoded_size); - - let len: u32 = self.0.len().try_into().expect("BUG: validated in constructor"); - - dst.write_u32(len); - dst.write_slice(self.0.as_bytes()); - dst.write_u8(b'\0'); - - Ok(()) - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn size(&self) -> usize { - // + + - self.0 - .len() - .checked_add(Self::FIXED_PART_SIZE + 1) - .expect("BUG: size overflow") - } -} - -impl Decode<'_> for NowLrgStr { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { - ensure_fixed_part_size!(in: src); - - let len: usize = cast_length!("len", src.read_u32())?; - - Self::ensure_message_size(len)?; - - ensure_size!(in: src, size: len); - let bytes = src.read_slice(len); - ensure_size!(in: src, size: 1); - let _null = src.read_u8(); - - let string = - String::from_utf8(bytes.to_vec()).map_err(|_| invalid_field_err!("string value", "invalid utf-8"))?; +/// NOW-PROTO: NOW_VARSTR +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub(crate) struct NowVarStr<'a>(Cow<'a, str>); - Ok(NowLrgStr(string)) - } -} +impl IntoOwned for NowVarStr<'_> { + type Owned = NowVarStr<'static>; -impl From for String { - fn from(value: NowLrgStr) -> Self { - value.0 + fn into_owned(self) -> Self::Owned { + NowVarStr(Cow::Owned(self.0.into_owned())) } } -/// String value up to 2^31 bytes long (Length has compact variable length encoding). -/// -/// NOW-PROTO: NOW_VARSTR -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowVarStr(String); - -impl NowVarStr { - pub const MAX_SIZE: usize = VarU32::MAX as usize; - +impl<'a> NowVarStr<'a> { const NAME: &'static str = "NOW_VARSTR"; - /// Returns empty string. - pub fn empty() -> Self { - Self(String::new()) - } - /// Creates `NowVarStr` from std string. Returns error if string is too big for the protocol. - pub fn new(value: impl Into) -> EncodeResult { + pub(crate) fn new(value: impl Into>) -> EncodeResult { let value = value.into(); // IMPORTANT: we need to check for encoded UTF-8 size, not the string length. @@ -140,13 +43,9 @@ impl NowVarStr { Ok(NowVarStr(value)) } - - pub fn value(&self) -> &str { - &self.0 - } } -impl Encode for NowVarStr { +impl Encode for NowVarStr<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let encoded_size = self.size(); ensure_size!(in: dst, size: encoded_size); @@ -175,8 +74,8 @@ impl Encode for NowVarStr { } } -impl Decode<'_> for NowVarStr { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowVarStr<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let len_u32 = VarU32::decode(src)?.value(); let len: usize = cast_length!("len", len_u32)?; @@ -185,138 +84,21 @@ impl Decode<'_> for NowVarStr { ensure_size!(in: src, size: 1); let _null = src.read_u8(); - let string = - String::from_utf8(bytes.to_vec()).map_err(|_| invalid_field_err!("string value", "invalid utf-8"))?; - - Ok(NowVarStr(string)) - } -} - -impl From for String { - fn from(value: NowVarStr) -> Self { - value.0 - } -} - -const fn restricted_str_name(str_len: u8) -> &'static str { - match str_len { - 15 => "NOW_STRING16", - 31 => "NOW_STRING32", - 63 => "NOW_STRING64", - 127 => "NOW_STRING128", - 255 => "NOW_STRING256", - _ => panic!("BUG: Requested restricted string variant is not defined in the protocol"), - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowRestrictedStr(String); - -impl NowRestrictedStr { - pub const MAX_ENCODED_UTF8_LEN: usize = MAX_LEN as usize; - - const NAME: &'static str = restricted_str_name(MAX_LEN); - const FIXED_PART_SIZE: usize = 1; - - /// Returns empty string. - pub fn empty() -> Self { - Self(String::new()) - } - - /// Creates `NowRestrictedStr` from std string. Returns error if string is too big for the protocol. - pub fn new(value: impl Into) -> EncodeResult { - let value = value.into(); - - // IMPORTANT: we need to check for encoded UTF-8 size, not the string length - if value.as_bytes().len() > MAX_LEN as usize { - return Err(invalid_field_err!("string value", concat!("too large string"))); + // Avoid owned vector allocation for empty strings. + if bytes.is_empty() { + return Ok(NowVarStr::default()); } - Ok(NowRestrictedStr(value)) - } - pub fn value(&self) -> &str { - &self.0 - } -} + let borrowed = str::from_utf8(bytes).map_err(|_| invalid_field_err!("string value", "invalid utf-8"))?; -impl Encode for NowRestrictedStr { - fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { - let encoded_size = self.size(); - ensure_size!(in: dst, size: encoded_size); - - let len: u8 = self.0.len().try_into().expect("BUG: validated in constructor"); - - dst.write_u8(len); - dst.write_slice(self.0.as_bytes()); - dst.write_u8(b'\0'); - - Ok(()) - } - - fn name(&self) -> &'static str { - Self::NAME - } - - // LINTS: Restricted string with u8 length ensures that the overall size value is within - // the bounds of usize. - #[allow(clippy::arithmetic_side_effects)] - fn size(&self) -> usize { - Self::FIXED_PART_SIZE /* u8 size */ - + self.0.len() /* utf-8 bytes */ - + 1 /* null terminator */ + Ok(NowVarStr(borrowed.into())) } } -impl Decode<'_> for NowRestrictedStr { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { - ensure_fixed_part_size!(in: src); - - let len = src.read_u8(); - if len > MAX_LEN { - return Err(invalid_field_err!("string value", "too large string")); - } - - let len_usize = len.into(); - - ensure_size!(in: src, size: len_usize); - let bytes = src.read_slice(len_usize); - ensure_size!(in: src, size: 1); - let _null = src.read_u8(); - - let string = - String::from_utf8(bytes.to_vec()).map_err(|_| invalid_field_err!("string value", "invalid utf-8"))?; - - Ok(NowRestrictedStr(string)) - } -} +impl Deref for NowVarStr<'_> { + type Target = str; -impl From> for String { - fn from(value: NowRestrictedStr) -> Self { - value.0 + fn deref(&self) -> &Self::Target { + &self.0 } } - -/// String value up to 16 bytes long. -/// -/// NOW-PROTO: NOW_STRING16 -pub type NowString16 = NowRestrictedStr<15>; - -/// String value up to 32 bytes long. -/// -/// NOW-PROTO: NOW_STRING32 -pub type NowString32 = NowRestrictedStr<31>; - -/// String value up to 64 bytes long. -/// -/// NOW-PROTO: NOW_STRING64 -pub type NowString64 = NowRestrictedStr<63>; - -/// String value up to 128 bytes long. -/// -/// NOW-PROTO: NOW_STRING128 -pub type NowString128 = NowRestrictedStr<127>; - -/// String value up to 256 bytes long. -/// -/// NOW-PROTO: NOW_STRING256 -pub type NowString256 = NowRestrictedStr<255>; diff --git a/crates/now-proto-pdu/src/exec/abort.rs b/crates/now-proto-pdu/src/exec/abort.rs index 75a5986..3ab4b57 100644 --- a/crates/now-proto-pdu/src/exec/abort.rs +++ b/crates/now-proto-pdu/src/exec/abort.rs @@ -3,7 +3,7 @@ use ironrdp_core::{ WriteCursor, }; -use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowStatus}; +use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass}; /// The NOW_EXEC_ABORT_MSG message is used to abort a remote execution immediately due to an /// unrecoverable error. This message can be sent at any time without an explicit response message. @@ -13,45 +13,39 @@ use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageCla #[derive(Debug, Clone, PartialEq, Eq)] pub struct NowExecAbortMsg { session_id: u32, - status: NowStatus, + exit_code: u32, } impl NowExecAbortMsg { const NAME: &'static str = "NOW_EXEC_ABORT_MSG"; - const FIXED_PART_SIZE: usize = 4; + const FIXED_PART_SIZE: usize = 8; - pub fn new(session_id: u32, status: NowStatus) -> Self { - Self { session_id, status } + pub fn new(session_id: u32, exit_code: u32) -> Self { + Self { session_id, exit_code } } pub fn session_id(&self) -> u32 { self.session_id } - pub fn status(&self) -> &NowStatus { - &self.status - } - - // LINTS: Overall message size always fits into usize - #[allow(clippy::arithmetic_side_effects)] - fn body_size(&self) -> usize { - Self::FIXED_PART_SIZE + self.status.size() + pub fn exit_code(&self) -> u32 { + self.exit_code } pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { ensure_fixed_part_size!(in: src); let session_id = src.read_u32(); - let status = NowStatus::decode(src)?; + let exit_code = src.read_u32(); - Ok(Self { session_id, status }) + Ok(Self { session_id, exit_code }) } } impl Encode for NowExecAbortMsg { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { - size: cast_length!("size", self.body_size())?, + size: cast_length!("size", Self::FIXED_PART_SIZE)?, class: NowMessageClass::EXEC, kind: NowExecMsgKind::ABORT.0, flags: 0, @@ -61,7 +55,7 @@ impl Encode for NowExecAbortMsg { ensure_fixed_part_size!(in: dst); dst.write_u32(self.session_id); - self.status.encode(dst)?; + dst.write_u32(self.exit_code); Ok(()) } @@ -73,7 +67,7 @@ impl Encode for NowExecAbortMsg { // LINTS: Overall message size always fits into usize #[allow(clippy::arithmetic_side_effects)] fn size(&self) -> usize { - NowHeader::FIXED_PART_SIZE + self.body_size() + NowHeader::FIXED_PART_SIZE + Self::FIXED_PART_SIZE } } @@ -88,7 +82,7 @@ impl Decode<'_> for NowExecAbortMsg { } } -impl From for NowMessage { +impl From for NowMessage<'_> { fn from(msg: NowExecAbortMsg) -> Self { NowMessage::Exec(NowExecMessage::Abort(msg)) } diff --git a/crates/now-proto-pdu/src/exec/batch.rs b/crates/now-proto-pdu/src/exec/batch.rs index 78ca7d2..fa488ed 100644 --- a/crates/now-proto-pdu/src/exec/batch.rs +++ b/crates/now-proto-pdu/src/exec/batch.rs @@ -1,59 +1,129 @@ +use alloc::borrow::Cow; + +use bitflags::bitflags; use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowVarStr}; +bitflags! { + /// NOW-PROTO: NOW_EXEC_BATCH_MSG msgFlags field. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct NowExecBatchFlags: u16 { + /// Set if directory field contains non-default value. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_BATCH_DIRECTORY_SET + const DIRECTORY_SET = 0x0001; + } +} + /// The NOW_EXEC_BATCH_MSG message is used to execute a remote batch command. /// /// NOW-PROTO: NOW_EXEC_BATCH_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecBatchMsg { +pub struct NowExecBatchMsg<'a> { + flags: NowExecBatchFlags, session_id: u32, - command: NowVarStr, + command: NowVarStr<'a>, + directory: NowVarStr<'a>, } -impl NowExecBatchMsg { +impl_pdu_borrowing!(NowExecBatchMsg<'_>, OwnedNowExecBatchMsg); + +impl IntoOwned for NowExecBatchMsg<'_> { + type Owned = OwnedNowExecBatchMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowExecBatchMsg { + flags: self.flags, + session_id: self.session_id, + command: self.command.into_owned(), + directory: self.directory.into_owned(), + } + } +} + +impl<'a> NowExecBatchMsg<'a> { const NAME: &'static str = "NOW_EXEC_BATCH_MSG"; const FIXED_PART_SIZE: usize = 4; - pub fn new(session_id: u32, command: NowVarStr) -> Self { - Self { session_id, command } + pub fn new(session_id: u32, command: impl Into>) -> EncodeResult { + let msg = Self { + flags: NowExecBatchFlags::empty(), + session_id, + command: NowVarStr::new(command)?, + directory: NowVarStr::default(), + }; + + msg.ensure_message_size()?; + + Ok(msg) + } + + pub fn with_directory(mut self, directory: impl Into>) -> EncodeResult { + self.flags |= NowExecBatchFlags::DIRECTORY_SET; + self.directory = NowVarStr::new(directory)?; + + self.ensure_message_size()?; + + Ok(self) } pub fn session_id(&self) -> u32 { self.session_id } - pub fn command(&self) -> &NowVarStr { + pub fn command(&self) -> &str { &self.command } + pub fn directory(&self) -> Option<&str> { + if self.flags.contains(NowExecBatchFlags::DIRECTORY_SET) { + Some(&self.directory) + } else { + None + } + } + // LINTS: Overall message size always fits into usize; VarStr size always a few powers of 2 less // than u32::MAX, therefore it fits into usize #[allow(clippy::arithmetic_side_effects)] fn body_size(&self) -> usize { - Self::FIXED_PART_SIZE + self.command.size() + Self::FIXED_PART_SIZE + self.command.size() + self.directory.size() + } + + fn ensure_message_size(&self) -> EncodeResult<()> { + ensure_now_message_size!(Self::FIXED_PART_SIZE, self.command.size(), self.directory.size()); + + Ok(()) } - pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); + let flags = NowExecBatchFlags::from_bits_retain(header.flags); let session_id = src.read_u32(); let command = NowVarStr::decode(src)?; - - Ok(Self { session_id, command }) + let directory = NowVarStr::decode(src)?; + + Ok(Self { + flags, + session_id, + command, + directory, + }) } } -impl Encode for NowExecBatchMsg { +impl Encode for NowExecBatchMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, class: NowMessageClass::EXEC, kind: NowExecMsgKind::BATCH.0, - flags: 0, + flags: self.flags.bits(), }; header.encode(dst)?; @@ -61,6 +131,7 @@ impl Encode for NowExecBatchMsg { ensure_fixed_part_size!(in: dst); dst.write_u32(self.session_id); self.command.encode(dst)?; + self.directory.encode(dst)?; Ok(()) } @@ -76,8 +147,8 @@ impl Encode for NowExecBatchMsg { } } -impl Decode<'_> for NowExecBatchMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowExecBatchMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowExecMsgKind(header.kind)) { @@ -87,8 +158,8 @@ impl Decode<'_> for NowExecBatchMsg { } } -impl From for NowMessage { - fn from(msg: NowExecBatchMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowExecBatchMsg<'a>) -> Self { NowMessage::Exec(NowExecMessage::Batch(msg)) } } diff --git a/crates/now-proto-pdu/src/exec/cancel_req.rs b/crates/now-proto-pdu/src/exec/cancel_req.rs index 9787035..e9275dd 100644 --- a/crates/now-proto-pdu/src/exec/cancel_req.rs +++ b/crates/now-proto-pdu/src/exec/cancel_req.rs @@ -70,7 +70,7 @@ impl Decode<'_> for NowExecCancelReqMsg { } } -impl From for NowMessage { +impl From for NowMessage<'_> { fn from(msg: NowExecCancelReqMsg) -> Self { NowMessage::Exec(NowExecMessage::CancelReq(msg)) } diff --git a/crates/now-proto-pdu/src/exec/cancel_rsp.rs b/crates/now-proto-pdu/src/exec/cancel_rsp.rs index d98d409..5b557db 100644 --- a/crates/now-proto-pdu/src/exec/cancel_rsp.rs +++ b/crates/now-proto-pdu/src/exec/cancel_rsp.rs @@ -1,33 +1,62 @@ use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; -use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowStatus}; +use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowStatus, NowStatusError}; /// The NOW_EXEC_CANCEL_RSP_MSG message is used to respond to a remote execution cancel request. /// /// NOW_PROTO: NOW_EXEC_CANCEL_RSP_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecCancelRspMsg { +pub struct NowExecCancelRspMsg<'a> { session_id: u32, - status: NowStatus, + status: NowStatus<'a>, } -impl NowExecCancelRspMsg { +impl_pdu_borrowing!(NowExecCancelRspMsg<'_>, OwnedNowExecCancelRspMsg); + +impl IntoOwned for NowExecCancelRspMsg<'_> { + type Owned = OwnedNowExecCancelRspMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowExecCancelRspMsg { + session_id: self.session_id, + status: self.status.into_owned(), + } + } +} + +impl<'a> NowExecCancelRspMsg<'a> { const NAME: &'static str = "NOW_EXEC_CANCEL_RSP_MSG"; const FIXED_PART_SIZE: usize = 4; - pub fn new(session_id: u32, status: NowStatus) -> Self { - Self { session_id, status } + pub fn new_success(session_id: u32) -> Self { + let msg = Self { + session_id, + status: NowStatus::new_success(), + }; + + msg + } + + pub fn new_error(session_id: u32, error: impl Into) -> EncodeResult { + let msg = Self { + session_id, + status: NowStatus::new_error(error), + }; + + ensure_now_message_size!(Self::FIXED_PART_SIZE, msg.status.size()); + + Ok(msg) } pub fn session_id(&self) -> u32 { self.session_id } - pub fn status(&self) -> &NowStatus { - &self.status + pub fn to_result(&self) -> Result<(), NowStatusError> { + self.status.to_result() } // LINTS: Overall message size always fits into usize @@ -36,17 +65,17 @@ impl NowExecCancelRspMsg { Self::FIXED_PART_SIZE + self.status.size() } - pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); - let session_id = src.read_u32(); + let status = NowStatus::decode(src)?; Ok(Self { session_id, status }) } } -impl Encode for NowExecCancelRspMsg { +impl Encode for NowExecCancelRspMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, @@ -75,8 +104,8 @@ impl Encode for NowExecCancelRspMsg { } } -impl Decode<'_> for NowExecCancelRspMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowExecCancelRspMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowExecMsgKind(header.kind)) { @@ -86,8 +115,8 @@ impl Decode<'_> for NowExecCancelRspMsg { } } -impl From for NowMessage { - fn from(msg: NowExecCancelRspMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowExecCancelRspMsg<'a>) -> Self { NowMessage::Exec(NowExecMessage::CancelRsp(msg)) } } diff --git a/crates/now-proto-pdu/src/exec/capset.rs b/crates/now-proto-pdu/src/exec/capset.rs deleted file mode 100644 index eba3b55..0000000 --- a/crates/now-proto-pdu/src/exec/capset.rs +++ /dev/null @@ -1,105 +0,0 @@ -use bitflags::bitflags; -use ironrdp_core::{invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; - -use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass}; - -bitflags! { - /// NOW-PROTO: NOW_EXEC_CAPSET_MSG msgFlags field. - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct NowExecCapsetFlags: u16 { - /// Generic "Run" execution style. - /// - /// NOW-PROTO: NOW_EXEC_STYLE_RUN - const STYLE_RUN = 0x0001; - /// Generic command execution style. - /// - /// NOW-PROTO: NOW_EXEC_STYLE_CMD - const STYLE_CMD = 0x0002; - /// CreateProcess() execution style. - /// - /// NOW-PROTO: NOW_EXEC_STYLE_PROCESS - const STYLE_PROCESS = 0x0004; - /// System shell (.sh) execution style. - /// - /// NOW-PROTO: NOW_EXEC_STYLE_SHELL - const STYLE_SHELL = 0x0008; - /// Windows batch file (.bat) execution style. - /// - /// NOW-PROTO: NOW_EXEC_STYLE_BATCH - const STYLE_BATCH = 0x0010; - /// Windows PowerShell (.ps1) execution style. - /// - /// NOW-PROTO: NOW_EXEC_STYLE_WINPS - const STYLE_WINPS = 0x0020; - /// PowerShell 7 (.ps1) execution style. - /// - /// NOW-PROTO: NOW_EXEC_STYLE_PWSH - const STYLE_PWSH = 0x0040; - /// Applescript (.scpt) execution style. - /// - /// NOW-PROTO: NOW_EXEC_STYLE_APPLESCRIPT - const STYLE_APPLESCRIPT = 0x0080; - } -} - -/// The NOW_EXEC_CAPSET_MSG message is sent to advertise capabilities. -/// -/// NOW-PROTO: NOW_EXEC_CAPSET_MSG -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecCapsetMsg { - flags: NowExecCapsetFlags, -} - -impl NowExecCapsetMsg { - const NAME: &'static str = "NOW_EXEC_CAPSET_MSG"; - - pub fn new(flags: NowExecCapsetFlags) -> Self { - Self { flags } - } - - pub fn flags(&self) -> NowExecCapsetFlags { - self.flags - } -} - -impl Encode for NowExecCapsetMsg { - fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { - let header = NowHeader { - size: 0, - class: NowMessageClass::EXEC, - kind: NowExecMsgKind::CAPSET.0, - flags: self.flags.bits(), - }; - - header.encode(dst)?; - - Ok(()) - } - - fn name(&self) -> &'static str { - Self::NAME - } - - fn size(&self) -> usize { - NowHeader::FIXED_PART_SIZE - } -} - -impl Decode<'_> for NowExecCapsetMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { - let header = NowHeader::decode(src)?; - - match (header.class, NowExecMsgKind(header.kind)) { - (NowMessageClass::EXEC, NowExecMsgKind::CAPSET) => Ok(Self { - flags: NowExecCapsetFlags::from_bits_retain(header.flags), - }), - _ => Err(invalid_field_err!("type", "invalid message type")), - } - } -} - -impl From for NowMessage { - fn from(msg: NowExecCapsetMsg) -> Self { - NowMessage::Exec(NowExecMessage::Capset(msg)) - } -} diff --git a/crates/now-proto-pdu/src/exec/cmd.rs b/crates/now-proto-pdu/src/exec/cmd.rs deleted file mode 100644 index d9ea461..0000000 --- a/crates/now-proto-pdu/src/exec/cmd.rs +++ /dev/null @@ -1 +0,0 @@ -// TODO: Not yet defined in specification diff --git a/crates/now-proto-pdu/src/exec/data.rs b/crates/now-proto-pdu/src/exec/data.rs index d41822c..f811e6c 100644 --- a/crates/now-proto-pdu/src/exec/data.rs +++ b/crates/now-proto-pdu/src/exec/data.rs @@ -1,7 +1,9 @@ +use alloc::borrow::Cow; + use bitflags::bitflags; use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowVarBuf}; @@ -9,27 +11,57 @@ use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageCla bitflags! { /// NOW-PROTO: NOW_EXEC_DATA_MSG flags field. #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct NowExecDataFlags: u16 { - /// This is the first data message. - /// - /// NOW-PROTO: NOW_EXEC_FLAG_DATA_FIRST - const FIRST = 0x0001; + struct NowExecDataFlags: u16 { /// This is the last data message, the command completed execution. /// /// NOW-PROTO: NOW_EXEC_FLAG_DATA_LAST - const LAST = 0x0002; + const LAST = 0x0001; /// The data is from the standard input. /// /// NOW-PROTO: NOW_EXEC_FLAG_DATA_STDIN - const STDIN = 0x0004; + const STDIN = 0x0002; /// The data is from the standard output. /// /// NOW-PROTO: NOW_EXEC_FLAG_DATA_STDOUT - const STDOUT = 0x0008; + const STDOUT = 0x0004; /// The data is from the standard error. /// /// NOW-PROTO: NOW_EXEC_FLAG_DATA_STDERR - const STDERR = 0x0010; + const STDERR = 0x0008; + } +} + +/// Redirected std stream kind for the NOW_EXEC_DATA_MSG message. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NowExecDataStreamKind { + Stdin, + Stdout, + Stderr, +} + +impl NowExecDataStreamKind { + fn to_flags(self) -> NowExecDataFlags { + match self { + NowExecDataStreamKind::Stdin => NowExecDataFlags::STDIN, + NowExecDataStreamKind::Stdout => NowExecDataFlags::STDOUT, + NowExecDataStreamKind::Stderr => NowExecDataFlags::STDERR, + } + } + + fn from_flags(flags: NowExecDataFlags) -> Option { + let flags = flags & (NowExecDataFlags::STDIN | NowExecDataFlags::STDOUT | NowExecDataFlags::STDERR); + + // Exactly one stream kind flag should be set + if flags.iter().count() != 1 { + return None; + } + + match flags { + NowExecDataFlags::STDIN => Some(NowExecDataStreamKind::Stdin), + NowExecDataFlags::STDOUT => Some(NowExecDataStreamKind::Stdout), + NowExecDataFlags::STDERR => Some(NowExecDataStreamKind::Stderr), + _ => unreachable!("validated by code above"), + } } } @@ -37,33 +69,65 @@ bitflags! { /// /// NOW-PROTO: NOW_EXEC_DATA_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecDataMsg { +pub struct NowExecDataMsg<'a> { flags: NowExecDataFlags, session_id: u32, - data: NowVarBuf, + data: NowVarBuf<'a>, } -impl NowExecDataMsg { +impl_pdu_borrowing!(NowExecDataMsg<'_>, OwnedNowExecDataMsg); + +impl IntoOwned for NowExecDataMsg<'_> { + type Owned = OwnedNowExecDataMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowExecDataMsg { + flags: self.flags, + session_id: self.session_id, + data: self.data.into_owned(), + } + } +} + +impl<'a> NowExecDataMsg<'a> { const NAME: &'static str = "NOW_EXEC_DATA_MSG"; const FIXED_PART_SIZE: usize = 4; - pub fn new(flags: NowExecDataFlags, session_id: u32, data: NowVarBuf) -> Self { - Self { + pub fn new( + session_id: u32, + stream: NowExecDataStreamKind, + last: bool, + data: impl Into>, + ) -> EncodeResult { + let mut flags = stream.to_flags(); + if last { + flags |= NowExecDataFlags::LAST; + } + + let msg = Self { flags, session_id, - data, - } + data: NowVarBuf::new(data)?, + }; + + ensure_now_message_size!(Self::FIXED_PART_SIZE, msg.data.size()); + + Ok(msg) + } + + pub fn stream_kind(&self) -> DecodeResult { + NowExecDataStreamKind::from_flags(self.flags).ok_or_else(|| invalid_field_err!("flags", "invalid stream kind")) } - pub fn flags(&self) -> NowExecDataFlags { - self.flags + pub fn is_last(&self) -> bool { + self.flags.contains(NowExecDataFlags::LAST) } pub fn session_id(&self) -> u32 { self.session_id } - pub fn data(&self) -> &NowVarBuf { + pub fn data(&self) -> &[u8] { &self.data } @@ -74,7 +138,7 @@ impl NowExecDataMsg { Self::FIXED_PART_SIZE + self.data.size() } - pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); let flags = NowExecDataFlags::from_bits_retain(header.flags); @@ -89,7 +153,7 @@ impl NowExecDataMsg { } } -impl Encode for NowExecDataMsg { +impl Encode for NowExecDataMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, @@ -118,8 +182,8 @@ impl Encode for NowExecDataMsg { } } -impl Decode<'_> for NowExecDataMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowExecDataMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowExecMsgKind(header.kind)) { @@ -129,8 +193,8 @@ impl Decode<'_> for NowExecDataMsg { } } -impl From for NowMessage { - fn from(msg: NowExecDataMsg) -> Self { +impl<'de> From> for NowMessage<'de> { + fn from(msg: NowExecDataMsg<'de>) -> Self { NowMessage::Exec(NowExecMessage::Data(msg)) } } diff --git a/crates/now-proto-pdu/src/exec/mod.rs b/crates/now-proto-pdu/src/exec/mod.rs index 9956034..f6393f0 100644 --- a/crates/now-proto-pdu/src/exec/mod.rs +++ b/crates/now-proto-pdu/src/exec/mod.rs @@ -2,62 +2,82 @@ mod abort; mod batch; mod cancel_req; mod cancel_rsp; -mod capset; -mod cmd; mod data; mod process; mod pwsh; mod result; mod run; mod shell; +mod started; mod win_ps; pub use abort::NowExecAbortMsg; -pub use batch::NowExecBatchMsg; +pub use batch::{NowExecBatchMsg, OwnedNowExecBatchMsg}; pub use cancel_req::NowExecCancelReqMsg; -pub use cancel_rsp::NowExecCancelRspMsg; -pub use capset::{NowExecCapsetFlags, NowExecCapsetMsg}; -pub use data::{NowExecDataFlags, NowExecDataMsg}; -use ironrdp_core::{invalid_field_err, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; -pub use process::NowExecProcessMsg; -pub use pwsh::NowExecPwshMsg; -pub use result::NowExecResultMsg; -pub use run::NowExecRunMsg; -pub use shell::NowExecShellMsg; -pub use win_ps::{NowExecWinPsFlags, NowExecWinPsMsg}; +pub use cancel_rsp::{NowExecCancelRspMsg, OwnedNowExecCancelRspMsg}; +pub use data::{NowExecDataMsg, NowExecDataStreamKind, OwnedNowExecDataMsg}; +use ironrdp_core::{invalid_field_err, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor}; +pub use process::{NowExecProcessMsg, OwnedNowExecProcessMsg}; +pub use pwsh::{NowExecPwshMsg, OwnedNowExecPwshMsg}; +pub use result::{NowExecResultMsg, OwnedNowExecResultMsg}; +pub use run::{NowExecRunMsg, OwnedNowExecRunMsg}; +pub use shell::{NowExecShellMsg, OwnedNowExecShellMsg}; +pub use started::NowExecStartedMsg; +pub(crate) use win_ps::NowExecWinPsFlags; +pub use win_ps::{ComApartmentStateKind, NowExecWinPsMsg, OwnedNowExecWinPsMsg}; use crate::NowHeader; #[derive(Debug, Clone, PartialEq, Eq)] -pub enum NowExecMessage { - Capset(NowExecCapsetMsg), +pub enum NowExecMessage<'a> { Abort(NowExecAbortMsg), CancelReq(NowExecCancelReqMsg), - CancelRsp(NowExecCancelRspMsg), - Result(NowExecResultMsg), - Data(NowExecDataMsg), - Run(NowExecRunMsg), - // TODO: Define `Cmd` message in specification - Process(NowExecProcessMsg), - Shell(NowExecShellMsg), - Batch(NowExecBatchMsg), - WinPs(NowExecWinPsMsg), - Pwsh(NowExecPwshMsg), + CancelRsp(NowExecCancelRspMsg<'a>), + Result(NowExecResultMsg<'a>), + Data(NowExecDataMsg<'a>), + Started(NowExecStartedMsg), + Run(NowExecRunMsg<'a>), + Process(NowExecProcessMsg<'a>), + Shell(NowExecShellMsg<'a>), + Batch(NowExecBatchMsg<'a>), + WinPs(NowExecWinPsMsg<'a>), + Pwsh(NowExecPwshMsg<'a>), } -impl NowExecMessage { +pub type OwnedNowExecMessage = NowExecMessage<'static>; + +impl IntoOwned for NowExecMessage<'_> { + type Owned = OwnedNowExecMessage; + + fn into_owned(self) -> Self::Owned { + match self { + Self::Abort(msg) => OwnedNowExecMessage::Abort(msg), + Self::CancelReq(msg) => OwnedNowExecMessage::CancelReq(msg), + Self::CancelRsp(msg) => OwnedNowExecMessage::CancelRsp(msg.into_owned()), + Self::Result(msg) => OwnedNowExecMessage::Result(msg.into_owned()), + Self::Data(msg) => OwnedNowExecMessage::Data(msg.into_owned()), + Self::Started(msg) => OwnedNowExecMessage::Started(msg), + Self::Run(msg) => OwnedNowExecMessage::Run(msg.into_owned()), + Self::Process(msg) => OwnedNowExecMessage::Process(msg.into_owned()), + Self::Shell(msg) => OwnedNowExecMessage::Shell(msg.into_owned()), + Self::Batch(msg) => OwnedNowExecMessage::Batch(msg.into_owned()), + Self::WinPs(msg) => OwnedNowExecMessage::WinPs(msg.into_owned()), + Self::Pwsh(msg) => OwnedNowExecMessage::Pwsh(msg.into_owned()), + } + } +} + +impl<'a> NowExecMessage<'a> { const NAME: &'static str = "NOW_EXEC_MSG"; - pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { match NowExecMsgKind(header.kind) { - NowExecMsgKind::CAPSET => Ok(Self::Capset(NowExecCapsetMsg::new( - NowExecCapsetFlags::from_bits_retain(header.flags), - ))), NowExecMsgKind::ABORT => Ok(Self::Abort(NowExecAbortMsg::decode_from_body(header, src)?)), NowExecMsgKind::CANCEL_REQ => Ok(Self::CancelReq(NowExecCancelReqMsg::decode_from_body(header, src)?)), NowExecMsgKind::CANCEL_RSP => Ok(Self::CancelRsp(NowExecCancelRspMsg::decode_from_body(header, src)?)), NowExecMsgKind::RESULT => Ok(Self::Result(NowExecResultMsg::decode_from_body(header, src)?)), NowExecMsgKind::DATA => Ok(Self::Data(NowExecDataMsg::decode_from_body(header, src)?)), + NowExecMsgKind::STARTED => Ok(Self::Started(NowExecStartedMsg::decode_from_body(header, src)?)), NowExecMsgKind::RUN => Ok(Self::Run(NowExecRunMsg::decode_from_body(header, src)?)), NowExecMsgKind::PROCESS => Ok(Self::Process(NowExecProcessMsg::decode_from_body(header, src)?)), NowExecMsgKind::SHELL => Ok(Self::Shell(NowExecShellMsg::decode_from_body(header, src)?)), @@ -69,15 +89,15 @@ impl NowExecMessage { } } -impl Encode for NowExecMessage { +impl Encode for NowExecMessage<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { match self { - Self::Capset(msg) => msg.encode(dst), Self::Abort(msg) => msg.encode(dst), Self::CancelReq(msg) => msg.encode(dst), Self::CancelRsp(msg) => msg.encode(dst), Self::Result(msg) => msg.encode(dst), Self::Data(msg) => msg.encode(dst), + Self::Started(msg) => msg.encode(dst), Self::Run(msg) => msg.encode(dst), Self::Process(msg) => msg.encode(dst), Self::Shell(msg) => msg.encode(dst), @@ -93,12 +113,12 @@ impl Encode for NowExecMessage { fn size(&self) -> usize { match self { - Self::Capset(msg) => msg.size(), Self::Abort(msg) => msg.size(), Self::CancelReq(msg) => msg.size(), Self::CancelRsp(msg) => msg.size(), Self::Result(msg) => msg.size(), Self::Data(msg) => msg.size(), + Self::Started(msg) => msg.size(), Self::Run(msg) => msg.size(), Self::Process(msg) => msg.size(), Self::Shell(msg) => msg.size(), @@ -113,8 +133,6 @@ impl Encode for NowExecMessage { pub struct NowExecMsgKind(pub u8); impl NowExecMsgKind { - /// NOW-PROTO: NOW_EXEC_CAPSET_MSG_ID - pub const CAPSET: Self = Self(0x00); /// NOW-PROTO: NOW_EXEC_ABORT_MSG_ID pub const ABORT: Self = Self(0x01); /// NOW-PROTO: NOW_EXEC_CANCEL_REQ_MSG_ID @@ -125,18 +143,18 @@ impl NowExecMsgKind { pub const RESULT: Self = Self(0x04); /// NOW-PROTO: NOW_EXEC_DATA_MSG_ID pub const DATA: Self = Self(0x05); + /// NOW-PROTO: NOW_EXEC_STARTED_MSG_ID + pub const STARTED: Self = Self(0x06); /// NOW-PROTO: NOW_EXEC_RUN_MSG_ID pub const RUN: Self = Self(0x10); - // /// NOW-PROTO: NOW_EXEC_CMD_MSG_ID - // pub const CMD: Self = Self(0x11); /// NOW-PROTO: NOW_EXEC_PROCESS_MSG_ID - pub const PROCESS: Self = Self(0x12); + pub const PROCESS: Self = Self(0x11); /// NOW-PROTO: NOW_EXEC_SHELL_MSG_ID - pub const SHELL: Self = Self(0x13); + pub const SHELL: Self = Self(0x12); /// NOW-PROTO: NOW_EXEC_BATCH_MSG_ID - pub const BATCH: Self = Self(0x14); + pub const BATCH: Self = Self(0x13); /// NOW-PROTO: NOW_EXEC_WINPS_MSG_ID - pub const WINPS: Self = Self(0x15); + pub const WINPS: Self = Self(0x14); /// NOW-PROTO: NOW_EXEC_PWSH_MSG_ID - pub const PWSH: Self = Self(0x16); + pub const PWSH: Self = Self(0x15); } diff --git a/crates/now-proto-pdu/src/exec/process.rs b/crates/now-proto-pdu/src/exec/process.rs index 24dc0d3..5f19033 100644 --- a/crates/now-proto-pdu/src/exec/process.rs +++ b/crates/now-proto-pdu/src/exec/process.rs @@ -1,36 +1,68 @@ +use alloc::borrow::Cow; + +use bitflags::bitflags; use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowVarStr}; +bitflags! { + /// NOW-PROTO: NOW_EXEC_PROCESS_MSG msgFlags field. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct NowExecProcessFlags: u16 { + /// Set if parameters field contains non-default value. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_PROCESS_PARAMETERS_SET + const PARAMETERS_SET = 0x0001; + + /// Set if directory field contains non-default value. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_PROCESS_DIRECTORY_SET + const DIRECTORY_SET = 0x0002; + } +} + /// The NOW_EXEC_PROCESS_MSG message is used to send a Windows CreateProcess() request. /// /// NOW-PROTO: NOW_EXEC_PROCESS_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecProcessMsg { +pub struct NowExecProcessMsg<'a> { + flags: NowExecProcessFlags, session_id: u32, - filename: NowVarStr, - parameters: NowVarStr, - directory: NowVarStr, + filename: NowVarStr<'a>, + parameters: NowVarStr<'a>, + directory: NowVarStr<'a>, } -impl NowExecProcessMsg { +impl_pdu_borrowing!(NowExecProcessMsg<'_>, OwnedNowExecProcessMsg); + +impl IntoOwned for NowExecProcessMsg<'_> { + type Owned = OwnedNowExecProcessMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowExecProcessMsg { + flags: self.flags, + session_id: self.session_id, + filename: self.filename.into_owned(), + parameters: self.parameters.into_owned(), + directory: self.directory.into_owned(), + } + } +} + +impl<'a> NowExecProcessMsg<'a> { const NAME: &'static str = "NOW_EXEC_PROCESS_MSG"; const FIXED_PART_SIZE: usize = 4; - pub fn new( - session_id: u32, - filename: NowVarStr, - parameters: NowVarStr, - directory: NowVarStr, - ) -> DecodeResult { + pub fn new(session_id: u32, filename: impl Into>) -> EncodeResult { let msg = Self { + flags: NowExecProcessFlags::empty(), session_id, - filename, - parameters, - directory, + filename: NowVarStr::new(filename)?, + parameters: NowVarStr::default(), + directory: NowVarStr::default(), }; msg.ensure_message_size()?; @@ -38,12 +70,31 @@ impl NowExecProcessMsg { Ok(msg) } - fn ensure_message_size(&self) -> DecodeResult<()> { - let _message_size = Self::FIXED_PART_SIZE - .checked_add(self.filename.size()) - .and_then(|size| size.checked_add(self.parameters.size())) - .and_then(|size| size.checked_add(self.directory.size())) - .ok_or_else(|| invalid_field_err!("size", "message size overflow"))?; + pub fn with_parameters(mut self, parameters: impl Into>) -> EncodeResult { + self.flags |= NowExecProcessFlags::PARAMETERS_SET; + self.parameters = NowVarStr::new(parameters)?; + + self.ensure_message_size()?; + + Ok(self) + } + + pub fn with_directory(mut self, directory: impl Into>) -> EncodeResult { + self.flags |= NowExecProcessFlags::DIRECTORY_SET; + self.directory = NowVarStr::new(directory)?; + + self.ensure_message_size()?; + + Ok(self) + } + + fn ensure_message_size(&self) -> EncodeResult<()> { + ensure_now_message_size!( + Self::FIXED_PART_SIZE, + self.filename.size(), + self.parameters.size(), + self.directory.size() + ); Ok(()) } @@ -52,16 +103,24 @@ impl NowExecProcessMsg { self.session_id } - pub fn filename(&self) -> &NowVarStr { + pub fn filename(&self) -> &str { &self.filename } - pub fn parameters(&self) -> &NowVarStr { - &self.parameters + pub fn parameters(&self) -> Option<&str> { + if self.flags.contains(NowExecProcessFlags::PARAMETERS_SET) { + Some(&self.parameters) + } else { + None + } } - pub fn directory(&self) -> &NowVarStr { - &self.directory + pub fn directory(&self) -> Option<&str> { + if self.flags.contains(NowExecProcessFlags::DIRECTORY_SET) { + Some(&self.directory) + } else { + None + } } // LINTS: Overall message size is validated in the constructor/decode method @@ -70,34 +129,34 @@ impl NowExecProcessMsg { Self::FIXED_PART_SIZE + self.filename.size() + self.parameters.size() + self.directory.size() } - pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); + let flags = NowExecProcessFlags::from_bits_retain(header.flags); let session_id = src.read_u32(); let filename = NowVarStr::decode(src)?; let parameters = NowVarStr::decode(src)?; let directory = NowVarStr::decode(src)?; let msg = Self { + flags, session_id, filename, parameters, directory, }; - msg.ensure_message_size()?; - Ok(msg) } } -impl Encode for NowExecProcessMsg { +impl Encode for NowExecProcessMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, class: NowMessageClass::EXEC, kind: NowExecMsgKind::PROCESS.0, - flags: 0, + flags: self.flags.bits(), }; header.encode(dst)?; @@ -122,8 +181,8 @@ impl Encode for NowExecProcessMsg { } } -impl Decode<'_> for NowExecProcessMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowExecProcessMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowExecMsgKind(header.kind)) { @@ -133,8 +192,8 @@ impl Decode<'_> for NowExecProcessMsg { } } -impl From for NowMessage { - fn from(msg: NowExecProcessMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowExecProcessMsg<'a>) -> Self { NowMessage::Exec(NowExecMessage::Process(msg)) } } diff --git a/crates/now-proto-pdu/src/exec/pwsh.rs b/crates/now-proto-pdu/src/exec/pwsh.rs index 2d65546..2cb9ca0 100644 --- a/crates/now-proto-pdu/src/exec/pwsh.rs +++ b/crates/now-proto-pdu/src/exec/pwsh.rs @@ -1,33 +1,57 @@ +use alloc::borrow::Cow; + use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; -use crate::{NowExecMessage, NowExecMsgKind, NowExecWinPsFlags, NowHeader, NowMessage, NowMessageClass, NowVarStr}; +use crate::{ + ComApartmentStateKind, NowExecMessage, NowExecMsgKind, NowExecWinPsFlags, NowHeader, NowMessage, NowMessageClass, + NowVarStr, +}; -/// The NOW_EXEC_PWSH_MSG message is used to execute a remote PowerShell 7 (pwsh) command. +/// The NOW_EXEC_PWSH_MSG message is used to execute a remote Windows PowerShell (powershell.exe) command. /// /// NOW-PROTO: NOW_EXEC_PWSH_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecPwshMsg { +pub struct NowExecPwshMsg<'a> { flags: NowExecWinPsFlags, session_id: u32, - command: NowVarStr, - execution_policy: NowVarStr, - configuration_name: NowVarStr, + command: NowVarStr<'a>, + directory: NowVarStr<'a>, + execution_policy: NowVarStr<'a>, + configuration_name: NowVarStr<'a>, +} + +impl_pdu_borrowing!(NowExecPwshMsg<'_>, OwnedNowExecPwshMsg); + +impl IntoOwned for NowExecPwshMsg<'_> { + type Owned = OwnedNowExecPwshMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowExecPwshMsg { + flags: self.flags, + session_id: self.session_id, + command: self.command.into_owned(), + directory: self.directory.into_owned(), + execution_policy: self.execution_policy.into_owned(), + configuration_name: self.configuration_name.into_owned(), + } + } } -impl NowExecPwshMsg { +impl<'a> NowExecPwshMsg<'a> { const NAME: &'static str = "NOW_EXEC_PWSH_MSG"; const FIXED_PART_SIZE: usize = 4; - pub fn new(session_id: u32, command: NowVarStr) -> DecodeResult { + pub fn new(session_id: u32, command: impl Into>) -> EncodeResult { let msg = Self { - session_id, - command, flags: NowExecWinPsFlags::empty(), - execution_policy: NowVarStr::empty(), - configuration_name: NowVarStr::empty(), + session_id, + command: NowVarStr::new(command)?, + directory: NowVarStr::default(), + execution_policy: NowVarStr::default(), + configuration_name: NowVarStr::default(), }; msg.ensure_message_size()?; @@ -35,53 +59,85 @@ impl NowExecPwshMsg { Ok(msg) } - fn ensure_message_size(&self) -> DecodeResult<()> { - let _message_size = Self::FIXED_PART_SIZE - .checked_add(self.command.size()) - .and_then(|size| size.checked_add(self.execution_policy.size())) - .and_then(|size| size.checked_add(self.configuration_name.size())) - .ok_or_else(|| invalid_field_err!("size", "message size overflow"))?; + pub fn with_directory(mut self, directory: impl Into>) -> EncodeResult { + self.flags |= NowExecWinPsFlags::DIRECTORY_SET; + self.directory = NowVarStr::new(directory)?; - Ok(()) - } + self.ensure_message_size()?; - #[must_use] - pub fn with_flags(mut self, flags: NowExecWinPsFlags) -> Self { - self.flags = flags; - self + Ok(self) } - pub fn with_execution_policy(mut self, execution_policy: NowVarStr) -> DecodeResult { - self.execution_policy = execution_policy; + pub fn with_execution_policy(mut self, execution_policy: impl Into>) -> EncodeResult { self.flags |= NowExecWinPsFlags::EXECUTION_POLICY; + self.execution_policy = NowVarStr::new(execution_policy)?; self.ensure_message_size()?; Ok(self) } - pub fn with_configuration_name(mut self, configuration_name: NowVarStr) -> DecodeResult { - self.configuration_name = configuration_name; + pub fn with_configuration_name(mut self, configuration_name: impl Into>) -> EncodeResult { self.flags |= NowExecWinPsFlags::CONFIGURATION_NAME; + self.configuration_name = NowVarStr::new(configuration_name)?; self.ensure_message_size()?; Ok(self) } - pub fn flags(&self) -> NowExecWinPsFlags { - self.flags + #[must_use] + pub fn set_no_logo(mut self) -> Self { + self.flags |= NowExecWinPsFlags::NO_LOGO; + self + } + + #[must_use] + pub fn set_no_exit(mut self) -> Self { + self.flags |= NowExecWinPsFlags::NO_EXIT; + self + } + + #[must_use] + pub fn set_no_profile(mut self) -> Self { + self.flags |= NowExecWinPsFlags::NO_PROFILE; + self + } + + #[must_use] + pub fn with_apartment_state(mut self, apartment_state: ComApartmentStateKind) -> Self { + self.flags |= apartment_state.to_flags(); + self + } + + fn ensure_message_size(&self) -> EncodeResult<()> { + let _message_size = Self::FIXED_PART_SIZE + .checked_add(self.command.size()) + .and_then(|size| size.checked_add(self.directory.size())) + .and_then(|size| size.checked_add(self.execution_policy.size())) + .and_then(|size| size.checked_add(self.configuration_name.size())) + .ok_or_else(|| invalid_field_err!("size", "message size overflow"))?; + + Ok(()) } pub fn session_id(&self) -> u32 { self.session_id } - pub fn command(&self) -> &NowVarStr { + pub fn command(&self) -> &str { &self.command } - pub fn execution_policy(&self) -> Option<&NowVarStr> { + pub fn directory(&self) -> Option<&str> { + if self.flags.contains(NowExecWinPsFlags::DIRECTORY_SET) { + Some(&self.directory) + } else { + None + } + } + + pub fn execution_policy(&self) -> Option<&str> { if self.flags.contains(NowExecWinPsFlags::EXECUTION_POLICY) { Some(&self.execution_policy) } else { @@ -89,7 +145,7 @@ impl NowExecPwshMsg { } } - pub fn configuration_name(&self) -> Option<&NowVarStr> { + pub fn configuration_name(&self) -> Option<&str> { if self.flags.contains(NowExecWinPsFlags::CONFIGURATION_NAME) { Some(&self.configuration_name) } else { @@ -97,18 +153,39 @@ impl NowExecPwshMsg { } } + pub fn is_no_logo(&self) -> bool { + self.flags.contains(NowExecWinPsFlags::NO_LOGO) + } + + pub fn is_no_exit(&self) -> bool { + self.flags.contains(NowExecWinPsFlags::NO_EXIT) + } + + pub fn is_no_profile(&self) -> bool { + self.flags.contains(NowExecWinPsFlags::NO_PROFILE) + } + + pub fn apartment_state(&self) -> DecodeResult> { + ComApartmentStateKind::from_flags(self.flags) + } + // LINTS: Overall message size is validated in the constructor/decode method #[allow(clippy::arithmetic_side_effects)] fn body_size(&self) -> usize { - Self::FIXED_PART_SIZE + self.command.size() + self.execution_policy.size() + self.configuration_name.size() + Self::FIXED_PART_SIZE + + self.command.size() + + self.directory.size() + + self.execution_policy.size() + + self.configuration_name.size() } - pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); let flags = NowExecWinPsFlags::from_bits_retain(header.flags); let session_id = src.read_u32(); let command = NowVarStr::decode(src)?; + let directory = NowVarStr::decode(src)?; let execution_policy = NowVarStr::decode(src)?; let configuration_name = NowVarStr::decode(src)?; @@ -116,17 +193,16 @@ impl NowExecPwshMsg { flags, session_id, command, + directory, execution_policy, configuration_name, }; - msg.ensure_message_size()?; - Ok(msg) } } -impl Encode for NowExecPwshMsg { +impl Encode for NowExecPwshMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, @@ -140,6 +216,7 @@ impl Encode for NowExecPwshMsg { ensure_fixed_part_size!(in: dst); dst.write_u32(self.session_id); self.command.encode(dst)?; + self.directory.encode(dst)?; self.execution_policy.encode(dst)?; self.configuration_name.encode(dst)?; @@ -150,15 +227,15 @@ impl Encode for NowExecPwshMsg { Self::NAME } - // LINTS: see body_size() + // LINTS: See body_size() #[allow(clippy::arithmetic_side_effects)] fn size(&self) -> usize { NowHeader::FIXED_PART_SIZE + self.body_size() } } -impl Decode<'_> for NowExecPwshMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowExecPwshMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowExecMsgKind(header.kind)) { @@ -168,8 +245,8 @@ impl Decode<'_> for NowExecPwshMsg { } } -impl From for NowMessage { - fn from(msg: NowExecPwshMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowExecPwshMsg<'a>) -> Self { NowMessage::Exec(NowExecMessage::Pwsh(msg)) } } diff --git a/crates/now-proto-pdu/src/exec/result.rs b/crates/now-proto-pdu/src/exec/result.rs index d601067..42b9fec 100644 --- a/crates/now-proto-pdu/src/exec/result.rs +++ b/crates/now-proto-pdu/src/exec/result.rs @@ -1,33 +1,70 @@ use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; -use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowStatus}; +use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowStatus, NowStatusError}; /// The NOW_EXEC_RESULT_MSG message is used to return the result of an execution request. /// /// NOW_PROTO: NOW_EXEC_RESULT_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecResultMsg { +pub struct NowExecResultMsg<'a> { session_id: u32, - status: NowStatus, + exit_code: u32, + status: NowStatus<'a>, } -impl NowExecResultMsg { +impl_pdu_borrowing!(NowExecResultMsg<'_>, OwnedNowExecResultMsg); + +impl IntoOwned for NowExecResultMsg<'_> { + type Owned = OwnedNowExecResultMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowExecResultMsg { + session_id: self.session_id, + exit_code: self.exit_code, + status: self.status.into_owned(), + } + } +} + +impl<'a> NowExecResultMsg<'a> { const NAME: &'static str = "NOW_EXEC_RESULT_MSG"; - const FIXED_PART_SIZE: usize = 4; + const FIXED_PART_SIZE: usize = 8; + + pub fn new_success(session_id: u32, exit_code: u32) -> Self { + let msg = Self { + session_id, + exit_code, + status: NowStatus::new_success(), + }; + + msg.ensure_message_size() + .expect("success message size always fits into payload"); + + msg + } + + pub fn new_error(session_id: u32, error: impl Into) -> EncodeResult { + let msg = Self { + session_id, + exit_code: 0, + status: NowStatus::new_error(error), + }; - pub fn new(session_id: u32, status: NowStatus) -> Self { - Self { session_id, status } + msg.ensure_message_size()?; + + Ok(msg) } pub fn session_id(&self) -> u32 { self.session_id } - pub fn status(&self) -> &NowStatus { - &self.status + /// Returns the process exit code of the executed command on success. + pub fn to_result(&self) -> Result { + self.status.to_result().map(|_| self.exit_code) } // LINTS: Overall message size always fits into usize @@ -36,17 +73,29 @@ impl NowExecResultMsg { Self::FIXED_PART_SIZE + self.status.size() } - pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + fn ensure_message_size(&self) -> EncodeResult<()> { + ensure_now_message_size!(Self::FIXED_PART_SIZE, self.status.size()); + + Ok(()) + } + + pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); let session_id = src.read_u32(); + let exit_code = src.read_u32(); + let status = NowStatus::decode(src)?; - Ok(Self { session_id, status }) + Ok(Self { + session_id, + exit_code, + status, + }) } } -impl Encode for NowExecResultMsg { +impl Encode for NowExecResultMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, @@ -59,6 +108,8 @@ impl Encode for NowExecResultMsg { ensure_fixed_part_size!(in: dst); dst.write_u32(self.session_id); + dst.write_u32(self.exit_code); + self.status.encode(dst)?; Ok(()) @@ -75,8 +126,8 @@ impl Encode for NowExecResultMsg { } } -impl Decode<'_> for NowExecResultMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowExecResultMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowExecMsgKind(header.kind)) { @@ -86,8 +137,8 @@ impl Decode<'_> for NowExecResultMsg { } } -impl From for NowMessage { - fn from(msg: NowExecResultMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowExecResultMsg<'a>) -> Self { NowMessage::Exec(NowExecMessage::Result(msg)) } } diff --git a/crates/now-proto-pdu/src/exec/run.rs b/crates/now-proto-pdu/src/exec/run.rs index b39174e..1905f1d 100644 --- a/crates/now-proto-pdu/src/exec/run.rs +++ b/crates/now-proto-pdu/src/exec/run.rs @@ -1,6 +1,8 @@ +use alloc::borrow::Cow; + use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowVarStr}; @@ -12,36 +14,44 @@ use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageCla /// /// NOW_PROTO: NOW_EXEC_RUN_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecRunMsg { +pub struct NowExecRunMsg<'a> { session_id: u32, - command: NowVarStr, + command: NowVarStr<'a>, +} + +impl_pdu_borrowing!(NowExecRunMsg<'_>, OwnedNowExecRunMsg); + +impl IntoOwned for NowExecRunMsg<'_> { + type Owned = OwnedNowExecRunMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowExecRunMsg { + session_id: self.session_id, + command: self.command.into_owned(), + } + } } -impl NowExecRunMsg { +impl<'a> NowExecRunMsg<'a> { const NAME: &'static str = "NOW_EXEC_RUN_MSG"; const FIXED_PART_SIZE: usize = 4; - pub fn new(session_id: u32, command: NowVarStr) -> DecodeResult { - let msg = Self { session_id, command }; + pub fn new(session_id: u32, command: impl Into>) -> EncodeResult { + let msg = Self { + session_id, + command: NowVarStr::new(command)?, + }; - msg.ensure_message_size()?; + ensure_now_message_size!(Self::FIXED_PART_SIZE, msg.command.size()); Ok(msg) } - fn ensure_message_size(&self) -> DecodeResult<()> { - let _message_size = Self::FIXED_PART_SIZE - .checked_add(self.command.size()) - .ok_or_else(|| invalid_field_err!("size", "message size overflow"))?; - - Ok(()) - } - pub fn session_id(&self) -> u32 { self.session_id } - pub fn command(&self) -> &NowVarStr { + pub fn command(&self) -> &str { &self.command } @@ -51,7 +61,7 @@ impl NowExecRunMsg { Self::FIXED_PART_SIZE + self.command.size() } - pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); let session_id = src.read_u32(); @@ -59,13 +69,11 @@ impl NowExecRunMsg { let msg = Self { session_id, command }; - msg.ensure_message_size()?; - Ok(msg) } } -impl Encode for NowExecRunMsg { +impl Encode for NowExecRunMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, @@ -94,8 +102,8 @@ impl Encode for NowExecRunMsg { } } -impl Decode<'_> for NowExecRunMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowExecRunMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowExecMsgKind(header.kind)) { @@ -105,8 +113,8 @@ impl Decode<'_> for NowExecRunMsg { } } -impl From for NowMessage { - fn from(msg: NowExecRunMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowExecRunMsg<'a>) -> Self { NowMessage::Exec(NowExecMessage::Run(msg)) } } diff --git a/crates/now-proto-pdu/src/exec/shell.rs b/crates/now-proto-pdu/src/exec/shell.rs index 53905bc..3de0cc7 100644 --- a/crates/now-proto-pdu/src/exec/shell.rs +++ b/crates/now-proto-pdu/src/exec/shell.rs @@ -1,29 +1,68 @@ +use alloc::borrow::Cow; + +use bitflags::bitflags; use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowVarStr}; +bitflags! { + /// NOW-PROTO: NOW_EXEC_SHELL_MSG msgFlags field. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct NowExecShellFlags: u16 { + /// Set if parameters shell contains non-default value. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_SHELL_SHELL_SET + const PARAMETERS_SET = 0x0001; + + /// Set if directory field contains non-default value. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_SHELL_DIRECTORY_SET + const DIRECTORY_SET = 0x0002; + } +} + /// The NOW_EXEC_SHELL_MSG message is used to execute a remote shell command. /// /// NOW-PROTO: NOW_EXEC_SHELL_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecShellMsg { +pub struct NowExecShellMsg<'a> { + flags: NowExecShellFlags, session_id: u32, - command: NowVarStr, - shell: NowVarStr, + command: NowVarStr<'a>, + shell: NowVarStr<'a>, + directory: NowVarStr<'a>, } -impl NowExecShellMsg { +impl_pdu_borrowing!(NowExecShellMsg<'_>, OwnedNowExecShellMsg); + +impl IntoOwned for NowExecShellMsg<'_> { + type Owned = OwnedNowExecShellMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowExecShellMsg { + flags: self.flags, + session_id: self.session_id, + command: self.command.into_owned(), + shell: self.shell.into_owned(), + directory: self.directory.into_owned(), + } + } +} + +impl<'a> NowExecShellMsg<'a> { const NAME: &'static str = "NOW_EXEC_SHELL_MSG"; const FIXED_PART_SIZE: usize = 4; - pub fn new(session_id: u32, command: NowVarStr, shell: NowVarStr) -> DecodeResult { + pub fn new(session_id: u32, command: impl Into>) -> EncodeResult { let msg = Self { + flags: NowExecShellFlags::empty(), session_id, - command, - shell, + command: NowVarStr::new(command)?, + shell: NowVarStr::default(), + directory: NowVarStr::default(), }; msg.ensure_message_size()?; @@ -31,11 +70,31 @@ impl NowExecShellMsg { Ok(msg) } - fn ensure_message_size(&self) -> DecodeResult<()> { - let _message_size = Self::FIXED_PART_SIZE - .checked_add(self.command.size()) - .and_then(|size| size.checked_add(self.shell.size())) - .ok_or_else(|| invalid_field_err!("size", "message size overflow"))?; + pub fn with_shell(mut self, shell: impl Into>) -> EncodeResult { + self.flags |= NowExecShellFlags::PARAMETERS_SET; + self.shell = NowVarStr::new(shell)?; + + self.ensure_message_size()?; + + Ok(self) + } + + pub fn with_directory(mut self, directory: impl Into>) -> EncodeResult { + self.flags |= NowExecShellFlags::DIRECTORY_SET; + self.directory = NowVarStr::new(directory)?; + + self.ensure_message_size()?; + + Ok(self) + } + + fn ensure_message_size(&self) -> EncodeResult<()> { + ensure_now_message_size!( + Self::FIXED_PART_SIZE, + self.command.size(), + self.shell.size(), + self.directory.size() + ); Ok(()) } @@ -44,46 +103,60 @@ impl NowExecShellMsg { self.session_id } - pub fn command(&self) -> &NowVarStr { + pub fn command(&self) -> &str { &self.command } - pub fn shell(&self) -> &NowVarStr { - &self.shell + pub fn shell(&self) -> Option<&str> { + if self.flags.contains(NowExecShellFlags::PARAMETERS_SET) { + Some(&self.shell) + } else { + None + } + } + + pub fn directory(&self) -> Option<&str> { + if self.flags.contains(NowExecShellFlags::DIRECTORY_SET) { + Some(&self.directory) + } else { + None + } } // LINTS: Overall message size is validated in the constructor/decode method #[allow(clippy::arithmetic_side_effects)] fn body_size(&self) -> usize { - Self::FIXED_PART_SIZE + self.command.size() + self.shell.size() + Self::FIXED_PART_SIZE + self.command.size() + self.shell.size() + self.directory.size() } - pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); + let flags = NowExecShellFlags::from_bits_retain(header.flags); let session_id = src.read_u32(); let command = NowVarStr::decode(src)?; let shell = NowVarStr::decode(src)?; + let directory = NowVarStr::decode(src)?; let msg = Self { + flags, session_id, command, shell, + directory, }; - msg.ensure_message_size()?; - Ok(msg) } } -impl Encode for NowExecShellMsg { +impl Encode for NowExecShellMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, class: NowMessageClass::EXEC, kind: NowExecMsgKind::SHELL.0, - flags: 0, + flags: self.flags.bits(), }; header.encode(dst)?; @@ -92,6 +165,7 @@ impl Encode for NowExecShellMsg { dst.write_u32(self.session_id); self.command.encode(dst)?; self.shell.encode(dst)?; + self.directory.encode(dst)?; Ok(()) } @@ -107,8 +181,8 @@ impl Encode for NowExecShellMsg { } } -impl Decode<'_> for NowExecShellMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowExecShellMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowExecMsgKind(header.kind)) { @@ -118,8 +192,8 @@ impl Decode<'_> for NowExecShellMsg { } } -impl From for NowMessage { - fn from(msg: NowExecShellMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowExecShellMsg<'a>) -> Self { NowMessage::Exec(NowExecMessage::Shell(msg)) } } diff --git a/crates/now-proto-pdu/src/exec/started.rs b/crates/now-proto-pdu/src/exec/started.rs new file mode 100644 index 0000000..0dadbea --- /dev/null +++ b/crates/now-proto-pdu/src/exec/started.rs @@ -0,0 +1,81 @@ +use ironrdp_core::{ + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, + WriteCursor, +}; + +use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass}; + +/// The NOW_EXEC_STARTED_MSG message is sent by the server after the execution session has been +/// successfully started. +/// +/// NOW-PROTO: NOW_EXEC_STARTED_MSG +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NowExecStartedMsg { + session_id: u32, +} + +impl NowExecStartedMsg { + const NAME: &'static str = "NOW_EXEC_STARTED_MSG"; + const FIXED_PART_SIZE: usize = 4; + + pub fn new(session_id: u32) -> Self { + Self { session_id } + } + + pub fn session_id(&self) -> u32 { + self.session_id + } + + pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let session_id = src.read_u32(); + + Ok(Self { session_id }) + } +} + +impl Encode for NowExecStartedMsg { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + let header = NowHeader { + size: cast_length!("size", Self::FIXED_PART_SIZE)?, + class: NowMessageClass::EXEC, + kind: NowExecMsgKind::STARTED.0, + flags: 0, + }; + + header.encode(dst)?; + + ensure_fixed_part_size!(in: dst); + dst.write_u32(self.session_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + Self::NAME + } + + // LINTS: Overall message size always fits into usize + #[allow(clippy::arithmetic_side_effects)] + fn size(&self) -> usize { + NowHeader::FIXED_PART_SIZE + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for NowExecStartedMsg { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + let header = NowHeader::decode(src)?; + + match (header.class, NowExecMsgKind(header.kind)) { + (NowMessageClass::EXEC, NowExecMsgKind::STARTED) => Self::decode_from_body(header, src), + _ => Err(invalid_field_err!("type", "invalid message type")), + } + } +} + +impl From for NowMessage<'_> { + fn from(msg: NowExecStartedMsg) -> Self { + NowMessage::Exec(NowExecMessage::Started(msg)) + } +} diff --git a/crates/now-proto-pdu/src/exec/win_ps.rs b/crates/now-proto-pdu/src/exec/win_ps.rs index 0eb4a22..3db616a 100644 --- a/crates/now-proto-pdu/src/exec/win_ps.rs +++ b/crates/now-proto-pdu/src/exec/win_ps.rs @@ -1,7 +1,9 @@ +use alloc::borrow::Cow; + use bitflags::bitflags; use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageClass, NowVarStr}; @@ -9,7 +11,7 @@ use crate::{NowExecMessage, NowExecMsgKind, NowHeader, NowMessage, NowMessageCla bitflags! { /// NOW-PROTO: NOW_EXEC_WINPS_MSG msgFlags field. #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct NowExecWinPsFlags: u16 { + pub(crate) struct NowExecWinPsFlags: u16 { /// PowerShell -NoLogo option. /// /// NOW-PROTO: NOW_EXEC_FLAG_PS_NO_LOGO @@ -44,6 +46,45 @@ bitflags! { /// /// NOW-PROTO: NOW_EXEC_FLAG_PS_CONFIGURATION_NAME const CONFIGURATION_NAME = 0x0080; + + /// `directory` field contains non-default value and specifies command working directory. + /// + /// NOW-PROTO: NOW_EXEC_FLAG_PS_DIRECTORY_SET + const DIRECTORY_SET = 0x0100; + } +} + +/// COM apartment state ([MSDN](https://learn.microsoft.com/en-us/dotnet/api/system.threading.apartmentstate)). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ComApartmentStateKind { + Sta, + Mta, +} + +impl ComApartmentStateKind { + pub(crate) fn to_flags(self) -> NowExecWinPsFlags { + match self { + ComApartmentStateKind::Sta => NowExecWinPsFlags::STA, + ComApartmentStateKind::Mta => NowExecWinPsFlags::MTA, + } + } + + pub(crate) fn from_flags(flags: NowExecWinPsFlags) -> DecodeResult> { + let flags = flags & (NowExecWinPsFlags::STA | NowExecWinPsFlags::MTA); + + // Exactly one apartment state flag should be set + + match flags.iter().count() { + 0 => return Ok(None), + 1 => {} + _ => return Err(invalid_field_err!("flags", "multiple apartment state flags set")), + } + + match flags { + NowExecWinPsFlags::STA => Ok(Some(ComApartmentStateKind::Sta)), + NowExecWinPsFlags::MTA => Ok(Some(ComApartmentStateKind::Mta)), + _ => unreachable!("validated by code above"), + } } } @@ -51,25 +92,44 @@ bitflags! { /// /// NOW-PROTO: NOW_EXEC_WINPS_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowExecWinPsMsg { +pub struct NowExecWinPsMsg<'a> { flags: NowExecWinPsFlags, session_id: u32, - command: NowVarStr, - execution_policy: NowVarStr, - configuration_name: NowVarStr, + command: NowVarStr<'a>, + directory: NowVarStr<'a>, + execution_policy: NowVarStr<'a>, + configuration_name: NowVarStr<'a>, } -impl NowExecWinPsMsg { +impl_pdu_borrowing!(NowExecWinPsMsg<'_>, OwnedNowExecWinPsMsg); + +impl IntoOwned for NowExecWinPsMsg<'_> { + type Owned = OwnedNowExecWinPsMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowExecWinPsMsg { + flags: self.flags, + session_id: self.session_id, + command: self.command.into_owned(), + directory: self.directory.into_owned(), + execution_policy: self.execution_policy.into_owned(), + configuration_name: self.configuration_name.into_owned(), + } + } +} + +impl<'a> NowExecWinPsMsg<'a> { const NAME: &'static str = "NOW_EXEC_WINPS_MSG"; const FIXED_PART_SIZE: usize = 4; - pub fn new(session_id: u32, command: NowVarStr) -> DecodeResult { + pub fn new(session_id: u32, command: impl Into>) -> EncodeResult { let msg = Self { - session_id, - command, flags: NowExecWinPsFlags::empty(), - execution_policy: NowVarStr::empty(), - configuration_name: NowVarStr::empty(), + session_id, + command: NowVarStr::new(command)?, + directory: NowVarStr::default(), + execution_policy: NowVarStr::default(), + configuration_name: NowVarStr::default(), }; msg.ensure_message_size()?; @@ -77,53 +137,86 @@ impl NowExecWinPsMsg { Ok(msg) } - fn ensure_message_size(&self) -> DecodeResult<()> { - let _message_size = Self::FIXED_PART_SIZE - .checked_add(self.command.size()) - .and_then(|size| size.checked_add(self.execution_policy.size())) - .and_then(|size| size.checked_add(self.configuration_name.size())) - .ok_or_else(|| invalid_field_err!("size", "message size overflow"))?; + pub fn with_directory(mut self, directory: impl Into>) -> EncodeResult { + self.flags |= NowExecWinPsFlags::DIRECTORY_SET; + self.directory = NowVarStr::new(directory)?; - Ok(()) - } + self.ensure_message_size()?; - #[must_use] - pub fn with_flags(mut self, flags: NowExecWinPsFlags) -> Self { - self.flags = flags; - self + Ok(self) } - pub fn with_execution_policy(mut self, execution_policy: NowVarStr) -> DecodeResult { - self.execution_policy = execution_policy; + pub fn with_execution_policy(mut self, execution_policy: impl Into>) -> EncodeResult { self.flags |= NowExecWinPsFlags::EXECUTION_POLICY; + self.execution_policy = NowVarStr::new(execution_policy)?; self.ensure_message_size()?; Ok(self) } - pub fn with_configuration_name(mut self, configuration_name: NowVarStr) -> DecodeResult { - self.configuration_name = configuration_name; + pub fn with_configuration_name(mut self, configuration_name: impl Into>) -> EncodeResult { self.flags |= NowExecWinPsFlags::CONFIGURATION_NAME; + self.configuration_name = NowVarStr::new(configuration_name)?; self.ensure_message_size()?; Ok(self) } - pub fn flags(&self) -> NowExecWinPsFlags { - self.flags + #[must_use] + pub fn set_no_logo(mut self) -> Self { + self.flags |= NowExecWinPsFlags::NO_LOGO; + self + } + + #[must_use] + pub fn set_no_exit(mut self) -> Self { + self.flags |= NowExecWinPsFlags::NO_EXIT; + self + } + + #[must_use] + pub fn set_no_profile(mut self) -> Self { + self.flags |= NowExecWinPsFlags::NO_PROFILE; + self + } + + #[must_use] + pub fn with_apartment_state(mut self, apartment_state: ComApartmentStateKind) -> Self { + self.flags |= apartment_state.to_flags(); + self + } + + fn ensure_message_size(&self) -> EncodeResult<()> { + ensure_now_message_size!( + Self::FIXED_PART_SIZE, + self.command.size(), + self.directory.size(), + self.execution_policy.size(), + self.configuration_name.size() + ); + + Ok(()) } pub fn session_id(&self) -> u32 { self.session_id } - pub fn command(&self) -> &NowVarStr { + pub fn command(&self) -> &str { &self.command } - pub fn execution_policy(&self) -> Option<&NowVarStr> { + pub fn directory(&self) -> Option<&str> { + if self.flags.contains(NowExecWinPsFlags::DIRECTORY_SET) { + Some(&self.directory) + } else { + None + } + } + + pub fn execution_policy(&self) -> Option<&str> { if self.flags.contains(NowExecWinPsFlags::EXECUTION_POLICY) { Some(&self.execution_policy) } else { @@ -131,7 +224,7 @@ impl NowExecWinPsMsg { } } - pub fn configuration_name(&self) -> Option<&NowVarStr> { + pub fn configuration_name(&self) -> Option<&str> { if self.flags.contains(NowExecWinPsFlags::CONFIGURATION_NAME) { Some(&self.configuration_name) } else { @@ -139,18 +232,39 @@ impl NowExecWinPsMsg { } } + pub fn is_no_logo(&self) -> bool { + self.flags.contains(NowExecWinPsFlags::NO_LOGO) + } + + pub fn is_no_exit(&self) -> bool { + self.flags.contains(NowExecWinPsFlags::NO_EXIT) + } + + pub fn is_no_profile(&self) -> bool { + self.flags.contains(NowExecWinPsFlags::NO_PROFILE) + } + + pub fn apartment_state(&self) -> DecodeResult> { + ComApartmentStateKind::from_flags(self.flags) + } + // LINTS: Overall message size is validated in the constructor/decode method #[allow(clippy::arithmetic_side_effects)] fn body_size(&self) -> usize { - Self::FIXED_PART_SIZE + self.command.size() + self.execution_policy.size() + self.configuration_name.size() + Self::FIXED_PART_SIZE + + self.command.size() + + self.directory.size() + + self.execution_policy.size() + + self.configuration_name.size() } - pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); let flags = NowExecWinPsFlags::from_bits_retain(header.flags); let session_id = src.read_u32(); let command = NowVarStr::decode(src)?; + let directory = NowVarStr::decode(src)?; let execution_policy = NowVarStr::decode(src)?; let configuration_name = NowVarStr::decode(src)?; @@ -158,17 +272,16 @@ impl NowExecWinPsMsg { flags, session_id, command, + directory, execution_policy, configuration_name, }; - msg.ensure_message_size()?; - Ok(msg) } } -impl Encode for NowExecWinPsMsg { +impl Encode for NowExecWinPsMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, @@ -182,6 +295,7 @@ impl Encode for NowExecWinPsMsg { ensure_fixed_part_size!(in: dst); dst.write_u32(self.session_id); self.command.encode(dst)?; + self.directory.encode(dst)?; self.execution_policy.encode(dst)?; self.configuration_name.encode(dst)?; @@ -199,8 +313,8 @@ impl Encode for NowExecWinPsMsg { } } -impl Decode<'_> for NowExecWinPsMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowExecWinPsMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowExecMsgKind(header.kind)) { @@ -210,8 +324,8 @@ impl Decode<'_> for NowExecWinPsMsg { } } -impl From for NowMessage { - fn from(msg: NowExecWinPsMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowExecWinPsMsg<'a>) -> Self { NowMessage::Exec(NowExecMessage::WinPs(msg)) } } diff --git a/crates/now-proto-pdu/src/lib.rs b/crates/now-proto-pdu/src/lib.rs index c4c85a6..6e2fa93 100644 --- a/crates/now-proto-pdu/src/lib.rs +++ b/crates/now-proto-pdu/src/lib.rs @@ -1,25 +1,17 @@ -#![doc = include_str!("../../../README.md")] +#![doc = include_str!("../README.md")] #![doc( html_logo_url = "https://webdevolutions.blob.core.windows.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg" )] #![no_std] extern crate alloc; -extern crate ironrdp_core; -/// Asserts that constant expressions evaluate to `true`. -/// -/// From -#[macro_export] -macro_rules! const_assert { - ($x:expr $(,)?) => { - #[allow(unknown_lints, clippy::eq_op)] - const _: [(); 0 - !{ - const ASSERT: bool = $x; - ASSERT - } as usize] = []; - }; -} +// Re-export ironrdp crates to allow users to use it without additional imports. +pub extern crate ironrdp_core; +pub extern crate ironrdp_error; + +#[macro_use] +mod macros; // Ensure that we do not compile on platforms with less than 4 bytes per u32. It is pretty safe // to assume that NOW-PROTO will not ever be used on 8/16-bit MCUs or CPUs. @@ -27,9 +19,7 @@ macro_rules! const_assert { // This is required to safely cast u32 to usize without additional checks. const_assert!(size_of::() >= 4); -#[macro_use] -mod macros; - +mod channel; mod core; mod exec; mod message; @@ -38,6 +28,7 @@ mod system; pub use core::*; +pub use channel::*; pub use exec::*; pub use message::*; pub use session::*; diff --git a/crates/now-proto-pdu/src/macros.rs b/crates/now-proto-pdu/src/macros.rs index e1fb25b..d16e7c3 100644 --- a/crates/now-proto-pdu/src/macros.rs +++ b/crates/now-proto-pdu/src/macros.rs @@ -1,5 +1,4 @@ /// Creates a `PduError` with `UnsupportedValue` kind -#[macro_export] macro_rules! unsupported_message_err { ( $name:expr, class: $class:expr, kind: $kind:expr $(,)? ) => {{ ironrdp_core::unsupported_value_err( @@ -12,3 +11,47 @@ macro_rules! unsupported_message_err { unsupported_message_err!(Self::NAME, class: $class, kind: $kind) }}; } + +/// Ensures that accumulated message size does not overflow u32 message size field. +macro_rules! ensure_now_message_size { + ($e:expr) => { + u32::try_from($e).map_err(|_| ironrdp_core::invalid_field_err!("size", "message size overflow"))?; + }; + ($e1:expr, $e2:expr) => { + $e1.checked_add($e2) + .ok_or_else(|| ironrdp_core::invalid_field_err!("size", "message size overflow"))?; + }; + + ($e1:expr, $e2:expr, $($er:expr),+) => { + $e1.checked_add($e2) + $(.and_then(|size| size.checked_add($er)))* + .ok_or_else(|| ironrdp_core::invalid_field_err!("size", "message size overflow"))?; + }; +} + +/// Asserts that constant expressions evaluate to `true`. +/// +/// From +macro_rules! const_assert { + ($x:expr $(,)?) => { + #[allow(unknown_lints, clippy::eq_op)] + const _: [(); 0 - !{ + const ASSERT: bool = $x; + ASSERT + } as usize] = []; + }; +} + +/// Implements additional traits for a borrowing PDU and defines a static-bounded owned version. +macro_rules! impl_pdu_borrowing { + ($pdu_ty:ident $(<$($lt:lifetime),+>)?, $owned_ty:ident) => { + pub type $owned_ty = $pdu_ty<'static>; + + impl $crate::ironrdp_core::DecodeOwned for $owned_ty { + fn decode_owned(src: &mut ReadCursor<'_>) -> DecodeResult { + let pdu = <$pdu_ty $(<$($lt),+>)? as $crate::ironrdp_core::Decode>::decode(src)?; + Ok($crate::ironrdp_core::IntoOwned::into_owned(pdu)) + } + } + }; +} diff --git a/crates/now-proto-pdu/src/message.rs b/crates/now-proto-pdu/src/message.rs index 259101f..e9affd6 100644 --- a/crates/now-proto-pdu/src/message.rs +++ b/crates/now-proto-pdu/src/message.rs @@ -1,24 +1,41 @@ -use ironrdp_core::{Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; +use ironrdp_core::{Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor}; -use crate::{NowExecMessage, NowHeader, NowMessageClass, NowSessionMessage, NowSystemMessage}; +use crate::{NowChannelMessage, NowExecMessage, NowHeader, NowMessageClass, NowSessionMessage, NowSystemMessage}; /// Wrapper type for messages transferred over the NOW-PROTO communication channel. /// /// NOW-PROTO: NOW_*_MSG messages #[derive(Debug, Clone, PartialEq, Eq)] -pub enum NowMessage { - System(NowSystemMessage), - Session(NowSessionMessage), - Exec(NowExecMessage), +pub enum NowMessage<'a> { + Channel(NowChannelMessage<'a>), + System(NowSystemMessage<'a>), + Session(NowSessionMessage<'a>), + Exec(NowExecMessage<'a>), } -impl NowMessage { +impl_pdu_borrowing!(NowMessage<'_>, OwnedNowMessage); + +impl IntoOwned for NowMessage<'_> { + type Owned = OwnedNowMessage; + + fn into_owned(self) -> Self::Owned { + match self { + Self::Channel(msg) => OwnedNowMessage::Channel(msg.into_owned()), + Self::System(msg) => OwnedNowMessage::System(msg.into_owned()), + Self::Session(msg) => OwnedNowMessage::Session(msg.into_owned()), + Self::Exec(msg) => OwnedNowMessage::Exec(msg.into_owned()), + } + } +} + +impl NowMessage<'_> { const NAME: &'static str = "NOW_MSG"; } -impl Encode for NowMessage { +impl Encode for NowMessage<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { match self { + Self::Channel(msg) => msg.encode(dst), Self::System(msg) => msg.encode(dst), Self::Session(msg) => msg.encode(dst), Self::Exec(msg) => msg.encode(dst), @@ -31,6 +48,7 @@ impl Encode for NowMessage { fn size(&self) -> usize { match self { + Self::Channel(msg) => msg.size(), Self::System(msg) => msg.size(), Self::Session(msg) => msg.size(), Self::Exec(msg) => msg.size(), @@ -38,16 +56,17 @@ impl Encode for NowMessage { } } -impl Decode<'_> for NowMessage { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowMessage<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; Self::decode_from_body(header, src) } } -impl NowMessage { - pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'a> NowMessage<'a> { + pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { match NowMessageClass(header.class.0) { + NowMessageClass::CHANNEL => Ok(Self::Channel(NowChannelMessage::decode_from_body(header, src)?)), NowMessageClass::SYSTEM => Ok(Self::System(NowSystemMessage::decode_from_body(header, src)?)), NowMessageClass::SESSION => Ok(Self::Session(NowSessionMessage::decode_from_body(header, src)?)), NowMessageClass::EXEC => Ok(Self::Exec(NowExecMessage::decode_from_body(header, src)?)), diff --git a/crates/now-proto-pdu/src/session/lock.rs b/crates/now-proto-pdu/src/session/lock.rs index d77f70b..09933b2 100644 --- a/crates/now-proto-pdu/src/session/lock.rs +++ b/crates/now-proto-pdu/src/session/lock.rs @@ -47,7 +47,7 @@ impl Decode<'_> for NowSessionLockMsg { } } -impl From for NowMessage { +impl From for NowMessage<'_> { fn from(val: NowSessionLockMsg) -> Self { NowMessage::Session(NowSessionMessage::Lock(val)) } diff --git a/crates/now-proto-pdu/src/session/logoff.rs b/crates/now-proto-pdu/src/session/logoff.rs index 8bba4ee..d8a72db 100644 --- a/crates/now-proto-pdu/src/session/logoff.rs +++ b/crates/now-proto-pdu/src/session/logoff.rs @@ -47,7 +47,7 @@ impl Decode<'_> for NowSessionLogoffMsg { } } -impl From for NowMessage { +impl From for NowMessage<'_> { fn from(msg: NowSessionLogoffMsg) -> Self { NowMessage::Session(NowSessionMessage::Logoff(msg)) } diff --git a/crates/now-proto-pdu/src/session/mod.rs b/crates/now-proto-pdu/src/session/mod.rs index 46a2e40..ec0e483 100644 --- a/crates/now-proto-pdu/src/session/mod.rs +++ b/crates/now-proto-pdu/src/session/mod.rs @@ -3,11 +3,11 @@ mod logoff; mod msg_box_req; mod msg_box_rsp; -use ironrdp_core::{DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; +use ironrdp_core::{DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor}; pub use lock::NowSessionLockMsg; pub use logoff::NowSessionLogoffMsg; -pub use msg_box_req::{NowMessageBoxStyle, NowSessionMsgBoxReqMsg}; -pub use msg_box_rsp::{NowMsgBoxResponse, NowSessionMsgBoxRspMsg}; +pub use msg_box_req::{NowMessageBoxStyle, NowSessionMsgBoxReqMsg, OwnedNowSessionMsgBoxReqMsg}; +pub use msg_box_rsp::{NowMsgBoxResponse, NowSessionMsgBoxRspMsg, OwnedNowSessionMsgBoxRspMsg}; use crate::NowHeader; @@ -28,17 +28,32 @@ impl NowSessionMessageKind { // Wrapper for the `NOW_SESSION_MSG_CLASS_ID` message class. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum NowSessionMessage { +pub enum NowSessionMessage<'a> { Lock(NowSessionLockMsg), Logoff(NowSessionLogoffMsg), - MsgBoxReq(NowSessionMsgBoxReqMsg), - MsgBoxRsp(NowSessionMsgBoxRspMsg), + MsgBoxReq(NowSessionMsgBoxReqMsg<'a>), + MsgBoxRsp(NowSessionMsgBoxRspMsg<'a>), } -impl NowSessionMessage { +pub type OwnedNowSessionMessage = NowSessionMessage<'static>; + +impl IntoOwned for NowSessionMessage<'_> { + type Owned = OwnedNowSessionMessage; + + fn into_owned(self) -> Self::Owned { + match self { + Self::Lock(msg) => OwnedNowSessionMessage::Lock(msg), + Self::Logoff(msg) => OwnedNowSessionMessage::Logoff(msg), + Self::MsgBoxReq(msg) => OwnedNowSessionMessage::MsgBoxReq(msg.into_owned()), + Self::MsgBoxRsp(msg) => OwnedNowSessionMessage::MsgBoxRsp(msg.into_owned()), + } + } +} + +impl<'a> NowSessionMessage<'a> { const NAME: &'static str = "NOW_SESSION_MSG"; - pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { match NowSessionMessageKind(header.kind) { NowSessionMessageKind::LOCK => Ok(Self::Lock(NowSessionLockMsg::default())), NowSessionMessageKind::LOGOFF => Ok(Self::Logoff(NowSessionLogoffMsg::default())), @@ -53,7 +68,7 @@ impl NowSessionMessage { } } -impl Encode for NowSessionMessage { +impl Encode for NowSessionMessage<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { match self { Self::Lock(msg) => msg.encode(dst), diff --git a/crates/now-proto-pdu/src/session/msg_box_req.rs b/crates/now-proto-pdu/src/session/msg_box_req.rs index f5e14f2..5627f6e 100644 --- a/crates/now-proto-pdu/src/session/msg_box_req.rs +++ b/crates/now-proto-pdu/src/session/msg_box_req.rs @@ -1,9 +1,10 @@ -use alloc::string::String; +use alloc::borrow::Cow; +use core::time; use bitflags::bitflags; use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, - WriteCursor, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, + ReadCursor, WriteCursor, }; use crate::{NowHeader, NowMessage, NowMessageClass, NowSessionMessage, NowSessionMessageKind, NowVarStr}; @@ -64,54 +65,57 @@ bitflags! { /// /// NOW_PROTO: NOW_SESSION_MSGBOX_REQ_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowSessionMsgBoxReqMsg { +pub struct NowSessionMsgBoxReqMsg<'a> { flags: NowSessionMessageBoxFlags, request_id: u32, style: NowMessageBoxStyle, timeout: u32, - title: NowVarStr, - message: NowVarStr, + title: NowVarStr<'a>, + message: NowVarStr<'a>, } -impl NowSessionMsgBoxReqMsg { +impl_pdu_borrowing!(NowSessionMsgBoxReqMsg<'_>, OwnedNowSessionMsgBoxReqMsg); + +impl IntoOwned for NowSessionMsgBoxReqMsg<'_> { + type Owned = OwnedNowSessionMsgBoxReqMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowSessionMsgBoxReqMsg { + flags: self.flags, + request_id: self.request_id, + style: self.style, + timeout: self.timeout, + title: self.title.into_owned(), + message: self.message.into_owned(), + } + } +} + +impl<'a> NowSessionMsgBoxReqMsg<'a> { const NAME: &'static str = "NOW_SESSION_MSGBOX_REQ_MSG"; const FIXED_PART_SIZE: usize = 12; - pub fn new(request_id: u32, message: NowVarStr) -> DecodeResult { + pub fn new(request_id: u32, message: impl Into>) -> EncodeResult { let msg = Self { flags: NowSessionMessageBoxFlags::empty(), request_id, style: NowMessageBoxStyle::OK, timeout: 0, - title: NowVarStr::new(String::new()).expect("empty string construction always succeeds"), - message, + title: NowVarStr::default(), + message: NowVarStr::new(message)?, }; - msg.ensure_message_size()?; - Ok(msg) } - fn ensure_message_size(&self) -> DecodeResult<()> { - let _message_size = Self::FIXED_PART_SIZE - .checked_add(self.title.size()) - .and_then(|size| size.checked_add(self.message.size())) - .ok_or_else(|| invalid_field_err!("size", "message size overflow"))?; - + fn ensure_message_size(&self) -> EncodeResult<()> { + ensure_now_message_size!(Self::FIXED_PART_SIZE, self.title.size(), self.message.size()); Ok(()) } - pub fn with_title(mut self, title: NowVarStr) -> DecodeResult { + pub fn with_title(mut self, title: impl Into>) -> EncodeResult { self.flags |= NowSessionMessageBoxFlags::TITLE; - self.title = title; - - self.ensure_message_size()?; - - Ok(self) - } - - pub fn with_message(mut self, message: NowVarStr) -> DecodeResult { - self.message = message; + self.title = NowVarStr::new(title)?; self.ensure_message_size()?; @@ -125,11 +129,19 @@ impl NowSessionMsgBoxReqMsg { self } - #[must_use] - pub fn with_timeout(mut self, timeout: u32) -> Self { + pub fn with_timeout(mut self, timeout: time::Duration) -> EncodeResult { + // Sanity check: Limit message box timeout to ~1 week. + const MAX_MSGBOX_TINEOUT: time::Duration = time::Duration::from_secs(60 * 60 * 24 * 7); + + if timeout > MAX_MSGBOX_TINEOUT { + return Err(invalid_field_err!("timeout", "too big message box timeout")); + } + + let timeout = u32::try_from(timeout.as_secs()).expect("timeout is within u32 range"); + self.flags |= NowSessionMessageBoxFlags::TIMEOUT; self.timeout = timeout; - self + Ok(self) } #[must_use] @@ -150,15 +162,15 @@ impl NowSessionMsgBoxReqMsg { } } - pub fn timeout(&self) -> Option { + pub fn timeout(&self) -> Option { if self.flags.contains(NowSessionMessageBoxFlags::TIMEOUT) && self.timeout > 0 { - Some(self.timeout) + Some(time::Duration::from_secs(self.timeout.into())) } else { None } } - pub fn title(&self) -> Option<&NowVarStr> { + pub fn title(&self) -> Option<&str> { if self.flags.contains(NowSessionMessageBoxFlags::TITLE) { Some(&self.title) } else { @@ -166,7 +178,7 @@ impl NowSessionMsgBoxReqMsg { } } - pub fn message(&self) -> &NowVarStr { + pub fn message(&self) -> &str { &self.message } @@ -180,7 +192,7 @@ impl NowSessionMsgBoxReqMsg { Self::FIXED_PART_SIZE + self.title.size() + self.message.size() } - pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); let flags = NowSessionMessageBoxFlags::from_bits_retain(header.flags); @@ -199,13 +211,11 @@ impl NowSessionMsgBoxReqMsg { message, }; - msg.ensure_message_size()?; - Ok(msg) } } -impl Encode for NowSessionMsgBoxReqMsg { +impl Encode for NowSessionMsgBoxReqMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, @@ -237,8 +247,8 @@ impl Encode for NowSessionMsgBoxReqMsg { } } -impl Decode<'_> for NowSessionMsgBoxReqMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowSessionMsgBoxReqMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowSessionMessageKind(header.kind)) { @@ -248,8 +258,8 @@ impl Decode<'_> for NowSessionMsgBoxReqMsg { } } -impl From for NowMessage { - fn from(val: NowSessionMsgBoxReqMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(val: NowSessionMsgBoxReqMsg<'a>) -> Self { NowMessage::Session(NowSessionMessage::MsgBoxReq(val)) } } diff --git a/crates/now-proto-pdu/src/session/msg_box_rsp.rs b/crates/now-proto-pdu/src/session/msg_box_rsp.rs index 4ff4175..695b3eb 100644 --- a/crates/now-proto-pdu/src/session/msg_box_rsp.rs +++ b/crates/now-proto-pdu/src/session/msg_box_rsp.rs @@ -1,6 +1,10 @@ -use ironrdp_core::{ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; +use ironrdp_core::{ + ensure_fixed_part_size, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor, +}; -use crate::{NowHeader, NowMessage, NowMessageClass, NowSessionMessage, NowSessionMessageKind}; +use crate::{ + NowHeader, NowMessage, NowMessageClass, NowSessionMessage, NowSessionMessageKind, NowStatus, NowStatusError, +}; /// Message box response; Directly maps to the WinAPI MessageBox function response. /// @@ -64,38 +68,76 @@ impl NowMsgBoxResponse { /// /// NOW_PROTO: NOW_SESSION_MSGBOX_RSP_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowSessionMsgBoxRspMsg { +pub struct NowSessionMsgBoxRspMsg<'a> { request_id: u32, response: NowMsgBoxResponse, + status: NowStatus<'a>, } -impl NowSessionMsgBoxRspMsg { +impl_pdu_borrowing!(NowSessionMsgBoxRspMsg<'_>, OwnedNowSessionMsgBoxRspMsg); + +impl IntoOwned for NowSessionMsgBoxRspMsg<'_> { + type Owned = OwnedNowSessionMsgBoxRspMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowSessionMsgBoxRspMsg { + request_id: self.request_id, + response: self.response, + status: self.status.into_owned(), + } + } +} + +impl<'a> NowSessionMsgBoxRspMsg<'a> { const NAME: &'static str = "NOW_SESSION_MSGBOX_RSP_MSG"; const FIXED_PART_SIZE: usize = 8; - pub fn new(request_id: u32, response: NowMsgBoxResponse) -> Self { - Self { request_id, response } + pub fn new_success(request_id: u32, response: NowMsgBoxResponse) -> Self { + Self { + request_id, + response, + status: NowStatus::new_success(), + } + } + + pub fn new_error(request_id: u32, error: impl Into) -> EncodeResult { + let msg = Self { + request_id, + response: NowMsgBoxResponse(0), + status: NowStatus::new_error(error), + }; + + ensure_now_message_size!(Self::FIXED_PART_SIZE, msg.status.size()); + + Ok(msg) } pub fn request_id(&self) -> u32 { self.request_id } - pub fn response(&self) -> NowMsgBoxResponse { - self.response + /// Get the response from the message box dialog. Returns (Err(_) if the request has failed). + pub fn to_result(&self) -> Result { + self.status.to_result().map(|_| self.response) } - pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub(super) fn decode_from_body(_header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); let request_id = src.read_u32(); let response = NowMsgBoxResponse(src.read_u32()); - Ok(Self { request_id, response }) + let status = NowStatus::decode(src)?; + + Ok(Self { + request_id, + response, + status, + }) } } -impl Encode for NowSessionMsgBoxRspMsg { +impl Encode for NowSessionMsgBoxRspMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: u32::try_from(Self::FIXED_PART_SIZE).expect("always fits in u32"), @@ -110,6 +152,8 @@ impl Encode for NowSessionMsgBoxRspMsg { dst.write_u32(self.request_id); dst.write_u32(self.response.value()); + self.status.encode(dst)?; + Ok(()) } @@ -118,12 +162,12 @@ impl Encode for NowSessionMsgBoxRspMsg { } fn size(&self) -> usize { - NowHeader::FIXED_PART_SIZE + Self::FIXED_PART_SIZE + NowHeader::FIXED_PART_SIZE + Self::FIXED_PART_SIZE + self.status.size() } } -impl Decode<'_> for NowSessionMsgBoxRspMsg { - fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { +impl<'de> Decode<'de> for NowSessionMsgBoxRspMsg<'de> { + fn decode(src: &mut ReadCursor<'de>) -> DecodeResult { let header = NowHeader::decode(src)?; match (header.class, NowSessionMessageKind(header.kind)) { @@ -133,8 +177,8 @@ impl Decode<'_> for NowSessionMsgBoxRspMsg { } } -impl From for NowMessage { - fn from(val: NowSessionMsgBoxRspMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(val: NowSessionMsgBoxRspMsg<'a>) -> Self { NowMessage::Session(NowSessionMessage::MsgBoxRsp(val)) } } diff --git a/crates/now-proto-pdu/src/system/mod.rs b/crates/now-proto-pdu/src/system/mod.rs index 7ce5f94..82f7425 100644 --- a/crates/now-proto-pdu/src/system/mod.rs +++ b/crates/now-proto-pdu/src/system/mod.rs @@ -1,20 +1,32 @@ mod shutdown; -use ironrdp_core::{DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; -pub use shutdown::{NowSystemShutdownFlags, NowSystemShutdownMsg}; +use ironrdp_core::{DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor}; +pub use shutdown::{NowSystemShutdownMsg, OwnedNowSystemShutdownMsg}; use crate::NowHeader; // Wrapper for the `NOW_SYSTEM_MSG_CLASS_ID` message class. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum NowSystemMessage { - Shutdown(NowSystemShutdownMsg), +pub enum NowSystemMessage<'a> { + Shutdown(NowSystemShutdownMsg<'a>), } -impl NowSystemMessage { +pub type OwnedNowSystemMessage = NowSystemMessage<'static>; + +impl IntoOwned for NowSystemMessage<'_> { + type Owned = OwnedNowSystemMessage; + + fn into_owned(self) -> Self::Owned { + match self { + Self::Shutdown(msg) => OwnedNowSystemMessage::Shutdown(msg.into_owned()), + } + } +} + +impl<'a> NowSystemMessage<'a> { const NAME: &'static str = "NOW_SYSTEM_MSG"; - pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { match NowSystemMessageKind(header.kind) { NowSystemMessageKind::SHUTDOWN => Ok(Self::Shutdown(NowSystemShutdownMsg::decode_from_body(header, src)?)), _ => Err(unsupported_message_err!(class: header.class.0, kind: header.kind)), @@ -22,7 +34,7 @@ impl NowSystemMessage { } } -impl Encode for NowSystemMessage { +impl Encode for NowSystemMessage<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { match self { Self::Shutdown(msg) => msg.encode(dst), diff --git a/crates/now-proto-pdu/src/system/shutdown.rs b/crates/now-proto-pdu/src/system/shutdown.rs index b16014c..bb760af 100644 --- a/crates/now-proto-pdu/src/system/shutdown.rs +++ b/crates/now-proto-pdu/src/system/shutdown.rs @@ -1,6 +1,9 @@ +use alloc::borrow::Cow; +use core::time; + use bitflags::bitflags; use ironrdp_core::{ - cast_length, ensure_fixed_part_size, invalid_field_err, Decode as _, DecodeResult, Encode, EncodeResult, + cast_length, ensure_fixed_part_size, invalid_field_err, Decode as _, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor, }; @@ -10,7 +13,7 @@ use crate::{NowHeader, NowMessage, NowMessageClass, NowSystemMessage, NowVarStr} bitflags! { /// NOW_PROTO: NOW_SYSTEM_SHUTDOWN_FLAG_* constants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct NowSystemShutdownFlags: u16 { + struct NowSystemShutdownFlags: u16 { /// Force shutdown /// /// NOW-PROTO: NOW_SHUTDOWN_FLAG_FORCE @@ -26,36 +29,79 @@ bitflags! { /// /// NOW_PROTO: NOW_SYSTEM_SHUTDOWN_MSG #[derive(Debug, Clone, PartialEq, Eq)] -pub struct NowSystemShutdownMsg { +pub struct NowSystemShutdownMsg<'a> { flags: NowSystemShutdownFlags, /// This system shutdown timeout, in seconds. timeout: u32, /// Optional shutdown message. - message: NowVarStr, + message: NowVarStr<'a>, +} + +pub type OwnedNowSystemShutdownMsg = NowSystemShutdownMsg<'static>; + +impl IntoOwned for NowSystemShutdownMsg<'_> { + type Owned = OwnedNowSystemShutdownMsg; + + fn into_owned(self) -> Self::Owned { + OwnedNowSystemShutdownMsg { + flags: self.flags, + timeout: self.timeout, + message: self.message.into_owned(), + } + } } -impl NowSystemShutdownMsg { +impl<'a> NowSystemShutdownMsg<'a> { const NAME: &'static str = "NOW_SYSTEM_SHUTDOWN_MSG"; const FIXED_PART_SIZE: usize = 4 /* u32 timeout */; - pub fn new(flags: NowSystemShutdownFlags, timeout: u32, message: NowVarStr) -> DecodeResult { + pub fn new(timeout: time::Duration, message: impl Into>) -> EncodeResult { + // Sanity check: Limit shutdown timeout to ~1 year. + const MAX_SHUTDOWN_TINEOUT: time::Duration = time::Duration::from_secs(60 * 60 * 24 * 365); + + if timeout > MAX_SHUTDOWN_TINEOUT { + return Err(invalid_field_err!("timeout", "too big shutdown timeout")); + } + + let timeout = u32::try_from(timeout.as_secs()).expect("timeout is within u32 range"); + let msg = Self { - flags, + flags: NowSystemShutdownFlags::empty(), timeout, - message, + message: NowVarStr::new(message)?, }; - msg.ensure_message_size()?; + ensure_now_message_size!(Self::FIXED_PART_SIZE, msg.message.size()); Ok(msg) } - fn ensure_message_size(&self) -> DecodeResult<()> { - let _message_size = Self::FIXED_PART_SIZE - .checked_add(self.message.size()) - .ok_or_else(|| invalid_field_err!("size", "message size overflow"))?; + #[must_use] + pub fn with_force_shutdown(mut self) -> Self { + self.flags |= NowSystemShutdownFlags::FORCE; + self + } - Ok(()) + #[must_use] + pub fn with_reboot(mut self) -> Self { + self.flags |= NowSystemShutdownFlags::REBOOT; + self + } + + pub fn is_force_shutdown(&self) -> bool { + self.flags.contains(NowSystemShutdownFlags::FORCE) + } + + pub fn is_reboot(&self) -> bool { + self.flags.contains(NowSystemShutdownFlags::REBOOT) + } + + pub fn timeout(&self) -> time::Duration { + time::Duration::from_secs(self.timeout.into()) + } + + pub fn message(&self) -> &str { + &self.message } // LINTS: Overall message size is validated in the constructor/decode method @@ -64,7 +110,7 @@ impl NowSystemShutdownMsg { Self::FIXED_PART_SIZE + self.message.size() } - pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'_>) -> DecodeResult { + pub fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'a>) -> DecodeResult { ensure_fixed_part_size!(in: src); let timeout = src.read_u32(); @@ -76,13 +122,11 @@ impl NowSystemShutdownMsg { message, }; - msg.ensure_message_size()?; - Ok(msg) } } -impl Encode for NowSystemShutdownMsg { +impl Encode for NowSystemShutdownMsg<'_> { fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { let header = NowHeader { size: cast_length!("size", self.body_size())?, @@ -111,8 +155,8 @@ impl Encode for NowSystemShutdownMsg { } } -impl From for NowMessage { - fn from(msg: NowSystemShutdownMsg) -> Self { +impl<'a> From> for NowMessage<'a> { + fn from(msg: NowSystemShutdownMsg<'a>) -> Self { NowMessage::System(NowSystemMessage::Shutdown(msg)) } } diff --git a/crates/now-proto-testsuite/Cargo.toml b/crates/now-proto-testsuite/Cargo.toml index 6dbbab0..4699935 100644 --- a/crates/now-proto-testsuite/Cargo.toml +++ b/crates/now-proto-testsuite/Cargo.toml @@ -17,3 +17,10 @@ harness = true [lints] workspace = true + +[dependencies] +now-proto-pdu = { path = "../now-proto-pdu" } +expect-test = "1" + +[dev-dependencies] +rstest = "0.23" \ No newline at end of file diff --git a/crates/now-proto-testsuite/src/lib.rs b/crates/now-proto-testsuite/src/lib.rs index 63cdd0b..24d3639 100644 --- a/crates/now-proto-testsuite/src/lib.rs +++ b/crates/now-proto-testsuite/src/lib.rs @@ -5,3 +5,5 @@ #![allow(clippy::cast_possible_wrap)] #![allow(clippy::cast_sign_loss)] #![allow(unused_crate_dependencies)] + +pub mod proto; diff --git a/crates/now-proto-testsuite/src/proto/mod.rs b/crates/now-proto-testsuite/src/proto/mod.rs new file mode 100644 index 0000000..d91fc56 --- /dev/null +++ b/crates/now-proto-testsuite/src/proto/mod.rs @@ -0,0 +1,18 @@ +use expect_test::Expect; +use now_proto_pdu::ironrdp_core::{Decode, IntoOwned, ReadCursor}; +use now_proto_pdu::NowMessage; + +pub fn now_msg_roundtrip(msg: impl Into>, expected_bytes: Expect) -> NowMessage<'static> { + let msg = msg.into(); + + let buf = now_proto_pdu::ironrdp_core::encode_vec(&msg).expect("failed to encode message"); + + expected_bytes.assert_eq(&format!("{:02X?}", buf)); + + let mut cursor = ReadCursor::new(&buf); + let decoded = NowMessage::decode(&mut cursor).expect("failed to decode message"); + + assert_eq!(msg, decoded); + + decoded.into_owned() +} diff --git a/crates/now-proto-testsuite/tests/main.rs b/crates/now-proto-testsuite/tests/main.rs index a67f9ea..a834799 100644 --- a/crates/now-proto-testsuite/tests/main.rs +++ b/crates/now-proto-testsuite/tests/main.rs @@ -1,4 +1,5 @@ #![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary +#![allow(clippy::unwrap_used)] // allow for tests //! Integration Tests (IT) //! @@ -11,4 +12,4 @@ //! Cargo will run all tests from a single binary in parallel, but //! binaries themselves are run sequentally. -// mod now_proto; +mod proto; diff --git a/crates/now-proto-testsuite/tests/proto/channel.rs b/crates/now-proto-testsuite/tests/proto/channel.rs new file mode 100644 index 0000000..f3ae7d0 --- /dev/null +++ b/crates/now-proto-testsuite/tests/proto/channel.rs @@ -0,0 +1,108 @@ +use core::time; + +use expect_test::expect; +use now_proto_pdu::*; +use now_proto_testsuite::proto::now_msg_roundtrip; + +#[test] +fn roundtrip_channel_capset() { + let msg = NowChannelCapsetMsg::default() + .with_exec_capset(NowExecCapsetFlags::STYLE_RUN | NowExecCapsetFlags::STYLE_SHELL) + .with_system_capset(NowSystemCapsetFlags::SHUTDOWN) + .with_session_capset(NowSessionCapsetFlags::MSGBOX) + .with_heartbeat_interval(time::Duration::from_secs(300)) + .unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0E, 00, 00, 00, 10, 01, 01, 00, 01, 00, 00, 00, 01, 00, 04, 00, 05, 00, 2C, 01, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Channel(NowChannelMessage::Capset(msg)) => msg, + _ => panic!("Expected NowChannelCapsetMsg"), + }; + + assert_eq!(actual.version().major, NowProtoVersion::CURRENT.major); + assert_eq!(actual.version().minor, NowProtoVersion::CURRENT.minor); + assert_eq!(actual.system_capset(), NowSystemCapsetFlags::SHUTDOWN); + assert_eq!(actual.session_capset(), NowSessionCapsetFlags::MSGBOX); + assert_eq!( + actual.exec_capset(), + NowExecCapsetFlags::STYLE_RUN | NowExecCapsetFlags::STYLE_SHELL + ); + assert_eq!(actual.heartbeat_interval(), Some(time::Duration::from_secs(300))); +} + +#[test] +fn roundtrip_channel_capset_simple() { + let msg = NowChannelCapsetMsg::default(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0E, 00, 00, 00, 10, 01, 00, 00, 01, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Channel(NowChannelMessage::Capset(msg)) => msg, + _ => panic!("Expected NowChannelCapsetMsg"), + }; + + assert!(actual.system_capset().is_empty()); + assert!(actual.session_capset().is_empty()); + assert!(actual.exec_capset().is_empty()); + assert!(actual.heartbeat_interval().is_none()); +} + +#[test] +fn roundtrip_channel_capset_too_big_heartbeat_interval() { + // Sanity check should fail + NowChannelCapsetMsg::default() + .with_heartbeat_interval(time::Duration::from_secs(60 * 60 * 24 * 2)) + .unwrap_err(); +} + +#[test] +fn roundtrip_channel_heartbeat() { + now_msg_roundtrip( + NowChannelHeartbeatMsg::default(), + expect!["[00, 00, 00, 00, 10, 02, 00, 00]"], + ); +} + +#[test] +fn roundtrip_channel_close_normal() { + let msg = NowChannelCloseMsg::default(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0A, 00, 00, 00, 10, 03, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Channel(NowChannelMessage::Close(msg)) => msg, + _ => panic!("Expected NowChannelCloseMsg"), + }; + + assert!(actual.to_result().is_ok()); +} + +#[test] +fn roundtrip_channel_close_error() { + let msg = NowChannelCloseMsg::from_error(NowStatusError::from(NowStatusErrorKind::Generic(0))).unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0A, 00, 00, 00, 10, 03, 00, 00, 01, 00, 00, 00, 00, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Channel(NowChannelMessage::Close(msg)) => msg, + _ => panic!("Expected NowChannelCloseMsg"), + }; + + assert_eq!( + actual.to_result(), + Err(NowStatusError::from(NowStatusErrorKind::Generic(0))) + ); +} diff --git a/crates/now-proto-testsuite/tests/proto/exec.rs b/crates/now-proto-testsuite/tests/proto/exec.rs new file mode 100644 index 0000000..f6a7f4c --- /dev/null +++ b/crates/now-proto-testsuite/tests/proto/exec.rs @@ -0,0 +1,431 @@ +use expect_test::expect; +use now_proto_pdu::*; +use now_proto_testsuite::proto::now_msg_roundtrip; + +#[test] +fn roundtrip_exec_abort() { + let msg = NowExecAbortMsg::new(0x12345678, 1); + + let decoded = now_msg_roundtrip( + msg, + expect!["[08, 00, 00, 00, 13, 01, 00, 00, 78, 56, 34, 12, 01, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Abort(msg)) => msg, + _ => panic!("Expected NowExecAbortMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.exit_code(), 1); +} + +#[test] +fn roundtrip_exec_cancel_req() { + let msg = NowExecCancelReqMsg::new(0x12345678); + + let decoded = now_msg_roundtrip(msg, expect!["[04, 00, 00, 00, 13, 02, 00, 00, 78, 56, 34, 12]"]); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::CancelReq(msg)) => msg, + _ => panic!("Expected NowExecCancelReqMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); +} + +#[test] +fn roundtrip_exec_cancel_rsp() { + let msg = NowExecCancelRspMsg::new_success(0x12345678); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0E, 00, 00, 00, 13, 03, 00, 00, 78, 56, 34, 12, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::CancelRsp(msg)) => msg, + _ => panic!("Expected NowExecCancelRspMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert!(actual.to_result().is_ok()); +} + +#[test] +fn roundtrip_exec_cancel_rsp_error() { + let msg = NowExecCancelRspMsg::new_error(0x12345678, NowStatusError::new_generic(0xDEADBEEF)).unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0E, 00, 00, 00, 13, 03, 00, 00, 78, 56, 34, 12, 01, 00, 00, 00, EF, BE, AD, DE, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::CancelRsp(msg)) => msg, + _ => panic!("Expected NowExecCancelRspMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.to_result().unwrap_err(), NowStatusError::new_generic(0xDEADBEEF)); +} + +#[test] +fn roundtrip_exec_result_success() { + let msg = NowExecResultMsg::new_success(0x12345678, 42); + + let decoded = now_msg_roundtrip( + msg, + expect![ + "[12, 00, 00, 00, 13, 04, 00, 00, 78, 56, 34, 12, 2A, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]" + ], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Result(msg)) => msg, + _ => panic!("Expected NowExecResultMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.to_result().unwrap(), 42); +} + +#[test] +fn roundtrip_exec_result_error() { + let msg = NowExecResultMsg::new_error( + 0x12345678, + NowStatusError::new_generic(0xDEADBEEF).with_message("ABC").unwrap(), + ) + .unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[15, 00, 00, 00, 13, 04, 00, 00, 78, 56, 34, 12, 00, 00, 00, 00, 03, 00, 00, 00, EF, BE, AD, DE, 03, 41, 42, 43, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Result(msg)) => msg, + _ => panic!("Expected NowExecResultMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!( + actual.to_result().unwrap_err(), + NowStatusError::new_generic(0xDEADBEEF).with_message("ABC").unwrap() + ); +} + +#[test] +fn roundtrip_exec_data() { + let msg = NowExecDataMsg::new(0x12345678, NowExecDataStreamKind::Stdout, true, &[0x01, 0x02, 0x03]).unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[08, 00, 00, 00, 13, 05, 05, 00, 78, 56, 34, 12, 03, 01, 02, 03]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Data(msg)) => msg, + _ => panic!("Expected NowExecDataMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.stream_kind().unwrap(), NowExecDataStreamKind::Stdout); + assert!(actual.is_last()); +} + +#[test] +fn roundtrip_exec_data_empty() { + let msg = NowExecDataMsg::new(0x12345678, NowExecDataStreamKind::Stdin, false, &[]).unwrap(); + + let decoded = now_msg_roundtrip(msg, expect!["[05, 00, 00, 00, 13, 05, 02, 00, 78, 56, 34, 12, 00]"]); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Data(msg)) => msg, + _ => panic!("Expected NowExecDataMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.stream_kind().unwrap(), NowExecDataStreamKind::Stdin); + assert!(!actual.is_last()); + assert_eq!(actual.data(), &[]); +} + +#[test] +fn roundtrip_exec_started() { + let msg = NowExecStartedMsg::new(0x12345678); + + let decoded = now_msg_roundtrip(msg, expect!["[04, 00, 00, 00, 13, 06, 00, 00, 78, 56, 34, 12]"]); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Started(msg)) => msg, + _ => panic!("Expected NowExecStartedMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); +} + +#[test] +fn roundtrip_exec_run() { + let msg = NowExecRunMsg::new(0x1234567, "hello").unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0B, 00, 00, 00, 13, 10, 00, 00, 67, 45, 23, 01, 05, 68, 65, 6C, 6C, 6F, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Run(msg)) => msg, + _ => panic!("Expected NowExecRunMsg"), + }; + + assert_eq!(actual.session_id(), 0x1234567); + assert_eq!(actual.command(), "hello"); +} + +#[test] +fn roundtrip_exec_process() { + let msg = NowExecProcessMsg::new(0x12345678, "a") + .unwrap() + .with_parameters("b") + .unwrap() + .with_directory("c") + .unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0D, 00, 00, 00, 13, 11, 03, 00, 78, 56, 34, 12, 01, 61, 00, 01, 62, 00, 01, 63, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Process(msg)) => msg, + _ => panic!("Expected NowExecProcessMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.filename(), "a"); + assert_eq!(actual.parameters().unwrap(), "b"); + assert_eq!(actual.directory().unwrap(), "c"); +} + +#[test] +fn roundtrip_exec_process_simple() { + let msg = NowExecProcessMsg::new(0x12345678, "a").unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0B, 00, 00, 00, 13, 11, 00, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Process(msg)) => msg, + _ => panic!("Expected NowExecProcessMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.filename(), "a"); + assert!(actual.parameters().is_none()); + assert!(actual.directory().is_none()); +} + +#[test] +fn roundtrip_exec_shell() { + let msg = NowExecShellMsg::new(0x12345678, "a") + .unwrap() + .with_shell("b") + .unwrap() + .with_directory("c") + .unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0D, 00, 00, 00, 13, 12, 03, 00, 78, 56, 34, 12, 01, 61, 00, 01, 62, 00, 01, 63, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Shell(msg)) => msg, + _ => panic!("Expected NowExecShellMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.command(), "a"); + assert_eq!(actual.shell().unwrap(), "b"); + assert_eq!(actual.directory().unwrap(), "c"); +} + +#[test] +fn roundtrip_exec_shell_simple() { + let msg = NowExecShellMsg::new(0x12345678, "a").unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0B, 00, 00, 00, 13, 12, 00, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Shell(msg)) => msg, + _ => panic!("Expected NowExecShellMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.command(), "a"); + assert!(actual.shell().is_none()); + assert!(actual.directory().is_none()); +} + +#[test] +fn roundtrip_exec_batch() { + let msg = NowExecBatchMsg::new(0x12345678, "a") + .unwrap() + .with_directory("b") + .unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0A, 00, 00, 00, 13, 13, 01, 00, 78, 56, 34, 12, 01, 61, 00, 01, 62, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Batch(msg)) => msg, + _ => panic!("Expected NowExecBatchMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.command(), "a"); + assert_eq!(actual.directory().unwrap(), "b"); +} + +#[test] +fn roundtrip_exec_batch_simple() { + let msg = NowExecBatchMsg::new(0x12345678, "a").unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[09, 00, 00, 00, 13, 13, 00, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Batch(msg)) => msg, + _ => panic!("Expected NowExecBatchMsg"), + }; + + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.command(), "a"); + assert!(actual.directory().is_none()); +} + +#[test] +fn roundtrip_exec_ps() { + let msg = NowExecWinPsMsg::new(0x12345678, "a") + .unwrap() + .with_apartment_state(ComApartmentStateKind::Mta) + .set_no_profile() + .set_no_logo() + .with_directory("d") + .unwrap() + .with_execution_policy("b") + .unwrap() + .with_configuration_name("c") + .unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[10, 00, 00, 00, 13, 14, D9, 01, 78, 56, 34, 12, 01, 61, 00, 01, 64, 00, 01, 62, 00, 01, 63, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::WinPs(msg)) => msg, + _ => panic!("Expected NowExecPwshMsg::WinPs"), + }; + + assert!(actual.is_no_profile()); + assert!(actual.is_no_logo()); + assert_eq!(actual.apartment_state().unwrap(), Some(ComApartmentStateKind::Mta)); + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.command(), "a"); + assert_eq!(actual.directory().unwrap(), "d"); + assert_eq!(actual.execution_policy().unwrap(), "b"); + assert_eq!(actual.configuration_name().unwrap(), "c"); +} + +#[test] +fn roundtrip_exec_ps_simple() { + let msg = NowExecWinPsMsg::new(0x12345678, "a").unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0D, 00, 00, 00, 13, 14, 00, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::WinPs(msg)) => msg, + _ => panic!("Expected NowExecPwshMsg::WinPs"), + }; + + assert!(!actual.is_no_profile()); + assert!(!actual.is_no_logo()); + assert!(actual.apartment_state().unwrap().is_none()); + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.command(), "a"); + assert!(actual.directory().is_none()); + assert!(actual.execution_policy().is_none()); + assert!(actual.configuration_name().is_none()); +} + +#[test] +fn roundtrip_exec_pwsh() { + let msg = NowExecPwshMsg::new(0x12345678, "a") + .unwrap() + .with_apartment_state(ComApartmentStateKind::Mta) + .set_no_profile() + .set_no_logo() + .with_directory("d") + .unwrap() + .with_execution_policy("b") + .unwrap() + .with_configuration_name("c") + .unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[10, 00, 00, 00, 13, 15, D9, 01, 78, 56, 34, 12, 01, 61, 00, 01, 64, 00, 01, 62, 00, 01, 63, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Pwsh(msg)) => msg, + _ => panic!("Expected NowExecPwshMsg::Pwsh"), + }; + + assert!(actual.is_no_profile()); + assert!(actual.is_no_logo()); + assert_eq!(actual.apartment_state().unwrap(), Some(ComApartmentStateKind::Mta)); + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.command(), "a"); + assert_eq!(actual.directory().unwrap(), "d"); + assert_eq!(actual.execution_policy().unwrap(), "b"); + assert_eq!(actual.configuration_name().unwrap(), "c"); +} + +#[test] +fn roundtrip_exec_pwsh_simple() { + let msg = NowExecPwshMsg::new(0x12345678, "a").unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0D, 00, 00, 00, 13, 15, 00, 00, 78, 56, 34, 12, 01, 61, 00, 00, 00, 00, 00, 00, 00]"], + ); + + let actual = match decoded { + NowMessage::Exec(NowExecMessage::Pwsh(msg)) => msg, + _ => panic!("Expected NowExecPwshMsg::Pwsh"), + }; + + assert!(!actual.is_no_profile()); + assert!(!actual.is_no_logo()); + assert!(actual.apartment_state().unwrap().is_none()); + assert_eq!(actual.session_id(), 0x12345678); + assert_eq!(actual.command(), "a"); + assert!(actual.directory().is_none()); + assert!(actual.execution_policy().is_none()); + assert!(actual.configuration_name().is_none()); +} diff --git a/crates/now-proto-testsuite/tests/proto/mod.rs b/crates/now-proto-testsuite/tests/proto/mod.rs new file mode 100644 index 0000000..48ad78c --- /dev/null +++ b/crates/now-proto-testsuite/tests/proto/mod.rs @@ -0,0 +1,4 @@ +mod channel; +mod exec; +mod session; +mod system; diff --git a/crates/now-proto-testsuite/tests/proto/session.rs b/crates/now-proto-testsuite/tests/proto/session.rs new file mode 100644 index 0000000..4232ed0 --- /dev/null +++ b/crates/now-proto-testsuite/tests/proto/session.rs @@ -0,0 +1,119 @@ +use expect_test::expect; +use now_proto_pdu::*; +use now_proto_testsuite::proto::now_msg_roundtrip; + +#[test] +fn roundtrip_session_lock() { + now_msg_roundtrip( + NowSessionLockMsg::default(), + expect!["[00, 00, 00, 00, 12, 01, 00, 00]"], + ); +} + +#[test] +fn roundtrip_session_logoff() { + now_msg_roundtrip( + NowSessionLogoffMsg::default(), + expect!["[00, 00, 00, 00, 12, 02, 00, 00]"], + ); +} + +#[test] +fn roundtrip_session_msgbox_req() { + let msg = NowSessionMsgBoxReqMsg::new(0x76543210, "hello") + .unwrap() + .with_response() + .with_style(NowMessageBoxStyle::ABORT_RETRY_IGNORE) + .with_title("world") + .unwrap() + .with_timeout(core::time::Duration::from_secs(3)) + .unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[1A, 00, 00, 00, 12, 03, 0F, 00, 10, 32, 54, 76, 02, 00, 00, 00, 03, 00, 00, 00, 05, 77, 6F, 72, 6C, 64, 00, 05, 68, 65, 6C, 6C, 6F, 00]"] + ); + + let actual = match decoded { + NowMessage::Session(NowSessionMessage::MsgBoxReq(msg)) => msg, + _ => panic!("Expected NowSessionMsgBoxReqMsg"), + }; + + assert_eq!(actual.request_id(), 0x76543210); + assert_eq!(actual.message(), "hello"); + assert!(actual.is_response_expected()); + assert_eq!(actual.style(), NowMessageBoxStyle::ABORT_RETRY_IGNORE); + assert_eq!(actual.title().unwrap(), "world"); + assert_eq!(actual.timeout().unwrap(), core::time::Duration::from_secs(3)); +} + +#[test] +fn roundtrip_session_msgbox_req_simple() { + let msg = NowSessionMsgBoxReqMsg::new(0x76543210, "hello").unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[15, 00, 00, 00, 12, 03, 00, 00, 10, 32, 54, 76, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 05, 68, 65, 6C, 6C, 6F, 00]"] + ); + + let actual = match decoded { + NowMessage::Session(NowSessionMessage::MsgBoxReq(msg)) => msg, + _ => panic!("Expected NowSessionMsgBoxReqMsg"), + }; + + assert_eq!(actual.request_id(), 0x76543210); + assert_eq!(actual.message(), "hello"); + assert!(!actual.is_response_expected()); + assert_eq!(actual.style(), NowMessageBoxStyle::OK); + assert!(actual.title().is_none()); + assert!(actual.timeout().is_none()); +} + +#[test] +fn roundtrip_session_msgbox_rsp() { + let msg = NowSessionMsgBoxRspMsg::new_success(0x01234567, NowMsgBoxResponse::RETRY); + + let decoded = now_msg_roundtrip( + msg, + expect![ + "[08, 00, 00, 00, 12, 04, 00, 00, 67, 45, 23, 01, 04, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00]" + ], + ); + + let actual = match decoded { + NowMessage::Session(NowSessionMessage::MsgBoxRsp(msg)) => msg, + _ => panic!("Expected NowSessionMsgBoxRspMsg"), + }; + + assert_eq!(actual.request_id(), 0x01234567); + assert_eq!(actual.to_result().unwrap(), NowMsgBoxResponse::RETRY); +} + +#[test] +fn roundtrip_session_msgbox_rsp_error() { + let msg = NowSessionMsgBoxRspMsg::new_error( + 0x01234567, + NowStatusError::from(NowStatusErrorKind::Now(NowProtoError::NotImplemented)) + .with_message("err") + .unwrap(), + ) + .unwrap(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[08, 00, 00, 00, 12, 04, 00, 00, 67, 45, 23, 01, 00, 00, 00, 00, 03, 00, 01, 00, 07, 00, 00, 00, 03, 65, 72, 72, 00]"], + ); + + let actual = match decoded { + NowMessage::Session(NowSessionMessage::MsgBoxRsp(msg)) => msg, + _ => panic!("Expected NowSessionMsgBoxRspMsg"), + }; + + assert_eq!(actual.request_id(), 0x01234567); + assert_eq!( + actual.to_result().unwrap_err(), + NowStatusError::from(NowStatusErrorKind::Now(NowProtoError::NotImplemented)) + .with_message("err") + .unwrap() + ); +} diff --git a/crates/now-proto-testsuite/tests/proto/system.rs b/crates/now-proto-testsuite/tests/proto/system.rs new file mode 100644 index 0000000..1900ddb --- /dev/null +++ b/crates/now-proto-testsuite/tests/proto/system.rs @@ -0,0 +1,22 @@ +use expect_test::expect; +use now_proto_pdu::*; +use now_proto_testsuite::proto::now_msg_roundtrip; + +#[test] +fn roundtrip_system_shutdown() { + let msg = NowSystemShutdownMsg::new(core::time::Duration::from_secs(123), "hello") + .unwrap() + .with_force_shutdown(); + + let decoded = now_msg_roundtrip( + msg, + expect!["[0B, 00, 00, 00, 11, 03, 01, 00, 7B, 00, 00, 00, 05, 68, 65, 6C, 6C, 6F, 00]"], + ); + + let actual = match decoded { + NowMessage::System(NowSystemMessage::Shutdown(msg)) => msg, + _ => panic!("Expected NowSystemShutdownMsg"), + }; + + assert_eq!(actual.timeout(), core::time::Duration::from_secs(123)); +} diff --git a/doc/NOW-spec.md b/docs/NOW-spec.md similarity index 92% rename from doc/NOW-spec.md rename to docs/NOW-spec.md index e450440..53b0eb3 100644 --- a/doc/NOW-spec.md +++ b/docs/NOW-spec.md @@ -30,7 +30,8 @@ The NOW virtual channel protocol use an RDP dynamic virtual channel ("Devolution ## Message Syntax -The following sections specify the NOW protocol message syntax. Unless otherwise specified, all fields defined in this document use the little-endian format. +The following sections specify the NOW protocol message syntax. +Unless otherwise specified, all fields defined in this document use the little-endian format. ### Common Structures @@ -179,8 +180,8 @@ Operation status code. | Value | Meaning | |-------|---------| -| NOW_NOW_STATUS_ERROR
0x0001 | This flag set for all error statuses. If flag is not set, operation was successful. | -| NOW_NOW_STATUS_ERROR_MESSAGE
0x0002 | `errorMessage` contains optional error message. | +| NOW_STATUS_ERROR
0x0001 | This flag set for all error statuses. If flag is not set, operation was successful. | +| NOW_STATUS_ERROR_MESSAGE
0x0002 | `errorMessage` contains optional error message. | **kind (1 byte)**: The status kind. When `NOW_STATUS_ERROR` is set, this field represents error kind. @@ -214,12 +215,13 @@ For successful operation this field value is operation specific. | NOW_CODE_ACCESS_DENIED
0x0005 | Resource can't be accessed. | | NOW_CODE_INTERNAL
0x0006 | Internal error. | | NOW_CODE_NOT_IMPLEMENTED
0x0007 | Operation is not implemented on current platform. | + | NOW_CODE_PROTOCOL_VERSION
0x0008 | Incompatible protocol versions. | - `NOW_STATUS_ERROR_KIND_WINAPI`: code contains standard WinAPI error. - `NOW_STATUS_ERROR_KIND_UNIX`: code contains standard UNIX error code. -**errorMessage(variable, optional)**: this value contains either optional error message if -`NOW_STATUS_ERROR_MESSAGE` flag is set, or empty sting if the flag is not set. +**errorMessage(variable)**: this value contains either an error message if +`NOW_STATUS_ERROR_MESSAGE` flag is set, or empty string if the flag is not set. ### Channel Messages Channel negotiation and life cycle messages. @@ -257,13 +259,15 @@ Channel negotiation and life cycle messages. |---------------------------------|----------------------| | NOW_CHANNEL_CAPSET_MSG_ID
0x01 | NOW_CHANNEL_CAPSET_MSG | | NOW_CHANNEL_HEARTBEAT_MSG_ID
0x02 | NOW_CHANNEL_HEARTBEAT_MSG | +| NOW_CHANNEL_CLOSE_MSG_ID
0x03 | NOW_CHANNEL_CLOSE_MSG | #### NOW_CHANNEL_CAPSET_MSG This message is first set by the client side, to advertise capabilities. -Received client message should be downgraded by the server (remove non-intersecting capabilities) and sent back to the client at the start of DVC channel communications. DVC channel should be closed if protocol -versions are not compatible. +Received client message should be downgraded by the server (remove non-intersecting capabilities) +and sent back to the client at the start of DVC channel communications. DVC channel should be +closed if protocol versions are not compatible. @@ -329,8 +333,8 @@ increment major version; Protocol implementations with different major version a | Flag | Meaning | |-------|---------| -| NOW_CAP_SESSION_LOCK
0x00000001 | Lock command support. | -| NOW_CAP_SESSION_LOGOFF
0x00000002 | Logoff command support. | +| NOW_CAP_SESSION_LOCK
0x00000001 | Session lock command support. | +| NOW_CAP_SESSION_LOGOFF
0x00000002 | Session logoff command support. | | NOW_CAP_SESSION_MSGBOX
0x00000004 | Message box command support. | **execCapset (4 bytes)**: Remote execution capabilities set. @@ -385,6 +389,45 @@ the specified interval, it should consider the connection as lost. **msgFlags (2 bytes)**: The message flags. +#### NOW_CHANNEL_CLOSE_MSG + +Channel close notice, could be sent by either parties at any moment of communication to gracefully +close DVC channel. + +
+ + + + + + + + + + + + + + + + + + + + + +
01234567891012345678920123456789301
msgSize
msgClassmsgTypemsgFlags
status (variable)
+ +**msgSize (4 bytes)**: The message size, excluding the header size (8 bytes). + +**msgClass (1 byte)**: The message class (NOW_CHANNEL_MSG_CLASS_ID). + +**msgType (1 byte)**: The message type (NOW_CHANNEL_CLOSE_MSG_ID). + +**msgFlags (2 bytes)**: The message flags. + +**status (variable)**: Channel close status represented as NOW_STATUS structure. + ### System Messages #### NOW_SYSTEM_MSG @@ -743,7 +786,6 @@ The NOW_EXEC_MSG message is used to execute remote commands or scripts. | Value | Meaning | |-------|---------| -| NOW_EXEC_CAPSET_MSG_ID
0x00 | NOW_EXEC_CAPSET_MSG | | NOW_EXEC_ABORT_MSG_ID
0x01 | NOW_EXEC_ABORT_MSG | | NOW_EXEC_CANCEL_REQ_MSG_ID
0x02 | NOW_EXEC_CANCEL_REQ_MSG | | NOW_EXEC_CANCEL_RSP_MSG_ID
0x03 | NOW_EXEC_CANCEL_RSP_MSG | @@ -761,7 +803,10 @@ The NOW_EXEC_MSG message is used to execute remote commands or scripts. #### NOW_EXEC_ABORT_MSG -The NOW_EXEC_ABORT_MSG message is used to abort a remote execution immediately due to an unrecoverable error. This message can be sent at any time without an explicit response message. The session is considered aborted as soon as this message is sent. +The NOW_EXEC_ABORT_MSG message is used to abort a remote execution immediately. +See NOW_EXEC_CANCEL_REQ if the graceful session cancellation is needed instead. +This message can be sent by the client at any point of session lifetime. +The session is considered aborted as soon as this message is sent. @@ -885,7 +930,8 @@ The NOW_EXEC_CANCEL_RSP_MSG message is used to respond to a remote execution can #### NOW_EXEC_RESULT_MSG -The NOW_EXEC_RESULT_MSG message is used to return the result of an execution request. The session is considered terminated as soon as this message is sent. +The NOW_EXEC_RESULT_MSG message is used to return the result of an execution request. +The session is considered terminated as soon as this message is sent.
@@ -973,17 +1019,13 @@ The NOW_EXEC_DATA_MSG message is used to send input/output data as part of a rem | Flag | Meaning | |----------------------------------------|---------------------------------| -| NOW_EXEC_FLAG_DATA_FIRST
0x00000001 | This is the first data message. | -| NOW_EXEC_FLAG_DATA_LAST
0x00000002 | This is the last data message, the command completed execution. | -| NOW_EXEC_FLAG_DATA_STDIN
0x00000004 | The data is from the standard input. | -| NOW_EXEC_FLAG_DATA_STDOUT
0x00000008 | The data is from the standard output. | -| NOW_EXEC_FLAG_DATA_STDERR
0x00000010 | The data is from the standard error. | +| NOW_EXEC_FLAG_DATA_LAST
0x00000001 | This is the last data message, the command completed execution. | +| NOW_EXEC_FLAG_DATA_STDIN
0x00000002 | The data is from the standard input. | +| NOW_EXEC_FLAG_DATA_STDOUT
0x00000004 | The data is from the standard output. | +| NOW_EXEC_FLAG_DATA_STDERR
0x00000008 | The data is from the standard error. | Message should contain exactly one of `NOW_EXEC_FLAG_DATA_STDIN`, `NOW_EXEC_FLAG_DATA_STDOUT` or `NOW_EXEC_FLAG_DATA_STDERR` flags set. -First stream message should always contain `NOW_EXEC_FLAG_DATA_FIRST` -flag. - `NOW_EXEC_FLAG_DATA_LAST` should indicate EOF for a stream, all consecutive messages for the given stream will be ignored by either party (client or sever). @@ -1120,8 +1162,8 @@ The NOW_EXEC_PROCESS_MSG message is used to send a Windows [CreateProcess()](htt | Flag | Meaning | |----------------------------------------|---------------------------| -| NOW_EXEC_FLAG_PROCESS_PARAMETERS_SET
0x00000001 | `parameters` field contains non-default value. | -| NOW_EXEC_FLAG_PROCESS_DIRECTORY_SET
0x00000002 | `directory` field contains non-default value. | +| NOW_EXEC_FLAG_PROCESS_PARAMETERS_SET
0x0001 | `parameters` field contains non-default value. | +| NOW_EXEC_FLAG_PROCESS_DIRECTORY_SET
0x0002 | `directory` field contains non-default value. | **sessionId (4 bytes)**: A 32-bit unsigned integer containing a unique remote execution session id. @@ -1186,8 +1228,9 @@ The NOW_EXEC_SHELL_MSG message is used to execute a remote shell script. **command (variable)**: A NOW_VARSTR structure containing the script file contents to execute. -**shell (variable)**: A NOW_VARSTR structure containing the shell to use for execution. If no shell is specified, the default system shell (/bin/sh) will be used. Ignored if NOW_EXEC_FLAG_SHELL_SHELL_SET -is not set. +**shell (variable)**: A NOW_VARSTR structure containing the shell to use for execution. +If no shell is specified, the default system shell (/bin/sh) will be used. +Ignored if NOW_EXEC_FLAG_SHELL_SHELL_SET is not set. **directory (variable)**: A NOW_VARSTR structure containing the command working directory. Ignored if NOW_EXEC_FLAG_SHELL_DIRECTORY_SET is not set. @@ -1273,6 +1316,9 @@ The NOW_EXEC_WINPS_MSG message is used to execute a remote Windows PowerShell (p + + + @@ -1300,14 +1346,21 @@ The NOW_EXEC_WINPS_MSG message is used to execute a remote Windows PowerShell (p | NOW_EXEC_FLAG_PS_NON_INTERACTIVE
0x00000020 | PowerShell -NonInteractive option | | NOW_EXEC_FLAG_PS_EXECUTION_POLICY
0x00000040 | `executionPolicy` field contains non-default value and specifies the PowerShell -ExecutionPolicy parameter | | NOW_EXEC_FLAG_PS_CONFIGURATION_NAME
0x00000080 | `configurationName` field contains non-default value and specifies the PowerShell -ConfigurationName parameter | +| NOW_EXEC_FLAG_PS_DIRECTORY_SET
0x00000100 | `directory` field contains non-default value and specifies command working directory | **sessionId (4 bytes)**: A 32-bit unsigned integer containing a unique remote execution session id. **command (variable)**: A NOW_VARSTR structure containing the command to execute. -**executionPolicy (variable)**: A NOW_VARSTR structure containing the execution policy (-ExecutionPolicy) parameter value. Ignored if NOW_EXEC_FLAG_PS_EXECUTION_POLICY is not set. +**directory (variable)**: A NOW_VARSTR structure containing the command working directory. +Corresponds to the lpCurrentDirectory parameter. +Ignored if NOW_EXEC_FLAG_PROCESS_DIRECTORY_SET is not set. -**configurationName (variable)**: A NOW_VARSTR structure containing the configuration name (-ConfigurationName) parameter value. Ignored if NOW_EXEC_FLAG_PS_CONFIGURATION_NAME is not set. +**executionPolicy (variable)**: A NOW_VARSTR structure containing the execution policy (-ExecutionPolicy) parameter value. +Ignored if NOW_EXEC_FLAG_PS_EXECUTION_POLICY is not set. + +**configurationName (variable)**: A NOW_VARSTR structure containing the configuration name (-ConfigurationName) parameter value. +Ignored if NOW_EXEC_FLAG_PS_CONFIGURATION_NAME is not set. #### NOW_EXEC_PWSH_MSG @@ -1337,6 +1390,9 @@ The NOW_EXEC_PWSH_MSG message is used to execute a remote PowerShell 7 (pwsh) co + + + @@ -1358,6 +1414,8 @@ The NOW_EXEC_PWSH_MSG message is used to execute a remote PowerShell 7 (pwsh) co **command (variable)**: A NOW_VARSTR structure containing the command to execute. +**directory (variable)**: A NOW_VARSTR structure, same as with NOW_EXEC_WINPS_MSG. + **executionPolicy (variable)**: A NOW_VARSTR structure, same as with NOW_EXEC_WINPS_MSG. **configurationName (variable)**: A NOW_VARSTR structure, same as with NOW_EXEC_WINPS_MSG. diff --git a/doc/make.ps1 b/docs/make.ps1 similarity index 100% rename from doc/make.ps1 rename to docs/make.ps1
command (variable)
directory (variable)
executionPolicy (variable)
command (variable)
directory (variable)
executionPolicy (variable)