diff --git a/Cargo.lock b/Cargo.lock index 2ae11967b..2e1f87a59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2861,6 +2861,11 @@ dependencies = [ [[package]] name = "ironrdp-rdpeusb" version = "0.1.0" +dependencies = [ + "ironrdp-core", + "ironrdp-pdu", + "ironrdp-str", +] [[package]] name = "ironrdp-rdpfile" diff --git a/crates/ironrdp-rdpeusb/Cargo.toml b/crates/ironrdp-rdpeusb/Cargo.toml index 04704e397..a5e2a9125 100644 --- a/crates/ironrdp-rdpeusb/Cargo.toml +++ b/crates/ironrdp-rdpeusb/Cargo.toml @@ -12,7 +12,14 @@ categories.workspace = true publish = false +[features] +default = [] +std = [] + [dependencies] +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.7", features = ["alloc"] } # public +ironrdp-str = { path = "../ironrdp-str", version = "0.1" } [lints] workspace = true diff --git a/crates/ironrdp-rdpeusb/src/lib.rs b/crates/ironrdp-rdpeusb/src/lib.rs index 1800568c7..d1ce420ea 100644 --- a/crates/ironrdp-rdpeusb/src/lib.rs +++ b/crates/ironrdp-rdpeusb/src/lib.rs @@ -1 +1,6 @@ #![cfg_attr(doc, doc = include_str!("../README.md"))] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod pdu; diff --git a/crates/ironrdp-rdpeusb/src/pdu/caps.rs b/crates/ironrdp-rdpeusb/src/pdu/caps.rs new file mode 100644 index 000000000..902e7ac30 --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/caps.rs @@ -0,0 +1,124 @@ +use ironrdp_core::{ + DecodeError, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_fixed_part_size, + unsupported_value_err, +}; + +use crate::ensure_payload_size; +use crate::pdu::header::SharedMsgHeader; +use crate::pdu::utils::HResult; + +/// Identifies the interface manipulation capabilties of server/client. +#[repr(u32)] +#[non_exhaustive] +#[derive(Debug, Clone, Copy)] +pub enum Capability { + #[doc(alias = "RIM_CAPABILITY_VERSION_01")] + RimCapabilityVersion01 = 0x1, +} + +impl Capability { + pub const FIXED_PART_SIZE: usize = size_of::(); +} + +impl TryFrom for Capability { + type Error = DecodeError; + + fn try_from(value: u32) -> Result { + if value == 0x1 { + Ok(Self::RimCapabilityVersion01) + } else { + Err(unsupported_value_err!( + "CapabilityValue", + "is not: `RIM_CAPABILITY_VERSION_01 = 0x1`".into() + )) + } + } +} + +#[doc(alias = "RIM_EXCHANGE_CAPABILITY_REQUEST")] +pub struct RimExchangeCapabilityRequest { + pub header: SharedMsgHeader, + pub capability: Capability, +} + +impl RimExchangeCapabilityRequest { + pub const PAYLOAD_SIZE: usize = Capability::FIXED_PART_SIZE; + + pub const FIXED_PART_SIZE: usize = Self::PAYLOAD_SIZE + SharedMsgHeader::SIZE_WHEN_NOT_RSP; + + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + ensure_payload_size!(in: src); + let capability = Capability::try_from(src.read_u32())?; + + Ok(Self { header, capability }) + } +} + +impl Encode for RimExchangeCapabilityRequest { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + self.header.encode(dst)?; + + #[expect(clippy::as_conversions)] + dst.write_u32(self.capability as u32); + + Ok(()) + } + + fn name(&self) -> &'static str { + "RIM_EXCHANGE_CAPABILITY_REQUEST" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +#[doc(alias = "RIM_EXCHANGE_CAPABILITY_RESPONSE")] +pub struct RimExchangeCapabilityResponse { + pub header: SharedMsgHeader, + pub capability: Capability, + pub result: HResult, +} + +impl RimExchangeCapabilityResponse { + pub const PAYLOAD_SIZE: usize = Capability::FIXED_PART_SIZE + size_of::(); + + pub const FIXED_PART_SIZE: usize = Self::PAYLOAD_SIZE + SharedMsgHeader::SIZE_WHEN_RSP; + + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + ensure_payload_size!(in: src); + let capability = Capability::try_from(src.read_u32())?; + + let result = src.read_u32(); + + Ok(Self { + header, + capability, + result, + }) + } +} + +impl Encode for RimExchangeCapabilityResponse { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + self.header.encode(dst)?; + + #[expect(clippy::as_conversions)] + dst.write_u32(self.capability as u32); + + dst.write_u32(self.result); + + Ok(()) + } + + fn name(&self) -> &'static str { + "RIM_EXCHANGE_CAPABILITY_RESPONSE" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} diff --git a/crates/ironrdp-rdpeusb/src/pdu/chan_notify.rs b/crates/ironrdp-rdpeusb/src/pdu/chan_notify.rs new file mode 100644 index 000000000..aa7ae0126 --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/chan_notify.rs @@ -0,0 +1,84 @@ +//! PDU's specific to the [Channel Notification][1] interface. +//! +//! Used by both the client and the server to communicate with the other side. For server-to-client +//! notifications, the default interface ID is `0x00000002`; for client-to-server notifications, the +//! default interface ID is `0x00000003`. +//! +//! [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a7ea1b33-80bb-4197-a502-ee62394399c0 + +use alloc::borrow::ToOwned as _; + +use ironrdp_core::{ + DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_fixed_part_size, unsupported_value_err, +}; + +use crate::ensure_payload_size; +use crate::pdu::header::SharedMsgHeader; + +/// The `CHANNEL_CREATED` message is sent from both the client and the server to inform the other +/// side of the RDP USB device redirection version supported. +// +/// * [MS-RDPEUSB § 2.2.5.1 Channel Created Message (CHANNEL_CREATED)][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/e2859c23-acda-47d4-a2fc-9e7415e4b8d6 +/// +// NOTE: Implementation of IO_CONTROL message's input_buffer_size and input_buffer fields may +// need modification if a newer version of MS-RDPEUSB is realeased. +#[doc(alias = "CHANNEL_CREATED")] +pub struct ChannelCreated { + pub header: SharedMsgHeader, +} + +impl ChannelCreated { + pub const PAYLOAD_SIZE: usize = size_of::() * 3; + + pub const FIXED_PART_SIZE: usize = Self::PAYLOAD_SIZE + SharedMsgHeader::SIZE_WHEN_NOT_RSP; + + /// The major version of RDP USB redirection supported. + #[doc(alias = "MajorVersion")] + pub const MAJOR_VER: u32 = 1; + + /// The minor version of RDP USB redirection supported. + #[doc(alias = "MinorVersion")] + pub const MINOR_VER: u32 = 0; + + /// The capabilities of RDP USB redirection supported. + #[doc(alias = "Capabilities")] + pub const CAPS: u32 = 0; + + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + ensure_payload_size!(in: src); + if src.read_u32() != Self::MAJOR_VER { + return Err(unsupported_value_err!("MajorVersion", "is not: 1".to_owned())); + } + if src.read_u32() != Self::MINOR_VER { + return Err(unsupported_value_err!("MinorVersion", "is not: 0".to_owned())); + } + if src.read_u32() != Self::CAPS { + return Err(unsupported_value_err!("Capabilities", "is not: 0".to_owned())); + } + Ok(Self { header }) + } +} + +impl Encode for ChannelCreated { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + self.header.encode(dst)?; + + dst.write_u32(Self::MAJOR_VER); + dst.write_u32(Self::MINOR_VER); + dst.write_u32(Self::CAPS); + + Ok(()) + } + + fn name(&self) -> &'static str { + "CHANNEL_CREATED" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} diff --git a/crates/ironrdp-rdpeusb/src/pdu/dev_sink.rs b/crates/ironrdp-rdpeusb/src/pdu/dev_sink.rs new file mode 100644 index 000000000..b96f9a62b --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/dev_sink.rs @@ -0,0 +1,336 @@ +//! PDU's specific to the [Device Sink][1] interface. +//! +//! Identified by the default interface ID `0x00000001`, this interface is used by the client to +//! communicate with the server about new USB devices. +//! +//! [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a9a8add7-4e99-4697-abd0-ad64c80c788d + +use alloc::borrow::ToOwned as _; +use alloc::string::ToString as _; + +use ironrdp_core::{ + Decode, DecodeError, DecodeOwned as _, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, + ensure_fixed_part_size, ensure_size, unsupported_value_err, +}; +use ironrdp_pdu::utils::strict_sum; +use ironrdp_str::multi_sz::MultiSzString; +use ironrdp_str::prefixed::Cch32String; + +use crate::pdu::header::{InterfaceId, SharedMsgHeader}; + +/// Specs: [MS-RDPEUSB § 2.2.1 SHARED_MSG_HEADER][1] +#[doc(alias = "ADD_VIRTUAL_CHANNEL")] +pub struct AddVirtualChannel { + pub header: SharedMsgHeader, +} + +impl AddVirtualChannel { + pub const FIZED_PART_SIZE: usize = SharedMsgHeader::SIZE_WHEN_NOT_RSP; +} + +impl Encode for AddVirtualChannel { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + self.header.encode(dst) + } + + fn name(&self) -> &'static str { + "ADD_VIRTUAL_CHANNEL" + } + + fn size(&self) -> usize { + Self::FIZED_PART_SIZE + } +} + +#[doc(alias = "USB_DEVICE_CAPABILITIES")] +pub struct UsbDeviceCaps { + pub usb_bus_iface_ver: UsbBusIfaceVer, + pub usbdi_ver: UsbdiVer, + pub supported_usb_ver: SupportedUsbVer, + pub device_speed: DeviceSpeed, + pub no_ack_isoch_write_jitter_buf_size: NoAckIsochWriteJitterBufSizeInMs, +} + +impl UsbDeviceCaps { + pub const CB_SIZE: u32 = 28; + + pub const HCD_CAPS: u32 = 0; + + #[expect(clippy::as_conversions)] + pub const FIXED_PART_SIZE: usize = Self::CB_SIZE as usize; +} + +impl Encode for UsbDeviceCaps { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + #[expect(clippy::as_conversions)] + { + dst.write_u32(Self::CB_SIZE); + dst.write_u32(self.usb_bus_iface_ver as u32); + dst.write_u32(self.usbdi_ver as u32); + dst.write_u32(self.supported_usb_ver as u32); + dst.write_u32(Self::HCD_CAPS); + dst.write_u32(self.device_speed as u32); + } + dst.write_u32(self.no_ack_isoch_write_jitter_buf_size.0); + + Ok(()) + } + + fn name(&self) -> &'static str { + "USB_DEVICE_CAPABILITIES" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for UsbDeviceCaps { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + if src.read_u32() != Self::CB_SIZE { + return Err(unsupported_value_err!("CbSize", "is not: 28".to_owned())); + } + let usb_bus_iface_ver = match src.read_u32() { + 0x0 => UsbBusIfaceVer::V0, + 0x1 => UsbBusIfaceVer::V1, + 0x2 => UsbBusIfaceVer::V2, + _ => { + return Err(unsupported_value_err!( + "UsbBusInterfaceVersion", + "is not one of: 0x0, 0x1, 0x2".to_owned() + )); + } + }; + let usbdi_ver = match src.read_u32() { + 0x500 => UsbdiVer::V0x500, + 0x600 => UsbdiVer::V0x600, + _ => { + return Err(unsupported_value_err!( + "USBDI_Version", + "is not one of: 0x500, 0x600".to_owned() + )); + } + }; + let supported_usb_ver = match src.read_u32() { + 0x100 => SupportedUsbVer::Usb10, + 0x110 => SupportedUsbVer::Usb11, + 0x200 => SupportedUsbVer::Usb20, + _ => { + return Err(unsupported_value_err!( + "SupportedUsbVersion", + "is not one of: 0x100, 0x110, 0x200".to_owned() + )); + } + }; + if src.read_u32() != Self::HCD_CAPS { + return Err(unsupported_value_err!("HcdCapabilities", "is not: 0x0".to_owned())); + } + let device_speed = match src.read_u32() { + 0x0 => DeviceSpeed::FullSpeed, + 0x1 => DeviceSpeed::HighSpeed, + _ => { + return Err(unsupported_value_err!( + "DeviceIsHighSpeed", + "is not one of: 0x0, 0x1".to_owned() + )); + } + }; + let no_ack_isoch_write_jitter_buf_size = match src.read_u32() { + 0 => NoAckIsochWriteJitterBufSizeInMs::TS_URB_ISOCH_TRANSER_NOT_SUPPORTED, + value @ 10..=512 => NoAckIsochWriteJitterBufSizeInMs(value), + _ => { + return Err(unsupported_value_err!( + "NoAckIsochWriteJitterBufferSizeInMs", + "is not: 0, or in the range 10..=512".to_owned() + )); + } + }; + + Ok(Self { + usb_bus_iface_ver, + usbdi_ver, + supported_usb_ver, + device_speed, + no_ack_isoch_write_jitter_buf_size, + }) + } +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum UsbBusIfaceVer { + V0 = 0x0, + V1 = 0x1, + V2 = 0x2, +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum UsbdiVer { + V0x500 = 0x500, + V0x600 = 0x600, +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum SupportedUsbVer { + Usb10 = 0x100, + Usb11 = 0x110, + Usb20 = 0x200, +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum DeviceSpeed { + FullSpeed = 0x0, + HighSpeed = 0x1, +} + +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] +pub struct NoAckIsochWriteJitterBufSizeInMs(u32); + +impl NoAckIsochWriteJitterBufSizeInMs { + const TS_URB_ISOCH_TRANSER_NOT_SUPPORTED: Self = Self(0); + + pub fn outstanding_isoch_data(&self) -> Option { + (self.0 != 0).then_some(self.0) + } +} + +impl TryFrom for NoAckIsochWriteJitterBufSizeInMs { + type Error = DecodeError; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::TS_URB_ISOCH_TRANSER_NOT_SUPPORTED), + 10..=512 => Ok(Self(value)), + value => Err(unsupported_value_err!( + "NoAckIsochWriteJitterBufferSizeInMs", + value.to_string() + )), + } + } +} + +#[doc(alias = "ADD_DEVICE")] +pub struct AddDevice { + pub header: SharedMsgHeader, + /// The (unique) interface ID to be used by request messages in the [USB Devices][1] interface. + /// + /// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/034257d7-f7a8-4fe1-b8c2-87ac8dc4f50e + pub usb_device: InterfaceId, + pub device_instance_id: Cch32String, + pub hw_ids: Option, + pub compat_ids: Option, + pub container_id: Cch32String, + pub usb_device_caps: UsbDeviceCaps, +} + +impl AddDevice { + pub const NUM_USB_DEVICE: u32 = 0x1; + + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + ensure_size!(in: src, size: 4); // NumUsbDevice + if src.read_u32() != 0x1 { + return Err(unsupported_value_err!("NumUsbDevice", "is not: 0x1".to_owned())); + } + + ensure_size!(in: src, size: InterfaceId::FIXED_PART_SIZE); + let usb_device = match src.read_u32() { + 0x0..0x4 => { + return Err(unsupported_value_err!( + "UsbDevice", + "is one of: 0x0, 0x1, 0x2, 0x3 (default interface ID's)".to_owned() + )); + } + value @ 0x4..=0x3F_FF_FF_FF => InterfaceId::from(value), + 0x40_00_00_00.. => { + return Err(unsupported_value_err!( + "UsbDevice", + "is greater than: 0x3F_FF_FF_FF (more than 30 bits)".to_owned() + )); + } + }; + let device_instance_id = Cch32String::decode_owned(src)?; + ensure_size!(in: src, size: 4); // cchHwIds + let hw_ids = if src.peek_u32() != 0 { + Some(MultiSzString::decode_owned(src)?) + } else { + let _ = src.read_u32(); // skip cchHwIds + None + }; + ensure_size!(in: src, size: 4); // cchCompatIds + let compat_ids = if src.peek_u32() != 0 { + Some(MultiSzString::decode_owned(src)?) + } else { + let _ = src.read_u32(); // skip cchCompatIds + None + }; + let container_id = Cch32String::decode_owned(src)?; + let usb_device_caps = UsbDeviceCaps::decode(src)?; + + Ok(Self { + header, + usb_device, + device_instance_id, + hw_ids, + compat_ids, + container_id, + usb_device_caps, + }) + } +} + +impl Encode for AddDevice { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.header.encode(dst)?; + dst.write_u32(Self::NUM_USB_DEVICE); + dst.write_u32(self.usb_device.into()); + self.device_instance_id.encode(dst)?; + match &self.hw_ids { + Some(ids) => ids.encode(dst)?, + None => dst.write_u32(0x0), + }; + match &self.compat_ids { + Some(ids) => ids.encode(dst)?, + None => dst.write_u32(0x0), + }; + self.container_id.encode(dst)?; + self.usb_device_caps.encode(dst)?; + + Ok(()) + } + + fn name(&self) -> &'static str { + "ADD_DEVICE" + } + + fn size(&self) -> usize { + let device_instance_id = self.device_instance_id.size(); + let hw_ids = match &self.hw_ids { + Some(hardware_ids) => hardware_ids.size(), + None => const { size_of::() }, // cchHwIds + }; + let compat_ids = match &self.compat_ids { + Some(compatibility_ids) => compatibility_ids.size(), + None => const { size_of::() }, // cchCompatIds + }; + let container_id = self.container_id.size(); + + strict_sum(&[SharedMsgHeader::SIZE_WHEN_NOT_RSP + + 4 // NumUsbDevice + + InterfaceId::FIXED_PART_SIZE // UsbDevice + + device_instance_id + + hw_ids + + compat_ids + + container_id + + UsbDeviceCaps::FIXED_PART_SIZE]) + } +} diff --git a/crates/ironrdp-rdpeusb/src/pdu/header.rs b/crates/ironrdp-rdpeusb/src/pdu/header.rs new file mode 100644 index 000000000..f86d0210c --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/header.rs @@ -0,0 +1,315 @@ +//! Common utils needed by PDU's under [MS-RDPEUSB][1]. +//! +//! [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a1004d0e-99e9-4968-894b-0b924ef2f125 + +#![allow(dead_code)] + +use alloc::borrow::ToOwned as _; +use alloc::string::ToString as _; + +use ironrdp_core::{ + Decode, DecodeError, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_size, invalid_field_err, + unsupported_value_err, +}; + +/// Unique ID for request-response pair. +/// +/// * [MS-RDPEUSB § 2.2.1 SHARED_MSG_HEADER][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/71cfb32c-ba15-4f95-9241-70f9df273909 +pub type MessageId = u32; + +/// Indicates in what context is a [`SHARED_MSG_HEADER`][1] being used. +/// +/// Bits 30-31 of the mask occupy the corresponding bits 0-1 of the header (in big endian). +/// +/// * [MS-RDPEUSB § 2.2.1 SHARED_MSG_HEADER][2] +/// +/// [1]: SharedMsgHeader +/// [2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/71cfb32c-ba15-4f95-9241-70f9df273909 +#[repr(u8)] +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Mask { + /// Indicates that the [`SHARED_MSG_HEADER`][1] is being used in a response message. + /// + /// [1]: SharedMsgHeader + #[doc(alias = "STREAM_ID_STUB")] + StreamIdStub = 0x2, + + /// Indicates that the [`SHARED_MSG_HEADER`][1] is not being used in a response message. + /// + /// [1]: SharedMsgHeader + #[doc(alias = "STREAM_ID_PROXY")] + StreamIdProxy = 0x1, + + /// Indicates that the [`SHARED_MSG_HEADER`][1] is being used in a message for capabilities + /// exchange ([`RIM_EXCHANGE_CAPABILITY_REQUEST`][2] / [`RIM_EXCHANGE_CAPABILITY_RESPONSE`][3]). + /// This value **MUST NOT** be used for any other messages. + /// + /// [1]: SharedMsgHeader + /// [2]: super::exchange_caps::RimExchangeCapabilityRequest + /// [3]: super::exchange_caps::RimExchangeCapabilityResponse + #[doc(alias = "STREAM_ID_NONE")] + StreamIdNone = 0x0, +} + +impl From for u32 { + #[expect(clippy::as_conversions)] + fn from(value: Mask) -> Self { + value as Self + } +} + +impl TryFrom for Mask { + type Error = DecodeError; + + fn try_from(value: u8) -> Result { + match value { + 0x0 => Ok(Self::StreamIdNone), + 0x1 => Ok(Self::StreamIdProxy), + 0x2 => Ok(Self::StreamIdStub), + 0x3 => Err(unsupported_value_err!("Mask", "is: 0x3".to_owned())), + 0x4.. => Err(invalid_field_err!("Mask", "more than 2 bits")), + } + } +} + +/// Groups similar kinds of messages together. +/// +/// An interface is a "group" of similar kinds of messages. Some interfaces have default ID's +/// (see associated constants), while other interfaces like the **USB Device** and **Request Completion** +/// get allotted interface ID's during the lifecycle a USB redirection channel. +/// +/// Goes without saying, server-client should maintain the interface ID's for the **Request +/// Completion** and **USB Devices** interfaces and match them with decoded interface ID's. +/// +/// Max value for interface ID's: `0x3F_FF_FF_FF` (30 bits). +/// +/// * [MS-RDPEUSB § 2.2.1 SHARED_MSG_HEADER][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/71cfb32c-ba15-4f95-9241-70f9df273909 +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InterfaceId(u32); + +impl InterfaceId { + pub const FIXED_PART_SIZE: usize = size_of::(); + /// **Exchange Capabilities** interface (ID: `0x0`). Used by both client and server to + /// exchange capabilities for interface manipulation. + /// + /// * [MS-RDPEUSB § 2.2.3 Interface Manipulation Exchange Capabilities Interface][1] + /// + /// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/6aee4e70-9d3b-49d7-a9b9-3c437cb27c8e + pub const CAPABILITIES: Self = Self(0x0); + + /// **Device Sink** interface (ID: `0x1`). Used by the client to communicate with the server + /// about new USB devices. + /// + /// * [MS-RDPEUSB § 2.2.4 Device Sink Interface][1] + /// + /// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a9a8add7-4e99-4697-abd0-ad64c80c788d + pub const DEVICE_SINK: Self = Self(0x1); + + /// **Channel Notification** interface (ID: `0x2`). Used by the server to communicate with the + /// client. + /// + /// * [MS-RDPEUSB § 2.2.5 Channel Notification Interface][1] + /// + /// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a7ea1b33-80bb-4197-a502-ee62 + pub const NOTIFY_CLIENT: Self = Self(0x2); + + /// **Channel Notification** interface (ID: `0x3`). Used by the client to communicate with the + /// server. + /// + /// * [MS-RDPEUSB § 2.2.5 Channel Notification Interface][1] + /// + /// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a7ea1b33-80bb-4197-a502-ee62 + pub const NOTIFY_SERVER: Self = Self(0x3); +} + +impl From for InterfaceId { + /// Constructs an `InterfaceId` from a value, discarding the highest 2 bits. + fn from(value: u32) -> Self { + Self(value & 0x3F_FF_FF_FF) + } +} + +impl From for u32 { + fn from(value: InterfaceId) -> Self { + value.0 + } +} + +/// Indicates a task/function to perform. +/// +/// Function ID's are defined for all interfaces: +/// +/// * Interface Manipulation Exchange Capabilities Interface +/// * [`FunctionId::RIM_EXCHANGE_CAPABILITY_REQUEST`] +/// * Device Sink Interface +/// * [`FunctionId::ADD_VIRTUAL_CHANNEL`] +/// * [`FunctionId::ADD_DEVICE`] +/// * Channel Notification Interface +/// * [`FunctionId::CHANNEL_CREATED`] +/// * USB Device Interface +/// * [`FunctionId::CANCEL_REQUEST`] +/// * [`FunctionId::REGISTER_REQUEST_CALLBACK`] +/// * [`FunctionId::IO_CONTROL`] +/// * [`FunctionId::INTERNAL_IO_CONTROL`] +/// * [`FunctionId::QUERY_DEVICE_TEXT`] +/// * [`FunctionId::TRANSFER_IN_REQUEST`] +/// * [`FunctionId::TRANSFER_OUT_REQUEST`] +/// * [`FunctionId::RETRACT_DEVICE`] +/// * Request Completion Interface +/// * [`FunctionId::IOCONTROL_COMPLETION`] +/// * [`FunctionId::URB_COMPLETION`] +/// * [`FunctionId::URB_COMPLETION_NO_DATA`] +/// +/// See [`InterfaceId`] for more info on interfaces. +/// +/// * [MS-RDPEUSB § 2.2.1 Shared Message Header (SHARED_MSG_HEADER)][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/71cfb32c-ba15-4f95-9241-70f9df273909 +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] +pub struct FunctionId(u32); + +impl FunctionId { + pub const FIXED_PART_SIZE: usize = size_of::(); + // // Needed for QI_REQ and QI_RSP + // + // /// Release the given interface ID. + // pub const RIMCALL_RELEASE: Self = Self(0x00000001); + // pub const RIMCALL_QUERYINTERFACE: Self = Self(0x00000002); + + // -------------------- Exchange Capabilities Interface --------------------------------------- + + /// The server sends the [`RIM_EXCHANGE_CAPABILITY_REQUEST`][1] message, or the client sends + /// the [`RIM_EXCHANGE_CAPABILITY_RESPONSE`][2] message in response to the former message. + /// + /// [1]: crate::pdu::caps::RimExchangeCapabilityRequest + /// [2]: crate::pdu::caps::RimExchangeCapabilityResponse + pub const RIM_EXCHANGE_CAPABILITY_REQUEST: Self = Self(0x100); + + // -------------------- Request Completion Interface ------------------------------------------ + + pub const IOCONTROL_COMPLETION: Self = Self(0x100); + pub const URB_COMPLETION: Self = Self(0x101); + pub const URB_COMPLETION_NO_DATA: Self = Self(0x102); + + // -------------------- USB Device Interface -------------------------------------------------- + + pub const CANCEL_REQUEST: Self = Self(0x100); + pub const REGISTER_REQUEST_CALLBACK: Self = Self(0x101); + pub const IO_CONTROL: Self = Self(0x102); + pub const INTERNAL_IO_CONTROL: Self = Self(0x103); + pub const QUERY_DEVICE_TEXT: Self = Self(0x104); + pub const TRANSFER_IN_REQUEST: Self = Self(0x105); + pub const TRANSFER_OUT_REQUEST: Self = Self(0x106); + pub const RETRACT_DEVICE: Self = Self(0x107); + + // -------------------- Device Sink Interface ------------------------------------------------- + + /// The client sends the [`ADD_VIRTUAL_CHANNEL`][1] message. + /// + /// [1]: super::device_sink::AddVirtualChannel + pub const ADD_VIRTUAL_CHANNEL: Self = Self(0x100); + pub const ADD_DEVICE: Self = Self(0x101); + + // -------------------- Channel Notification Interface ---------------------------------------- + + pub const CHANNEL_CREATED: Self = Self(0x100); +} + +impl TryFrom for FunctionId { + type Error = DecodeError; + + fn try_from(value: u32) -> Result { + // if matches!(value, 0x001 | 0x002 | 0x100..=0x107) { + if matches!(value, 0x100..=0x107) { + Ok(Self(value)) + } else { + Err(unsupported_value_err!("FunctionId", value.to_string())) + } + } +} + +/// Common header `SHARED_MSG_HEADER` for all messages under [MS-RDPEUSB][1]. +/// +/// ⚠️ Never use the [`size_of`][2] or [`size_of_val`][3] functions with the header, +/// use [`SharedMsgHeader::size`] instead. +/// +/// * [MS-RDPEUSB § 2.2.1 Shared Message Header (SHARED_MSG_HEADER)][4] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a1004d0e-99e9-4968-894b-0b924ef2f125 +/// [2]: core::mem::size_of +/// [3]: core::mem::size_of_val +/// [4]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/71cfb32c-ba15-4f95-9241-70f9df273909 +#[doc(alias = "SHARED_MSG_HEADER")] +pub struct SharedMsgHeader { + pub interface_id: InterfaceId, + pub mask: Mask, + pub message_id: MessageId, + pub function_id: Option, +} + +impl SharedMsgHeader { + pub const SIZE_WHEN_RSP: usize = InterfaceId::FIXED_PART_SIZE + size_of::(); + + pub const SIZE_WHEN_NOT_RSP: usize = Self::SIZE_WHEN_RSP + FunctionId::FIXED_PART_SIZE; +} + +impl Encode for SharedMsgHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + let first32 = u32::from(self.interface_id) | (u32::from(self.mask) << 30); + dst.write_u32(first32); + dst.write_u32(self.message_id); + + if let Some(id) = self.function_id { + dst.write_u32(id.0); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + "SHARED_MSG_HEADER" + } + + fn size(&self) -> usize { + if self.function_id.is_some() { + Self::SIZE_WHEN_NOT_RSP + } else { + Self::SIZE_WHEN_RSP + } + } +} + +impl Decode<'_> for SharedMsgHeader { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_size!(in: src, size: InterfaceId::FIXED_PART_SIZE + size_of::()); + + let first32 = src.read_u32(); + let interface_id = InterfaceId::from(first32); + #[expect(clippy::as_conversions)] + let mask = Mask::try_from((first32 >> 30) as u8)?; + let message_id = src.read_u32(); + + let function_id = if mask == Mask::StreamIdStub { + None + } else { + ensure_size!(in: src, size: FunctionId::FIXED_PART_SIZE); + Some(FunctionId::try_from(src.read_u32())?) + }; + + Ok(SharedMsgHeader { + interface_id, + mask, + message_id, + function_id, + }) + } +} diff --git a/crates/ironrdp-rdpeusb/src/pdu/mod.rs b/crates/ironrdp-rdpeusb/src/pdu/mod.rs new file mode 100644 index 000000000..f172b6cec --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/mod.rs @@ -0,0 +1,65 @@ +use ironrdp_core::{Decode, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor}; + +use crate::pdu::header::{InterfaceId, SharedMsgHeader}; + +pub mod caps; +pub mod chan_notify; +pub mod dev_sink; +pub mod req_complete; +pub mod usb_dev; + +pub mod header; + +pub mod utils; + +pub mod ts_urb; + +pub enum UrbdrcServerPdu { + Caps(caps::RimExchangeCapabilityRequest), + CancelReq(usb_dev::CancelRequest), + RegReqCallback(usb_dev::RegisterRequestCallback), +} + +// impl UrbdrcServerPdu { +// pub fn decode(src: &mut ReadCursor<'_>, device_ifaces: I) -> DecodeResult +// where +// I: IntoIterator, +// I::Item: Into, +// { +// use UrbdrcServerPdu::*; +// +// let header = SharedMsgHeader::decode(src)?; +// match header.interface_id { +// // InterfaceId::CAPABILITIES => Ok(Caps(caps::RimExchangeCapabilityRequest::decode(src, header)?)), +// } +// } +// } + +impl Encode for UrbdrcServerPdu { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + use UrbdrcServerPdu::*; + match self { + Caps(rim_exchange_capability_request) => rim_exchange_capability_request.encode(dst), + CancelReq(cancel_request) => cancel_request.encode(dst), + RegReqCallback(register_request_callback) => register_request_callback.encode(dst), + } + } + + fn name(&self) -> &'static str { + use UrbdrcServerPdu::*; + match self { + Caps(rim_exchange_capability_request) => rim_exchange_capability_request.name(), + CancelReq(cancel_request) => cancel_request.name(), + RegReqCallback(register_request_callback) => register_request_callback.name(), + } + } + + fn size(&self) -> usize { + use UrbdrcServerPdu::*; + match self { + Caps(rim_exchange_capability_request) => rim_exchange_capability_request.size(), + CancelReq(cancel_request) => cancel_request.size(), + RegReqCallback(register_request_callback) => register_request_callback.size(), + } + } +} diff --git a/crates/ironrdp-rdpeusb/src/pdu/req_complete.rs b/crates/ironrdp-rdpeusb/src/pdu/req_complete.rs new file mode 100644 index 000000000..fc86e9969 --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/req_complete.rs @@ -0,0 +1,153 @@ +//! PDU's specific to the [Request Completion][1] interface. +//! +//! Used by the client to send the final result for a request previously sent from the server. +//! The unique interface ID for this interface is provided by the server using the +//! [`REGISTER_REQUEST_CALLBACK`] message, during the lifecycle of a USB redirection channel. +//! +//! [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/c0a146fc-20cf-4897-af27-a3c5474151ac + +use alloc::vec::Vec; + +use ironrdp_core::{ + DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_size, invalid_field_err, other_err, +}; +use ironrdp_pdu::utils::strict_sum; + +use crate::pdu::header::SharedMsgHeader; +use crate::pdu::utils::{HResult, RequestIdIoctl}; + +/// * [MS-ERREF § 2.2 Win32 Error Codes][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d +const ERROR_INSUFFICIENT_BUFFER: u32 = 0x7A; + +/// * [MS-ERREF § 2.1.2 HRESULT From WIN32 Error Code Macro][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0c0bcf55-277e-4120-b5dc-f6115fc8dc38 +const FACILITY_WIN32: u32 = 0x7; + +/// * [MS-ERREF § 2.1.2 HRESULT From WIN32 Error Code Macro][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0c0bcf55-277e-4120-b5dc-f6115fc8dc38 +macro_rules! HRESULT_FROM_WIN32 { + ($x: expr) => {{ + #[expect(clippy::cast_possible_wrap, clippy::as_conversions)] + if ($x as i32) <= 0 { + $x + } else { + $x & 0x0000FFFF | (FACILITY_WIN32 << 16) | 0x80000000 + } + }}; +} + +const HRESULT_FROM_WIN32_ERROR_INSUFFICIENT_BUFFER: u32 = HRESULT_FROM_WIN32!(ERROR_INSUFFICIENT_BUFFER); + +#[doc(alias = "IOCONTROL_COMPLETION")] +pub struct IoctlCompletion { + pub header: SharedMsgHeader, + pub request_id: RequestIdIoctl, + pub hresult: HResult, + pub information: u32, + pub output_buffer_size: u32, + pub output_buffer: Vec, +} + +impl IoctlCompletion { + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + let fixed_bytes = size_of::() + size_of::() + size_of::() + size_of::(); + ensure_size!(in: src, size: fixed_bytes); + + let request_id = src.read_u32(); + let hresult = src.read_u32(); + let information = src.read_u32(); + let output_buffer_size = src.read_u32(); + + // TODO: Should this stuff be part of some validate() function? + if hresult == 0 { + if information != output_buffer_size { + return Err(invalid_field_err!( + "Information != OutputBufferSize", + "HResult is: 0x0 (IOCTL success), but Information != OutputBufferSize" + )); + } + } else if hresult != HRESULT_FROM_WIN32_ERROR_INSUFFICIENT_BUFFER && output_buffer_size != 0 { + // > If the HResult field is equal to HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER) + // > then ... . For any other case `OutputBufferSize` **MUST** be set to 0 ... + // + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/b1722374-0658-47ba-8368-87bf9d3db4d4 + return Err(invalid_field_err!( + "OutputBufferSize != 0", + "HResult is not one of: 0x0 (IOCTL success), 0x8007007A (insufficient buffer error) +OutputBufferSize is not: 0x0 +OutputBufferSize should be: 0x0" + )); + } + + let output_buffer = match hresult { + // #[expect(clippy::as_conversions)] + 0 | HRESULT_FROM_WIN32_ERROR_INSUFFICIENT_BUFFER => { + let n = information.try_into().map_err(|e| other_err!(source: e))?; + Vec::from(src.read_slice(n)) + } + // > For any other case [OutputBufferSize] MUST be set to 0 + // Which means empty output_buffer + _ => Vec::new(), + }; + + Ok(Self { + header, + request_id, + hresult, + information, + output_buffer_size, + output_buffer, + }) + } +} + +impl Encode for IoctlCompletion { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.header.encode(dst)?; + + dst.write_u32(self.request_id); + dst.write_u32(self.hresult); + dst.write_u32(self.information); + dst.write_u32(self.output_buffer_size); + + dst.write_slice(&self.output_buffer); + + Ok(()) + } + + fn name(&self) -> &'static str { + "IOCONTROL_COMPLETION" + } + + fn size(&self) -> usize { + #[expect(clippy::as_conversions)] + let out_buf = if self.hresult == 0 { + assert_eq!(self.information, self.output_buffer_size); + self.output_buffer.len() + } else if self.hresult == HRESULT_FROM_WIN32!(ERROR_INSUFFICIENT_BUFFER) { + self.information as usize + } else { + 0 + }; + + strict_sum(&[SharedMsgHeader::SIZE_WHEN_NOT_RSP + + const { + size_of::(/* RequestId */) + + size_of::() + + size_of::(/* Information */) + + size_of::(/* OutputBufferSize */) + } + + out_buf]) + } +} + +// pub struct UrbCompletion { +// pub header: SharedMsgHeader, +// req_id: ReqIdTsUrb, +// } diff --git a/crates/ironrdp-rdpeusb/src/pdu/ts_urb/header.rs b/crates/ironrdp-rdpeusb/src/pdu/ts_urb/header.rs new file mode 100644 index 000000000..c60b8da89 --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/ts_urb/header.rs @@ -0,0 +1,467 @@ +use alloc::format; + +use ironrdp_core::{ + Decode, DecodeError, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_fixed_part_size, + unsupported_value_err, +}; + +use crate::pdu::utils::RequestIdTsUrb; + +/// Numeric code that indicates the requested operation for a USB Request Block (URB). +/// +/// URB Function codes are used with [`TS_URB_HEADER`][1]'s. This code should represent the +/// `TS_URB` structure the [`TS_URB_HEADER`][1] is used with. +/// +/// * [WDK: USB: _URB_HEADER][2] +/// * [USB request blocks (URBs)][3] +/// +/// [1]: TsUrbHeader +/// [2]: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/usb/ns-usb-_urb_header +/// [3]: https://learn.microsoft.com/en-us/windows-hardware/drivers/usbcon/communicating-with-a-usb-device +// +// NOTE: There are a few variants for Memory Descriptor Lists (MDL). Should a client just behave +// like it did not receive any of the MDL variants? Cause the client receives the data buffer over +// the network, so MDL's don't really make a point. +#[repr(u16)] +#[non_exhaustive] +#[derive(Debug, Clone, Copy)] +pub enum UrbFunction { + /// Indicates to the host controller driver that a configuration is to be selected. If set, + /// the URB is used with [`(TS)_URB_SELECT_CONFIGURATION`] as the data structure. + #[doc(alias = "URB_FUNCTION_SELECT_CONFIGURATION")] + SelectConfiguration = 0, + + /// Indicates to the host controller driver that an alternate interface setting is being + /// selected for an interface. If set, the URB is used with [`(TS)_URB_SELECT_INTERFACE`] as the data + /// structure. + #[doc(alias = "URB_FUNCTION_SELECT_INTERFACE")] + SelectInterface = 1, + + /// Indicates that all outstanding requests for a pipe should be canceled. If set, the URB is + /// used with [`(TS)_URB_PIPE_REQUEST`] as the data structure. This general-purpose request enables a + /// client to cancel any pending transfers for the specified pipe. Pipe state and endpoint + /// state are unaffected. The abort request might complete before all outstanding requests + /// have completed. Do not assume that completion of the abort request implies that all other + /// outstanding requests have completed. + #[doc(alias = "URB_FUNCTION_ABORT_PIPE")] + AbortPipe = 2, + + /// Requests the current frame number from the host controller driver. If set, the URB is used + /// with [`(TS)_URB_GET_CURRENT_FRAME_NUMBER`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_CURRENT_FRAME_NUMBER")] + GetCurrentFrameNumber = 7, + + /// Transfers data to or from a control pipe. If set, the URB is used with + /// [`(TS)_URB_CONTROL_TRANSFER`] as the data structure. + #[doc(alias = "URB_FUNCTION_CONTROL_TRANSFER")] + ControlTransfer = 8, + + /// Transfers data to or from a control pipe without a time limit specified by a timeout + /// value. If set, the URB is used with [`(TS)_URB_CONTROL_TRANSFER_EX`] as the data structure. + #[doc(alias = "URB_FUNCTION_CONTROL_TRANSFER_EX")] + ControlTransferEx = 50, + + /// Transfers data from a bulk pipe or interrupt pipe or to a bulk pipe. If set, the URB is + /// used with [`(TS)_URB_BULK_OR_INTERRUPT_TRANSFER`] as the data structure. + #[doc(alias = "URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER")] + BulkOrInterruptTransfer = 9, + + /// Transfers data to and from a bulk pipe or interrupt pipe, by using chained MDLs. If set, + /// the URB is used with [`(TS)_URB_BULK_OR_INTERRUPT_TRANSFER`] as the data structure. The client + /// driver must set the TransferBufferMDL member to the first MDL structure in the chain that + /// contains the transfer buffer. The USB driver stack ignores the TransferBuffer member when + /// processing this URB. + #[doc(alias = "URB_FUNCTION_BULK_OR_INTERRUPT_TRANSFER_USING_CHAINED_MDL")] + BulkOrInterruptTransferUsingChainedMdl = 55, + + /// Transfers data to or from an isochronous pipe. If set, the URB is used with + /// [`(TS)_URB_ISOCH_TRANSFER`] as the data structure. + #[doc(alias = "URB_FUNCTION_ISOCH_TRANSFER")] + IsochTransfer = 10, + + /// Transfers data to or from an isochronous pipe by using chained MDLs. If set, the URB is + /// used with [`(TS)_URB_ISOCH_TRANSFER`] as the data structure. The client driver must set the + /// TransferBufferMDL member to the first MDL in the chain that contains the transfer buffer. + /// The USB driver stack ignores the TransferBuffer member when processing this URB. + #[doc(alias = "URB_FUNCTION_ISOCH_TRANSFER_USING_CHAINED_MDL")] + IsochTransferUsingChainedMdl = 56, + + /// Resets the indicated pipe. If set, this URB is used with [`(TS)_URB_PIPE_REQUEST.`] The bus driver + /// accomplishes three tasks in response to this URB: + /// + /// First, for all pipes except isochronous pipes, this URB sends a CLEAR_FEATURE request to + /// clear the device's ENDPOINT_HALT feature. + /// + /// Second, the USB bus driver resets the data + /// toggle on the host side, as required by the USB specification. The USB device should reset + /// the data toggle on the device side when the bus driver clears its ENDPOINT_HALT feature. + /// Since some non-compliant devices do not support this feature, Microsoft provides the two + /// additional URBs: URB_FUNCTION_SYNC_CLEAR_STALL and URB_FUNCTION_SYNC_RESET_PIPE. These + /// allow client drivers to clear the ENDPOINT_HALT feature on the device, or reset the pipe + /// on the host side, respectively, without affecting the data toggle on the host side. If the + /// device does not reset the data toggle when it should, then the client driver can compensate + /// for this defect by not resetting the host-side data toggle. If the data toggle is reset on + /// the host side but not on the device side, packets will get out of sequence, and the device + /// might drop packets. + /// + /// Third, after the bus driver has successfully reset the pipe, it resumes transfers with the + /// next queued URB. After a pipe reset, transfers resume with the next queued URB. It is not + /// necessary to clear a halt condition on a default control pipe. The default control pipe + /// must always accept setup packets, and so if it halts, the USB stack will clear the halt + /// condition automatically. The client driver does not need to take any special action to + /// clear the halt condition on a default pipe. All transfers must be aborted or canceled + /// before attempting to reset the pipe. This URB must be sent at PASSIVE_LEVEL. + #[doc(alias = "URB_FUNCTION_SYNC_RESET_PIPE_AND_CLEAR_STALL")] + SyncResetPipeAndClearStall = 30, + + /// Clears the halt condition on the host side of a pipe. If set, this URB is used with + /// [`(TS)_URB_PIPE_REQUEST`] as the data structure. + /// + /// This URB allows a client to clear the halted state of a pipe without resetting the data + /// toggle and without clearing the endpoint stall condition (feature ENDPOINT_HALT). To clear + /// a halt condition on the pipe, reset the host-side data toggle and clear a stall on the + /// device with a single operation, use SYNC_RESET_PIPE_AND_CLEAR_STALL. + /// + /// The following status codes are important and have the indicated meaning: + /// + /// USBD_STATUS_INVALID_PIPE_HANDLE: The PipeHandle is not valid + /// + /// USBD_STATUS_ERROR_BUSY: The endpoint has active transfers pending. + /// + /// It is not necessary to clear a halt condition on a default control pipe. The default + /// control pipe must always accept setup packets, and so if it halts, the USB stack will clear + /// the halt condition automatically. The client driver does not need to take any special + /// action to clear the halt condition on a default pipe. + /// + /// All transfers must be aborted or canceled before attempting to reset the pipe. + /// + /// This URB must be sent at PASSIVE_LEVEL. + #[doc(alias = "URB_FUNCTION_SYNC_RESET_PIPE")] + SyncResetPipe = 48, + + /// Clears the stall condition on the endpoint. For all pipes except isochronous pipes, this + /// URB sends a CLEAR_FEATURE request to clear the device's ENDPOINT_HALT feature. However, + /// unlike the RB_FUNCTION_SYNC_RESET_PIPE_AND_CLEAR_STALL function, this URB function does + /// not reset the data toggle on the host side of the pipe. The USB specification requires + /// devices to reset the device-side data toggle after the client clears the device's + /// ENDPOINT_HALT feature, but some non-compliant devices do not reset their data toggle + /// properly. Client drivers that manage such devices can compensate for this defect by + /// clearing the stall condition directly with SYNC_CLEAR_STALL instead of + /// resetting the pipe with SYNC_RESET_PIPE_AND_CLEAR_STALL. + /// SYNC_CLEAR_STALL clears a stall condition on the device without resetting + /// the host-side data toggle. This prevents a non-compliant device from interpreting the + /// next packet as a retransmission and dropping the packet. + /// + /// If set, the URB is used with [`(TS)_URB_PIPE_REQUEST`] as the data structure. + /// + /// This URB function should be sent at PASSIVE_LEVEL + #[doc(alias = "URB_FUNCTION_SYNC_CLEAR_STALL")] + SyncClearStall = 49, + + /// Retrieves the device descriptor from a specific USB device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_DESCRIPTOR_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_DESCRIPTOR_FROM_DEVICE")] + GetDescriptorFromDevice = 11, + + /// Retrieves the descriptor from an endpoint on an interface for a USB device. If set, the + /// URB is used with [`(TS)_URB_CONTROL_DESCRIPTOR_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_DESCRIPTOR_FROM_ENDPOINT")] + GetDescriptorFromEndpoint = 36, + + /// Sets a device descriptor on a device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_DESCRIPTOR_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_SET_DESCRIPTOR_TO_DEVICE")] + SetDescriptorToDevice = 12, + + /// Sets an endpoint descriptor on an endpoint for an interface. If set, the URB is used with + /// [`(TS)_URB_CONTROL_DESCRIPTOR_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_SET_DESCRIPTOR_TO_ENDPOINT")] + SetDescriptorToEndpoint = 37, + + /// Sets a USB-defined feature on a device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_FEATURE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_SET_FEATURE_TO_DEVICE")] + SetFeatureToDevice = 13, + + /// Sets a USB-defined feature on an interface for a device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_FEATURE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_SET_FEATURE_TO_INTERFACE")] + SetFeatureToInterface = 14, + + /// Sets a USB-defined feature on an endpoint for an interface on a USB device. If set, the + /// URB is used with [`(TS)_URB_CONTROL_FEATURE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_SET_FEATURE_TO_ENDPOINT")] + SetFeatureToEndpoint = 15, + + /// Sets a USB-defined feature on a device-defined target on a USB device. If set, the URB is + /// used with [`(TS)_URB_CONTROL_FEATURE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_SET_FEATURE_TO_OTHER")] + SetFeatureToOther = 35, + + /// Clears a USB-defined feature on a device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_FEATURE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_CLEAR_FEATURE_TO_DEVICE")] + ClearFeatureToDevice = 16, + + /// Clears a USB-defined feature on an interface for a device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_FEATURE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_CLEAR_FEATURE_TO_INTERFACE")] + ClearFeatureToInterface = 17, + + /// Clears a USB-defined feature on an endpoint, for an interface, on a USB device. If set, + /// the URB is used with [`(TS)_URB_CONTROL_FEATURE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_CLEAR_FEATURE_TO_ENDPOINT")] + ClearFeatureToEndpoint = 18, + + /// Clears a USB-defined feature on a device defined target on a USB device. If set, the URB + /// is used with [`(TS)_URB_CONTROL_FEATURE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_CLEAR_FEATURE_TO_OTHER")] + ClearFeatureToOther = 34, + + /// Retrieves status from a USB device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_GET_STATUS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_STATUS_FROM_DEVICE")] + GetStatusFromDevice = 19, + + /// Retrieves status from an interface on a USB device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_GET_STATUS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_STATUS_FROM_INTERFACE")] + GetStatusFromInterface = 20, + + /// Retrieves status from an endpoint for an interface on a USB device. If set, the URB is + /// used with [`(TS)_URB_CONTROL_GET_STATUS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_STATUS_FROM_ENDPOINT")] + GetStatusFromEndpoint = 21, + + /// Retrieves status from a device-defined target on a USB device. If set, the URB is + /// used with [`(TS)_URB_CONTROL_GET_STATUS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_STATUS_FROM_OTHER")] + GetStatusFromOther = 33, + + /// Sends a vendor-specific command to a USB device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_VENDOR_OR_CLASS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_VENDOR_DEVICE")] + VendorDevice = 23, + + /// Sends a vendor-specific command for an interface on a USB device. If set, the URB is + /// used with [`(TS)_URB_CONTROL_VENDOR_OR_CLASS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_VENDOR_INTERFACE")] + VendorInterface = 24, + + /// Sends a vendor-specific command for an endpoint on an interface on a USB device. If set, + /// the URB is used with [`(TS)_URB_CONTROL_VENDOR_OR_CLASS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_VENDOR_ENDPOINT")] + VendorEndpoint = 25, + + /// Sends a vendor-specific command to a device-defined target on a USB device. If set, the + /// URB is used with [`(TS)_URB_CONTROL_VENDOR_OR_CLASS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_VENDOR_OTHER")] + VendorOther = 32, + + /// Sends a USB-defined class-specific command to a USB device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_VENDOR_OR_CLASS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_CLASS_DEVICE")] + ClassDevice = 26, + + /// Sends a USB-defined class-specific command to an interface on a USB device. If set, the + /// URB is used with [`(TS)_URB_CONTROL_VENDOR_OR_CLASS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_CLASS_INTERFACE")] + ClassInterface = 27, + + /// Sends a USB-defined class-specific command to an endpoint, on an interface, on a USB + /// device. If set, the URB is used with [`(TS)_URB_CONTROL_VENDOR_OR_CLASS_REQUEST`] as the data + /// structure. + #[doc(alias = "URB_FUNCTION_CLASS_ENDPOINT")] + ClassEndpoint = 28, + + /// Sends a USB-defined class-specific command to a device defined target on a USB device. If + /// set, the URB is used with [`(TS)_URB_CONTROL_VENDOR_OR_CLASS_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_CLASS_OTHER")] + ClassOther = 31, + + /// Retrieves the current configuration on a USB device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_GET_CONFIGURATION_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_CONFIGURATION")] + GetConfiguration = 38, + + /// Retrieves the current settings for an interface on a USB device. If set, the URB is used + /// with [`(TS)_URB_CONTROL_GET_INTERFACE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_INTERFACE")] + GetInterface = 39, + + /// Retrieves the descriptor from an interface for a USB device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_DESCRIPTOR_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_GET_DESCRIPTOR_FROM_INTERFACE")] + GetDescriptorFromInterface = 40, + + /// Sets a descriptor for an interface on a USB device. If set, the URB is used with + /// [`(TS)_URB_CONTROL_DESCRIPTOR_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_SET_DESCRIPTOR_TO_INTERFACE")] + SetDescriptorToInterface = 41, + + /// Retrieves a Microsoft OS feature descriptor from a USB device or an interface on a USB + /// device. If set, the URB is used with [`(TS)_URB_OS_FEATURE_DESCRIPTOR_REQUEST`] as the data + /// structure. + #[doc(alias = "URB_FUNCTION_GET_MS_FEATURE_DESCRIPTOR")] + GetMsFeatureDescriptor = 42, + + /// Closes all opened streams in the specified bulk endpoint. If set, the URB is used with + /// [`(TS)_URB_PIPE_REQUEST`] as the data structure. + #[doc(alias = "URB_FUNCTION_CLOSE_STATIC_STREAMS")] + CloseStaticStreams = 54, +} + +impl From for u16 { + #[expect(clippy::as_conversions)] + fn from(value: UrbFunction) -> Self { + value as Self + } +} + +impl TryFrom for UrbFunction { + type Error = DecodeError; + + fn try_from(value: u16) -> Result { + use UrbFunction::*; + + match value { + 0 => Ok(SelectConfiguration), + 1 => Ok(SelectInterface), + 2 => Ok(AbortPipe), + 7 => Ok(GetCurrentFrameNumber), + 8 => Ok(ControlTransfer), + 9 => Ok(BulkOrInterruptTransfer), + 10 => Ok(IsochTransfer), + 11 => Ok(GetDescriptorFromDevice), + 12 => Ok(SetDescriptorToDevice), + 13 => Ok(SetFeatureToDevice), + 14 => Ok(SetFeatureToInterface), + 15 => Ok(SetFeatureToEndpoint), + 16 => Ok(ClearFeatureToDevice), + 17 => Ok(ClearFeatureToInterface), + 18 => Ok(ClearFeatureToEndpoint), + 19 => Ok(GetStatusFromDevice), + 20 => Ok(GetStatusFromInterface), + 21 => Ok(GetStatusFromEndpoint), + 23 => Ok(VendorDevice), + 24 => Ok(VendorInterface), + 25 => Ok(VendorEndpoint), + 26 => Ok(ClassDevice), + 27 => Ok(ClassInterface), + 28 => Ok(ClassEndpoint), + 30 => Ok(SyncResetPipeAndClearStall), + 31 => Ok(ClassOther), + 32 => Ok(VendorOther), + 33 => Ok(GetStatusFromOther), + 34 => Ok(ClearFeatureToOther), + 35 => Ok(SetFeatureToOther), + 36 => Ok(GetDescriptorFromEndpoint), + 37 => Ok(SetDescriptorToEndpoint), + 38 => Ok(GetConfiguration), + 39 => Ok(GetInterface), + 40 => Ok(GetDescriptorFromInterface), + 41 => Ok(SetDescriptorToInterface), + 42 => Ok(GetMsFeatureDescriptor), + 48 => Ok(SyncResetPipe), + 49 => Ok(SyncClearStall), + 50 => Ok(ControlTransferEx), + 54 => Ok(CloseStaticStreams), + 55 => Ok(BulkOrInterruptTransferUsingChainedMdl), + 56 => Ok(IsochTransferUsingChainedMdl), + + value => Err(unsupported_value_err!( + "URB Function", + format!("unsupported value: {value}") + )), + } + } +} + +/// Header for every `TS_URB` structure, analogous to how [`SHARED_MSG_HEADER`][1] is for all +/// messages defined in [MS-RDPEUSB][2]. +/// +/// [1]: crate::pdu::common::SharedMsgHeader +/// [2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a1004d0e-99e9-4968-894b-0b924ef2f125 +#[doc(alias = "TS_URB_HEADER")] +pub struct TsUrbHeader { + /// Size in bytes of the `TS_URB` structure the header is used for. + pub size: u16, + /// Indicates what function to perform (see [`UrbFunc`]). + pub urb_function: UrbFunction, + /// An ID that uniquely identifies a [`TRANSFER_IN_REQUEST`][1] or [`TRANSFER_OUT_REQUEST`][2] + /// message. + pub request_id: RequestIdTsUrb, + /// Determines if the client is to send a **Request Completion** message for a + /// [`TRANSFER_IN_REQUEST`] or [`TRANSFER_OUT_REQUEST`] message. + /// + /// * If the header is for a [`TRANSFER_IN_REQUEST`] message, this field **MUST** be `false`; + /// and the client is to send a message in response (either [`URB_COMPLETION`][3] or + /// [`URB_COMPLETION_NO_DATA`][4]). + /// + /// * If the header is for a [`TRANSFER_OUT_REQUEST`] message and this field is `false`; + /// the client is to send a ([`URB_COMPLETION_NO_DATA`]) message in response. + /// + /// * If the header is for a [`TRANSFER_OUT_REQUEST`] message and this field is `true`; + /// the client is *not* to send a ([`URB_COMPLETION_NO_DATA`]) message in response. This field + /// *can* be `true` if: + /// + /// 1. `urb_func` is set to [`UrbFunc::IsochTransfer`] (so the header is being used for a + /// [`TS_URB_ISOCH_TRANSFER`][5] structure), and + /// + /// 2. the [`USB_DEVICE_CAPABILITIES.NoAckIsochWriteJitterBufferSizeInMs`][6] field is + /// non-zero, which represents the amount of outstanding isochronous data the client + /// expects from the server (can be checked with + /// [`NoAckIsochWriteJitterBufSizeInMs::outstanding_isoch_data`][7]). + /// + /// + /// [6]: crate::pdu::dev_sink::UsbDeviceCaps::no_ack_isoch_write_jitter_buf_size + /// [7]: crate::pdu::dev_sink::NoAckIsochWriteJitterBufSizeInMs::outstanding_isoch_data + pub no_ack: bool, +} + +impl TsUrbHeader { + const FIXED_PART_SIZE: usize = const { size_of::() + size_of::() + size_of::() }; +} + +impl Encode for TsUrbHeader { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + dst.write_u16(self.size); + #[expect(clippy::as_conversions)] + dst.write_u16(self.urb_function as u16); + + let no_ack = u32::from(self.no_ack) << 31; + let last32 = u32::from(self.request_id) | no_ack; + dst.write_u32(last32); + + Ok(()) + } + + fn name(&self) -> &'static str { + "TS_URB_HEADER" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +impl Decode<'_> for TsUrbHeader { + fn decode(src: &mut ReadCursor<'_>) -> DecodeResult { + ensure_fixed_part_size!(in: src); + + let size = src.read_u16(); + let urb_func = UrbFunction::try_from(src.read_u16())?; + let last32 = src.read_u32(); + let req_id = RequestIdTsUrb::from(last32); + let no_ack = (last32 >> 31) != 0; + + Ok(Self { + size, + urb_function: urb_func, + request_id: req_id, + no_ack, + }) + } +} diff --git a/crates/ironrdp-rdpeusb/src/pdu/ts_urb/mod.rs b/crates/ironrdp-rdpeusb/src/pdu/ts_urb/mod.rs new file mode 100644 index 000000000..f505d688b --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/ts_urb/mod.rs @@ -0,0 +1 @@ +pub mod header; diff --git a/crates/ironrdp-rdpeusb/src/pdu/usb_dev.rs b/crates/ironrdp-rdpeusb/src/pdu/usb_dev.rs new file mode 100644 index 000000000..a8e2f2db4 --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/usb_dev.rs @@ -0,0 +1,501 @@ +//! Messages specific to the [USB Device][1] interface. +//! +//! This interface is used by the client to communicate with the server about new USB devices. Has +//! no default ID, is allotted an interface ID during the lifetime of a USB Redirection Channel. +//! +//! [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/034257d7-f7a8-4fe1-b8c2-87ac8dc4f50e + +use alloc::format; + +use ironrdp_core::{ + DecodeError, DecodeOwned as _, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_fixed_part_size, + ensure_size, unsupported_value_err, +}; +use ironrdp_pdu::utils::strict_sum; +use ironrdp_str::prefixed::Cch32String; + +use crate::ensure_payload_size; +use crate::pdu::header::{InterfaceId, SharedMsgHeader}; +use crate::pdu::utils::{HResult, RequestId, RequestIdIoctl}; + +/// The `CANCEL_REQUEST` message is sent from the server to the client to cancel an outstanding IO +/// request. +/// +/// * [MS-RDPEUSB § 2.2.6.1 Cancel Request Message (CANCEL_REQUEST)][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/93912b05-1fc8-4a43-8abd-78d9aab65d71 +#[doc(alias = "CANCEL_REQUEST")] +pub struct CancelRequest { + /// The `InterfaceId` field **MUST** match the value sent previously in the `UsbDevice` field + /// of the [`ADD_DEVICE`][1] message. The `Mask` field **MUST** be set to + /// [`STREAM_ID_PROXY`][2]. The `FunctionId` field **MUST** be set to [`CANCEL_REQUEST`][3]. + /// + /// [1]: crate::pdu::dev_sink::AddDevice + /// [2]: crate::pdu::common::Mask::StreamIdProxy + /// [3]: crate::pdu::common::FunctionId::CANCEL_REQUEST + pub header: SharedMsgHeader, + /// Request ID of the oustanding IO request to cancel previously sent via [`IO_CONTROL`], + /// [`INTERNAL_IO_CONTROL`], [`TRANSFER_IN_REQUEST`], or [`TRANSFER_OUT_REQUEST`] message. + pub request_id: RequestId, +} + +impl CancelRequest { + const PAYLOAD_SIZE: usize = size_of::(); + + const FIXED_PART_SIZE: usize = Self::PAYLOAD_SIZE + SharedMsgHeader::SIZE_WHEN_NOT_RSP; + + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + ensure_payload_size!(in: src); + let request_id = src.read_u32(); + + Ok(Self { header, request_id }) + } +} + +impl Encode for CancelRequest { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + self.header.encode(dst)?; + dst.write_u32(self.request_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + "CANCEL_REQUEST" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +/// The `REGISTER_REQUEST_CALLBACK` message is sent from the server to the client to provide an +/// interface ID for the **Request Completion** interface to the client. +/// +/// * [MS-RDPEUSB § 2.2.6.2 Register Request Callback Message (REGISTER_REQUEST_CALLBACK)][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/8693de72-5e87-4b64-a252-101e865311a5 +#[doc(alias = "REGISTER_REQUEST_CALLBACK")] +pub struct RegisterRequestCallback { + /// The `InterfaceId` field **MUST** match the value sent previously in the `UsbDevice` field + /// of the [`ADD_DEVICE`][1] message. The `Mask` field **MUST** be set to + /// [`STREAM_ID_PROXY`][2]. The `FunctionId` field **MUST** be set to + /// [`REGISTER_REQUEST_CALLBACK`][3]. + /// + /// [1]: crate::pdu::dev_sink::AddDevice + /// [2]: crate::pdu::common::Mask::StreamIdProxy + /// [3]: crate::pdu::common::FunctionId::REGISTER_REQUEST_CALLBACK + pub header: SharedMsgHeader, + /// A unique `InterfaceID` to be used by all messages defined in the **Request Completion** + /// interface. + /// + /// NOTE: `Interface` **MUST** be the [`NonDefault`][1] variant. + /// + /// [1]: crate::pdu::common::Interface::NonDefault + pub request_completion: Option, +} + +impl RegisterRequestCallback { + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + ensure_size!(in: src, size: size_of::()); + let request_completion = if src.read_u32(/* NumRequestCompletion */) == 0 { + None + } else { + ensure_size!(in: src, size: InterfaceId::FIXED_PART_SIZE); + let id = src.read_u32(); + Some(InterfaceId::from(id)) + }; + Ok(Self { + header, + request_completion, + }) + } +} + +impl Encode for RegisterRequestCallback { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + self.header.encode(dst)?; + if let Some(request_completion) = self.request_completion { + dst.write_u32(0x1); + dst.write_u32(request_completion.into()); + } else { + dst.write_u32(0x0); + } + + Ok(()) + } + + fn name(&self) -> &'static str { + "REGISTER_REQUEST_CALLBACK" + } + + fn size(&self) -> usize { + const NUM_REQUEST_COMPLETION: usize = size_of::(); + let request_completion = match self.request_completion { + Some(_) => InterfaceId::FIXED_PART_SIZE, + None => 0, + }; + + strict_sum(&[SharedMsgHeader::SIZE_WHEN_NOT_RSP + NUM_REQUEST_COMPLETION + request_completion]) + } +} + +#[repr(u32)] +#[non_exhaustive] +#[doc(alias = "IOCTL_INTERNAL_USB")] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum UsbIoctlCode { + /// `IOCTL_INTERNAL_USB_RESET_PORT` I/O control request. Used by a driver to reset the + /// upstream port of the device it manages. + #[doc(alias = "IOCTL_INTERNAL_USB_RESET_PORT")] + ResetPort = 0x220_007, + + #[doc(alias = "IOCTL_INTERNAL_USB_GET_PORT_STATUS")] + GetPortStatus = 0x220_013, + + #[doc(alias = "IOCTL_INTERNAL_USB_GET_HUB_COUNT")] + GetHubCount = 0x220_01B, + + #[doc(alias = "IOCTL_INTERNAL_USB_CYCLE_PORT")] + CyclePort = 0x220_01F, + + #[doc(alias = "IOCTL_INTERNAL_USB_GET_HUB_NAME")] + GetHubName = 0x220_020, + + #[doc(alias = "IOCTL_INTERNAL_USB_GET_BUS_INFO")] + GetBusInfo = 0x220_420, + + #[doc(alias = "IOCTL_INTERNAL_USB_GET_CONTROLLER_NAME")] + GetControllerName = 0x220_424, +} + +impl UsbIoctlCode { + pub const FIZED_PART_SIZE: usize = size_of::(); +} + +impl TryFrom for UsbIoctlCode { + type Error = DecodeError; + + fn try_from(value: u32) -> Result { + use UsbIoctlCode::*; + + match value { + 0x220_007 => Ok(ResetPort), + 0x220_013 => Ok(GetPortStatus), + 0x220_01B => Ok(GetHubCount), + 0x220_01F => Ok(CyclePort), + 0x220_020 => Ok(GetHubName), + 0x220_420 => Ok(GetBusInfo), + 0x220_424 => Ok(GetControllerName), + value => Err(unsupported_value_err!( + "IoControlCode", + format!( + "is: {value}; is not one of: \ +IOCTL_INTERNAL_USB_RESET_PORT (0x00220007), \ +IOCTL_INTERNAL_USB_GET_PORT_STATUS (0x00220013) \ +IOCTL_INTERNAL_USB_GET_HUB_COUNT (0x0022001B) \ +IOCTL_INTERNAL_USB_CYCLE_PORT (0x0022001F) \ +IOCTL_INTERNAL_USB_GET_HUB_NAME (0x00220020) \ +IOCTL_INTERNAL_USB_GET_BUS_INFO (0x00220420) \ +IOCTL_INTERNAL_USB_GET_CONTROLLER_NAME (0x00220424)", + ) + )), + } + } +} + +#[doc(alias = "IO_CONTROL")] +pub struct IoCtl { + pub header: SharedMsgHeader, + pub ioctl_code: UsbIoctlCode, + // As of v20240423, all USB IO Control Code's ([MS-RDPEUSB] 2.2.12 USB IO Control Code) used + // in the protocol require setting input_buffer_size = 0 + // + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/4f4574f0-9368-4708-8f98-06aa2f44e198 + // pub input_buffer_size: u32, + // pub input_buffer: Vec, + pub output_buffer_size: u32, + pub request_id: RequestIdIoctl, +} + +impl IoCtl { + #[expect(clippy::identity_op, reason = "for developer documentation purposes?")] + pub const PAYLOAD_SIZE: usize = UsbIoctlCode::FIZED_PART_SIZE + + size_of::(/* InputBufferSize */) + + 0 /* InputBuffer */ + + size_of::(/* OutputBufferSize */) + + size_of::(/* RequestId */); + + pub const FIXED_PART_SIZE: usize = Self::PAYLOAD_SIZE + SharedMsgHeader::SIZE_WHEN_NOT_RSP; + + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + ensure_payload_size!(in: src); + + let ioctl_code = UsbIoctlCode::try_from(src.read_u32())?; + + if let size @ 1.. = src.read_u32(/* InputBufferSize */) { + return Err(unsupported_value_err!( + "IO_CONTROL::InputBufferSize", + format!("is: {size:#X}; should be: 0x0") + )); + } + + let output_buffer_size = { + let size = src.read_u32(); + + const NAME: &str = "IO_CONTROL::OutputBufferSize"; + + use UsbIoctlCode::*; + match ioctl_code { + ResetPort | CyclePort if size != 0x0 => { + return Err(unsupported_value_err!(NAME, format!("is: {size:#X}; should be: 0x0"))); + } + GetPortStatus | GetHubCount if size != 0x4 => { + return Err(unsupported_value_err!(NAME, format!("is: {size:#X}; should be: 0x4"))); + } + _ => size, + } + }; + + let request_id = src.read_u32(); + + Ok(Self { + header, + ioctl_code, + output_buffer_size, + request_id, + }) + } +} + +impl Encode for IoCtl { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + self.header.encode(dst)?; + + #[expect(clippy::as_conversions)] + dst.write_u32(self.ioctl_code as u32); + + dst.write_u32(0x0); // InputBufferSize + // dst.write_slice(Vec::from(...); // since InputBufferSize = 0x0 + + dst.write_u32(self.output_buffer_size); + dst.write_u32(self.request_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + "IO_CONTROL" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +// #[repr(u32)] +// #[non_exhaustive] +// #[doc(alias = "IOCTL_TSUSBGD_IOCTL_USBDI_QUERY_BUS_TIME")] +// #[derive(Debug, PartialEq, Eq, Clone, Copy)] +// pub enum UsbInternalIoctlCode { +// #[doc(alias = "IOCTL_TSUSBGD_IOCTL_USBDI_QUERY_BUS_TIME")] +// IoctlTsusbgdIoctlUsbdiQueryBusTime = 0x00224000, +// } + +const IOCTL_TSUSBGD_IOCTL_USBDI_QUERY_BUS_TIME: u32 = 0x00224000; + +#[doc(alias = "INTERNAL_IO_CONTROL")] +pub struct InternalIoCtl { + pub header: SharedMsgHeader, + // As of v20240423, only USB Internal IO Control Code ([MS-RDPEUSB] 2.2.13 USB Internal IO + // Control Code) used in the protocol is IOCTL_TSUSBGD_IOCTL_USBDI_QUERY_BUS_TIME (0x00224000) + // + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/55d1cd44-eda3-4cba-931c-c3cb8b3c3c92 + // pub ioctl_code: UsbInternalIoctlCode, + // + // As of v20240423, all USB Internal IO Control Code's ([MS-RDPEUSB] 2.2.13 USB Internal IO + // Control Code) used in the protocol require setting input_buffer_size = 0 + // + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/55d1cd44-eda3-4cba-931c-c3cb8b3c3c92 + // pub input_buffer_size: u32, + // pub input_buffer: Vec, + // + // As of v20240423, all USB Internal IO Control Code's ([MS-RDPEUSB] 2.2.13 USB Internal IO + // Control Code) used in the protocol require setting output_buffer_size = 0x4 + // + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/55d1cd44-eda3-4cba-931c-c3cb8b3c3c92 + // pub output_buffer_size: u32, + pub request_id: RequestIdIoctl, +} + +impl InternalIoCtl { + #[expect(clippy::identity_op, reason = "for developer documentation purposes?")] + pub const PAYLOAD_SIZE: usize = size_of::() // IoControlCode + + size_of::(/* InputBufferSize */) + + 0 // InputBuffer + + size_of::(/* OutputBufferSize */) + + size_of::(/* RequestId */); + + pub const FIXED_PART_SIZE: usize = Self::PAYLOAD_SIZE + SharedMsgHeader::SIZE_WHEN_NOT_RSP; + + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + ensure_payload_size!(in: src); + + { + let code = src.read_u32(); + if code != IOCTL_TSUSBGD_IOCTL_USBDI_QUERY_BUS_TIME { + return Err(unsupported_value_err!( + "INTERNAL_IO_CONTROL::IoControlCode", + format!("is: {code:#X}; should be: {IOCTL_TSUSBGD_IOCTL_USBDI_QUERY_BUS_TIME:#X}") + )); + } + } + { + let size = src.read_u32(/* InputBufferSize */); + if size != 0x0 { + return Err(unsupported_value_err!( + "INTERNAL_IO_CONTROL::InputBufferSize", + format!("is: {size:#X}; should be: 0x0") + )); + } + } + { + let size = src.read_u32(/* OutputBufferSize */); + if size != 0x4 { + return Err(unsupported_value_err!( + "INTERNAL_IO_CONTROL::InputBufferSize", + format!("is: {size:#X}; should be: 0x4") + )); + } + } + let request_id = src.read_u32(); + + Ok(Self { header, request_id }) + } +} + +impl Encode for InternalIoCtl { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + self.header.encode(dst)?; + dst.write_u32(IOCTL_TSUSBGD_IOCTL_USBDI_QUERY_BUS_TIME); // IoControlCode + dst.write_u32(0x0); // InputBufferSize + dst.write_u32(0x4); // OutputBufferSize + dst.write_u32(self.request_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + "INTERNAL_IO_CONTROL" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +#[doc(alias = "QUERY_DEVICE_TEXT")] +pub struct QueryDeviceText { + pub header: SharedMsgHeader, + // NOTE: TextType and LocaleId fields aren't just merely "numbers", they can be made into an + // enum ([1]) and struct ([2]) respectively. + // + // But QUERY_DEVICE_TEXT is just a "bridge" for IRP_MN_QUERY_DEVICE_TEXT ([3]) sent by the USB + // driver stack on the server side. For the server, these don't *need* to mean anything more + // than "just mere numbers". At the client side, the client just needs to hand these off to the + // USB host controller. + // + // [3]: https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/irp-mn-query-device-text + // [1]: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ne-wdm-device_text_type + // [2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f + pub text_type: u32, + pub locale_id: u32, +} + +impl QueryDeviceText { + pub const PAYLOAD_SIZE: usize = size_of::(/* TextType */) + size_of::(/* LocaleId */); + + pub const FIXED_PART_SIZE: usize = Self::PAYLOAD_SIZE + SharedMsgHeader::SIZE_WHEN_NOT_RSP; + + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + ensure_payload_size!(in: src); + + let text_type = src.read_u32(); + let locale_id = src.read_u32(); + + Ok(Self { + header, + text_type, + locale_id, + }) + } +} + +impl Encode for QueryDeviceText { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_fixed_part_size!(in: dst); + + self.header.encode(dst)?; + dst.write_u32(self.text_type); + dst.write_u32(self.locale_id); + + Ok(()) + } + + fn name(&self) -> &'static str { + "QUERY_DEVICE_TEXT" + } + + fn size(&self) -> usize { + Self::FIXED_PART_SIZE + } +} + +#[doc(alias = "QUERY_DEVICE_TEXT_RSP")] +pub struct QueryDeviceTextRsp { + pub header: SharedMsgHeader, + pub device_description: Cch32String, + pub hresult: HResult, +} + +impl QueryDeviceTextRsp { + pub fn decode(src: &mut ReadCursor<'_>, header: SharedMsgHeader) -> DecodeResult { + let device_description = Cch32String::decode_owned(src)?; + + ensure_size!(in: src, size: 4); // HResult + let hresult = src.read_u32(); + + Ok(Self { + header, + device_description, + hresult, + }) + } +} + +impl Encode for QueryDeviceTextRsp { + fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { + ensure_size!(in: dst, size: self.size()); + + self.header.encode(dst)?; + self.device_description.encode(dst)?; + + dst.write_u32(self.hresult); + + Ok(()) + } + + fn name(&self) -> &'static str { + "QUERY_DEVICE_TEXT_RSP" + } + + fn size(&self) -> usize { + strict_sum(&[SharedMsgHeader::SIZE_WHEN_RSP + self.device_description.size() + const { size_of::() }]) + } +} diff --git a/crates/ironrdp-rdpeusb/src/pdu/utils.rs b/crates/ironrdp-rdpeusb/src/pdu/utils.rs new file mode 100644 index 000000000..a2f989e9a --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/pdu/utils.rs @@ -0,0 +1,71 @@ +/// An integer value that indicates the result or status of an operation. +/// +/// * [MS-ERREF § 2.1 HRESULT][1] +/// +/// [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0642cb2f-2075-4469-918c-4441e69c548a +pub type HResult = u32; + +/// The [`CANCEL_REQUEST::request_id`] field. Represents the ID of a request previously sent via +/// IO_CONTROL, INTERNAL_IO_CONTROL, TRANSFER_IN_REQUEST, or TRANSFER_OUT_REQUEST message. Think of +/// this like an "umbrella" type for [`RequestIdIoctl`] and [`RequestIdTsUrb`]. +pub type RequestId = u32; + +/// Represents a request ID that uniquely identifies an `IO_CONTROL` or `INTERNAL_IO_CONTROL` +/// message. +pub type RequestIdIoctl = u32; + +/// Represents a request ID that uniquely identifies a `TRANSFER_IN_REQUEST` or +/// `TRANSFER_OUT_REQUEST` message. 31 bits. +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] +pub struct RequestIdTsUrb(u32); + +impl From for RequestIdTsUrb { + /// Construct a request ID for `TRANSFER_IN_REQUEST` or `TRANSFER_OUT_REQUEST`. Discards + /// highest bit. + fn from(value: u32) -> Self { + Self(value & 0x7F_FF_FF_FF) + } +} + +impl From for u32 { + fn from(value: RequestIdTsUrb) -> Self { + value.0 + } +} + +// TODO: This could be moved to ironrdp-core. +// +/// Ensures that a buffer has at least the payload size of a struct. +/// +/// This macro is a specialized version of `ensure_size` that uses the +/// `PAYLOAD_SIZE` constant of the current struct. +/// +/// # Examples +/// +/// ``` +/// use ironrdp_rdpeusb::ensure_payload_size; +/// +/// struct MyStruct { +/// // ... fields +/// } +/// +/// impl MyStruct { +/// const PAYLOAD_SIZE: usize = 20; +/// +/// fn parse(buf: &[u8]) -> Result { +/// ensure_payload_size!(in: buf); +/// // ... parsing logic +/// } +/// } +/// ``` +/// +/// # Note +/// +/// This macro assumes that the current struct has a `PAYLOAD_SIZE` constant defined. +#[macro_export] +macro_rules! ensure_payload_size { + (in: $buf:ident) => {{ + ironrdp_core::ensure_size!(ctx: ironrdp_core::function!(), in: $buf, size: Self::PAYLOAD_SIZE) + }}; +}