diff --git a/Cargo.lock b/Cargo.lock index 017254825..87b8424e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2801,6 +2801,7 @@ name = "ironrdp-rdpeusb" version = "0.1.0" dependencies = [ "ironrdp-core", + "ironrdp-dvc", "ironrdp-pdu", "ironrdp-str", ] diff --git a/crates/ironrdp-rdpeusb/Cargo.toml b/crates/ironrdp-rdpeusb/Cargo.toml index c34482407..3b44cc35a 100644 --- a/crates/ironrdp-rdpeusb/Cargo.toml +++ b/crates/ironrdp-rdpeusb/Cargo.toml @@ -23,6 +23,7 @@ std = [] [dependencies] ironrdp-core = { path = "../ironrdp-core", version = "0.2", features = ["alloc"] } # public ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.8", features = ["alloc"] } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.6" } # public ironrdp-str = { path = "../ironrdp-str", version = "0.1" } [lints] diff --git a/crates/ironrdp-rdpeusb/src/client/device.rs b/crates/ironrdp-rdpeusb/src/client/device.rs new file mode 100644 index 000000000..488e902c1 --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/client/device.rs @@ -0,0 +1,453 @@ +//! Backend-neutral USB device facts and the RDPEUSB-specific ADD_DEVICE conversion. +//! +//! Backends should fill [`DeviceInfo`] with raw USB topology/descriptor data. This module is +//! responsible for turning those facts into Windows PnP-style strings and RDPEUSB wire wrappers. +//! +//! The split is intentional: +//! - RDPEUSB defines the ADD_DEVICE fields and their wire types, but not every generation detail. +//! - Windows PnP/USB defines the usual hardware ID and compatibility ID formats used for driver +//! matching. +//! - Device instance ID and container ID are enumerator policy. They must be stable identifiers +//! with the RDPEUSB-required shape, so this implementation follows FreeRDP's observed strategy. +//! +//! References: +//! - [MS-RDPEUSB ADD_DEVICE] +//! - [MS-RDPEUSB USB_DEVICE_CAPABILITIES] +//! - [Windows device identification strings] +//! - [Standard USB identifiers] +//! - [USB composite device enumeration] +//! - [USB container ID assignment] +//! - [FreeRDP urbdrc_main.c] +//! - [FreeRDP libusb_udevice.c] +//! +//! [MS-RDPEUSB ADD_DEVICE]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/a26bcb6d-d45d-48a9-b9bd-22e0107d8393 +//! [MS-RDPEUSB USB_DEVICE_CAPABILITIES]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/98d4650e-b6d8-47e5-b71b-4d320ab542ee +//! [Windows device identification strings]: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/device-identification-strings +//! [Standard USB identifiers]: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/standard-usb-identifiers +//! [USB composite device enumeration]: https://learn.microsoft.com/en-us/windows-hardware/drivers/usbcon/enumeration-of-the-composite-parent-device +//! [USB container ID assignment]: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/how-usb-devices-are-assigned-container-ids +//! [FreeRDP urbdrc_main.c]: https://github.com/FreeRDP/FreeRDP/blob/master/channels/urbdrc/client/urbdrc_main.c +//! [FreeRDP libusb_udevice.c]: https://github.com/FreeRDP/FreeRDP/blob/master/channels/urbdrc/client/libusb/libusb_udevice.c + +use alloc::{format, string::String, vec, vec::Vec}; + +use ironrdp_pdu::{PduResult, pdu_other_err}; +use ironrdp_str::multi_sz::MultiSzString; +use ironrdp_str::prefixed::Cch32String; + +use crate::pdu::header::{InterfaceId, MessageId}; +use crate::pdu::sink::{ + AddDevice, DeviceSpeed, NoAckIsochWriteJitterBufSizeInMs, SupportedUsbVer, UsbBusIfaceVer, UsbDeviceCaps, UsbdiVer, +}; + +const ADD_DEVICE_MESSAGE_ID: MessageId = 0; +const DEFAULT_NO_ACK_ISOCH_WRITE_JITTER_MS: u32 = 0x50; + +const USB_CLASS_PER_INTERFACE: u8 = 0x00; +const USB_CLASS_MISCELLANEOUS: u8 = 0xef; +const USB_SUBCLASS_COMMON: u8 = 0x02; +const USB_PROTOCOL_INTERFACE_ASSOCIATION: u8 = 0x01; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceInfo { + /// Physical/topological location. Used to derive stable Windows PnP instance/container IDs. + pub location: UsbDeviceLocation, + /// Raw fields from the USB device descriptor. + pub descriptor: UsbDeviceDescriptorInfo, + /// Active configuration, if the backend can read it. Used for composite detection and + /// first-interface class codes. + pub active_config: Option, + /// Backend-observed connection speed. RDPEUSB only carries a high-speed boolean. + pub speed: UsbConnectionSpeed, +} + +impl DeviceInfo { + pub fn new( + location: UsbDeviceLocation, + descriptor: UsbDeviceDescriptorInfo, + active_config: Option, + speed: UsbConnectionSpeed, + ) -> Self { + Self { + location, + descriptor, + active_config, + speed, + } + } + + fn is_composite(&self) -> bool { + let descriptor_class = self.descriptor.class_codes; + // Match FreeRDP/libusb composite detection: either a per-interface class device with + // multiple interfaces, or an Interface Association Descriptor style device class. + // + // Refs: [USB composite device enumeration]; [FreeRDP libusb_udevice.c] + // `interface_create()`. + let has_single_config_multiple_interfaces = self.descriptor.num_configurations == 1 + && descriptor_class.class_code == USB_CLASS_PER_INTERFACE + && self + .active_config + .as_ref() + .is_some_and(|config| config.interfaces.len() > 1); + + let has_interface_association_descriptor = descriptor_class.class_code == USB_CLASS_MISCELLANEOUS + && descriptor_class.sub_class_code == USB_SUBCLASS_COMMON + && descriptor_class.protocol_code == USB_PROTOCOL_INTERFACE_ASSOCIATION; + + has_single_config_multiple_interfaces || has_interface_association_descriptor + } + + fn pnp_class_codes(&self) -> UsbClassCodes { + // FreeRDP uses the first active interface class for compatibility IDs after checking + // whether the whole device is composite. + // + // Ref: [FreeRDP libusb_udevice.c] `interface_create()`. + self.active_config + .as_ref() + .and_then(|config| config.interfaces.first()) + .map(|interface| interface.class_codes) + .unwrap_or(self.descriptor.class_codes) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UsbDeviceLocation { + pub bus_number: u8, + pub address: u8, + pub port_numbers: Vec, +} + +impl UsbDeviceLocation { + pub fn new(bus_number: u8, address: u8, port_numbers: Vec) -> Self { + Self { + bus_number, + address, + port_numbers, + } + } + + fn path(&self) -> String { + // FreeRDP uses "bus-last_port" as the device path. Keep the full port chain in + // DeviceInfo for backend fidelity, but only the last port participates in ADD_DEVICE IDs. + // + // Ref: [FreeRDP libusb_udevice.c] `udev_get_device_handle()`. + let last_port_or_address = self.port_numbers.last().copied().unwrap_or(self.address); + + format!("{}-{last_port_or_address}", self.bus_number) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UsbDeviceDescriptorInfo { + pub vendor_id: u16, + pub product_id: u16, + pub device_version: UsbBcdVersion, + pub usb_version: UsbBcdVersion, + pub class_codes: UsbClassCodes, + pub num_configurations: u8, +} + +impl UsbDeviceDescriptorInfo { + pub const fn new( + vendor_id: u16, + product_id: u16, + device_version: UsbBcdVersion, + usb_version: UsbBcdVersion, + class_codes: UsbClassCodes, + num_configurations: u8, + ) -> Self { + Self { + vendor_id, + product_id, + device_version, + usb_version, + class_codes, + num_configurations, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UsbConfigInfo { + pub interfaces: Vec, +} + +impl UsbConfigInfo { + pub fn new(interfaces: Vec) -> Self { + Self { interfaces } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UsbInterfaceInfo { + pub class_codes: UsbClassCodes, +} + +impl UsbInterfaceInfo { + pub const fn new(class_codes: UsbClassCodes) -> Self { + Self { class_codes } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UsbClassCodes { + pub class_code: u8, + pub sub_class_code: u8, + pub protocol_code: u8, +} + +impl UsbClassCodes { + pub const PER_INTERFACE: Self = Self::new(0x00, 0x00, 0x00); + + pub const fn new(class_code: u8, sub_class_code: u8, protocol_code: u8) -> Self { + Self { + class_code, + sub_class_code, + protocol_code, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UsbBcdVersion { + pub major: u8, + pub minor: u8, + pub sub_minor: u8, +} + +impl UsbBcdVersion { + pub const fn new(major: u8, minor: u8, sub_minor: u8) -> Self { + Self { + major, + minor, + sub_minor, + } + } + + #[expect(clippy::as_conversions)] + pub const fn from_bcd(value: u16) -> Self { + Self { + major: ((value >> 8) & 0x0f) as u8, + minor: ((value >> 4) & 0x0f) as u8, + sub_minor: (value & 0x0f) as u8, + } + } + + fn to_bcd(self, invalid_description: &'static str) -> PduResult { + if self.major <= 0x0f && self.minor <= 0x0f && self.sub_minor <= 0x0f { + Ok((u16::from(self.major) << 8) | (u16::from(self.minor) << 4) | u16::from(self.sub_minor)) + } else { + Err(pdu_other_err!(invalid_description)) + } + } + + fn to_supported_usb_version(self) -> PduResult { + let bcd = self.to_bcd("usb_version is not a valid USB BCD version")?; + + Ok(if bcd >= 0x0200 { + SupportedUsbVer::Usb20 + } else if bcd >= 0x0110 { + SupportedUsbVer::Usb11 + } else { + SupportedUsbVer::Usb10 + }) + } + + fn is_at_least_usb20(self) -> PduResult { + Ok(self.to_bcd("usb_version is not a valid USB BCD version")? >= 0x0200) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UsbConnectionSpeed { + Unknown, + Low, + Full, + High, + Super, + SuperPlus, +} + +/// Convert backend USB facts into the RDPEUSB ADD_DEVICE PDU. +/// +/// `usb_device` is deliberately passed separately: it is the per-device USB interface ID allocated +/// by the DVC processor, not a property of the USB backend device. +/// +/// The output strings are Windows PnP identifiers. They are opaque to this crate once generated; +/// the important part is using the standard USB forms and keeping instance/container values stable. +pub fn add_device_from_info(usb_device: InterfaceId, info: &DeviceInfo) -> PduResult { + let device_version = info + .descriptor + .device_version + .to_bcd("device_version is not a valid USB BCD version")?; + let location_path = info.location.path(); + + Ok(AddDevice { + msg_id: ADD_DEVICE_MESSAGE_ID, + usb_device, + // Cch32String and MultiSzString are wire-format concerns. Keep DeviceInfo plain and build + // these counted UTF-16 wrappers only at the RDPEUSB boundary. + // + // Ref: [MS-RDPEUSB ADD_DEVICE] field definitions for cchDeviceInstanceId, cchHwIds, + // cchCompatIds, and cchContainerId. + device_instance_id: Cch32String::new(device_instance_id(&location_path)), + hw_ids: Some( + MultiSzString::new(hardware_ids( + info.descriptor.vendor_id, + info.descriptor.product_id, + device_version, + )) + .map_err(|e| pdu_other_err!("generated ADD_DEVICE hardware IDs contain an embedded nul", source: e))?, + ), + compat_ids: Some(MultiSzString::new(compatibility_ids(info)).map_err( + |e| pdu_other_err!("generated ADD_DEVICE compatibility IDs contain an embedded nul", source: e), + )?), + container_id: Cch32String::new(container_id( + info.descriptor.vendor_id, + info.descriptor.product_id, + &location_path, + )), + usb_device_caps: usb_device_caps(info)?, + }) +} + +fn hardware_ids(vendor_id: u16, product_id: u16, device_version: u16) -> Vec { + // Windows PnP hardware IDs, ordered from most specific to less specific. + // + // Refs: [Standard USB identifiers]; [FreeRDP urbdrc_main.c] + // `urdbrc_send_usb_device_add()`. + vec![ + format!("USB\\VID_{vendor_id:04X}&PID_{product_id:04X}&REV_{device_version:04X}"), + format!("USB\\VID_{vendor_id:04X}&PID_{product_id:04X}"), + ] +} + +fn compatibility_ids(info: &DeviceInfo) -> Vec { + if info.is_composite() { + // Composite devices advertise DevClass_00 plus USB\COMPOSITE, matching FreeRDP. + // + // Refs: [USB composite device enumeration]; [FreeRDP urbdrc_main.c] + // `urdbrc_send_usb_device_add()`. + vec![ + String::from("USB\\DevClass_00&SubClass_00&Prot_00"), + String::from("USB\\DevClass_00&SubClass_00"), + String::from("USB\\DevClass_00"), + String::from("USB\\COMPOSITE"), + ] + } else { + let codes = info.pnp_class_codes(); + + // Non-composite devices advertise class/subclass/protocol in decreasing specificity. + // + // Refs: [Standard USB identifiers]; [FreeRDP urbdrc_main.c] + // `urdbrc_send_usb_device_add()`. + vec![ + format!( + "USB\\Class_{:02X}&SubClass_{:02X}&Prot_{:02X}", + codes.class_code, codes.sub_class_code, codes.protocol_code + ), + format!( + "USB\\Class_{:02X}&SubClass_{:02X}", + codes.class_code, codes.sub_class_code + ), + format!("USB\\Class_{:02X}", codes.class_code), + ] + } +} + +fn usb_device_caps(info: &DeviceInfo) -> PduResult { + // These constants mirror FreeRDP's ADD_DEVICE capabilities. The current PDU enum only models + // USB 1.0/1.1/2.0, so USB 3.x backend versions are reported as Usb20 for this field. + // + // Refs: [MS-RDPEUSB USB_DEVICE_CAPABILITIES]; [FreeRDP urbdrc_main.c] + // `urbdrc_send_add_device()`. + Ok(UsbDeviceCaps { + usb_bus_iface_ver: UsbBusIfaceVer::V2, + usbdi_ver: UsbdiVer::V0x600, + supported_usb_ver: info.descriptor.usb_version.to_supported_usb_version()?, + device_speed: device_speed(info)?, + no_ack_isoch_write_jitter_buf_size: NoAckIsochWriteJitterBufSizeInMs::try_from( + DEFAULT_NO_ACK_ISOCH_WRITE_JITTER_MS, + ) + .map_err(|_| pdu_other_err!("default isochronous jitter buffer size is invalid"))?, + }) +} + +fn device_speed(info: &DeviceInfo) -> PduResult { + match info.speed { + UsbConnectionSpeed::Low | UsbConnectionSpeed::Full => Ok(DeviceSpeed::FullSpeed), + UsbConnectionSpeed::High | UsbConnectionSpeed::Super | UsbConnectionSpeed::SuperPlus => { + Ok(DeviceSpeed::HighSpeed) + } + UsbConnectionSpeed::Unknown => { + if info.descriptor.usb_version.is_at_least_usb20()? { + Ok(DeviceSpeed::HighSpeed) + } else { + Ok(DeviceSpeed::FullSpeed) + } + } + } +} + +fn device_instance_id(location_path: &str) -> String { + // FreeRDP formats a zero-padded 16-byte ASCII seed as a GUID-looking instance ID. + // + // RDPEUSB only requires a null-terminated Unicode string identifying the USB device instance. + // Windows device identification strings are opaque string-comparison keys, so this is an + // enumerator policy choice rather than a USB descriptor field. + // + // Refs: [MS-RDPEUSB ADD_DEVICE] DeviceInstanceId; [Windows device identification strings]; + // [FreeRDP urbdrc_main.c] `func_instance_id_generate()`. + let raw = format!("\\{location_path}"); + + guid_from_bytes(bytes16_from_ascii(raw.as_bytes()), false) +} + +fn container_id(vendor_id: u16, product_id: u16, location_path: &str) -> String { + // Container ID uses VID/PID plus the last 8 bytes of the location path, with braces. + // + // RDPEUSB requires a non-zero GUID string. Windows uses container IDs to group devnodes that + // represent the same physical device; without the full Windows USB/ACPI/container descriptor + // heuristic available on the client side, follow FreeRDP's stable VID/PID/path-derived value. + // + // Refs: [MS-RDPEUSB ADD_DEVICE] ContainerId; [USB container ID assignment]; + // [FreeRDP urbdrc_main.c] `func_container_id_generate()`. + let path_suffix = location_path + .get(location_path.len().saturating_sub(8)..) + .expect("location path is ASCII"); + let raw = format!("{vendor_id:04X}{product_id:04X}{path_suffix}"); + guid_from_bytes(bytes16_from_ascii(raw.as_bytes()), true) +} + +fn bytes16_from_ascii(value: &[u8]) -> [u8; 16] { + let mut bytes = [0; 16]; + let copy_len = value.len().min(bytes.len()); + + bytes[..copy_len].copy_from_slice(&value[..copy_len]); + + bytes +} + +fn guid_from_bytes(bytes: [u8; 16], braces: bool) -> String { + let guid = format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + bytes[0], + bytes[1], + bytes[2], + bytes[3], + bytes[4], + bytes[5], + bytes[6], + bytes[7], + bytes[8], + bytes[9], + bytes[10], + bytes[11], + bytes[12], + bytes[13], + bytes[14], + bytes[15], + ); + + if braces { format!("{{{guid}}}") } else { guid } +} diff --git a/crates/ironrdp-rdpeusb/src/client/mod.rs b/crates/ironrdp-rdpeusb/src/client/mod.rs new file mode 100644 index 000000000..6a590efba --- /dev/null +++ b/crates/ironrdp-rdpeusb/src/client/mod.rs @@ -0,0 +1,770 @@ +use alloc::collections::btree_map::{BTreeMap, Entry}; +use alloc::string::String; +use alloc::vec; +use alloc::{boxed::Box, vec::Vec}; +use ironrdp_core::{Decode as _, EncodeResult, ReadCursor, impl_as_any, other_err}; +use ironrdp_dvc::{DvcChannelListener, DvcClientProcessor, DvcMessage, DvcProcessor}; +use ironrdp_pdu::{PduResult, decode_err, pdu_other_err}; + +use crate::CHANNEL_NAME; +use crate::pdu::UrbdrcServerDevicePdu; +use crate::pdu::completion::ts_urb_result::TsUrbResult; +use crate::pdu::completion::{IoControlCompletion, UrbCompletion, UrbCompletionNoData}; +use crate::pdu::header::{InterfaceId, Mask, MessageId}; +use crate::pdu::iface_manipulation::{InterfaceRelease, QueryInterfaceFailureResponse}; +use crate::pdu::sink::AddVirtualChannel; +use crate::pdu::usb_dev::ts_urb::TsUrbOut; +use crate::pdu::usb_dev::{InternalIoControl, IoControl, QueryDeviceTextRsp, TransferInRequest, TransferOutRequest}; +use crate::pdu::utils::{HResult, RequestId, RequestIdTransferInOut}; +use crate::pdu::{ + UrbdrcServerControlPdu, + caps::{Capability, RimExchangeCapabilityResponse}, + notify::ChannelCreated, +}; + +pub mod device; +pub use device::*; + +pub trait DeviceManagerBackend: Send { + /// Called when the first URBDRC DVC is assigned as the control DVC. + /// + /// This happens from listener.create(channel_id), before the DVC is fully open. + fn control_channel_assigned(&mut self, channel_id: u32); + + /// Called for each later URBDRC DVC create request. + /// + /// The manager should pop the pending device that caused ADD_VIRTUAL_CHANNEL + fn take_device_for_channel(&mut self, channel_id: u32) -> Option>; +} + +pub struct UrbdrcListener { + ctl_created: bool, + on_capability_exchanged: Option, + device_man: Box, + iface_man: InterfaceAlloc, +} + +impl UrbdrcListener { + pub fn new(callback: OnCapabilityExchanged, device_man: Box) -> Self { + Self { + ctl_created: false, + on_capability_exchanged: Some(callback), + device_man, + iface_man: InterfaceAlloc::new(), + } + } +} + +struct InterfaceAlloc { + id: u32, +} + +impl InterfaceAlloc { + #[inline] + const fn new() -> Self { + Self { id: 3 } + } + + #[inline] + const fn alloc(&mut self) -> InterfaceId { + self.id = self.id.checked_add(1).expect("USB device amount overflow"); + InterfaceId(self.id) + } +} + +impl DvcChannelListener for UrbdrcListener { + fn channel_name(&self) -> &str { + CHANNEL_NAME + } + + fn create(&mut self, channel_id: u32) -> Option> { + #[expect(clippy::as_conversions)] + if self.ctl_created { + self.device_man.take_device_for_channel(channel_id).map(|backend| { + Box::new(UrbdrcDeviceClient::new(self.iface_man.alloc(), backend)) as Box + }) + } else { + self.device_man.control_channel_assigned(channel_id); + self.on_capability_exchanged.take().map(|callback| { + self.ctl_created = true; + Box::new(UrbdrcControlClient::new(callback)) as Box + }) + } + } +} + +/// A client for the URBDRC Control Virtual Channel. +pub struct UrbdrcControlClient { + /// Indicates whether the channel is ready for add virtual channel. + ready: bool, + + /// Spec [3.1]: + /// Exchange-completed event: Signifies that the capability exchange is completed, that is, + /// the client has sent a Channel Created message. + /// + /// [3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/511b4cd7-1940-4631-90ac-bf2189ba6735 + on_capability_exchanged: OnCapabilityExchanged, +} + +type OnCapabilityExchanged = Box PduResult> + Send>; + +impl UrbdrcControlClient { + /// Create a new [UrbdrcControlClient] with the given callback. + /// + /// The `callback` will be called when the capability exchange is completed and the channel is + /// ready to redirect new devices. + /// + /// Please note the `callback` will be called only once. + pub fn new PduResult> + Send + 'static>(callback: F) -> Self { + Self { + ready: false, + on_capability_exchanged: Box::new(callback), + } + } + + /// Whether the channel is ready for add virtual channel. + pub const fn ready(&self) -> bool { + self.ready + } + + /// Spec [3.3.5.1.1]: + /// + /// The client sends the ADD_VIRTUAL_CHANNEL message to server to request the server to create a + /// new instance of dynamic virtual channel for USB redirection. The client sends this message + /// for every USB device to be redirected. This isolates messages for each USB device in its own + /// instance of a dynamic virtual channel. + /// + /// [3.3.5.1.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/c7b1920a-d632-46d2-b62a-5c7e53570628 + pub fn add_virtual_channel(&self, dev_id: u32) -> EncodeResult { + if !self.ready { + return Err(other_err!("is not ready for ADD_VIRTUAL_CHANNEL")); + } + // Follow FreeRDP use device id as message id + Ok(Box::new(AddVirtualChannel { msg_id: dev_id })) + } +} + +impl DvcProcessor for UrbdrcControlClient { + fn channel_name(&self) -> &str { + CHANNEL_NAME + } + + fn start(&mut self, _channel_id: u32) -> PduResult> { + Ok(Vec::new()) + } + + fn process(&mut self, _channel_id: u32, payload: &[u8]) -> PduResult> { + let pdu = UrbdrcServerControlPdu::decode(&mut ReadCursor::new(payload)).map_err(|e| decode_err!(e))?; + use UrbdrcServerControlPdu::*; + match pdu { + Caps(caps_req_pdu) => Ok(vec![Box::new(RimExchangeCapabilityResponse { + msg_id: caps_req_pdu.msg_id, + capability: Capability::RimCapabilityVersion01, + result: 0, + })]), + ChanCreated(chan_created_pdu) => Ok(vec![Box::new(ChannelCreated { + msg_id: chan_created_pdu.msg_id, + direction: crate::pdu::notify::Direction::ToServer, + })]), + QueryIfaceReq(query_face_pdu) => Ok(vec![Box::new(QueryInterfaceFailureResponse { + iface_id: query_face_pdu.iface_id, + msg_id: query_face_pdu.msg_id, + })]), + IfaceRelease(InterfaceRelease { + iface_id, + msg_id: _msg_id, + }) => { + if iface_id == InterfaceId::NOTIFY_CLIENT.with_mask(Mask::Proxy) && !self.ready { + // NOTE: MS-RDPEUSB does not normatively define RIMCALL_RELEASE as a + // server-ready-proceed barrier; the semantic comes from observed Windows + // urbdrc-server behavior. Pattern matches FreeRDP urbdrc_main.c since 2012 + // (commit fa4d8fca1be, Atrust contribution). Two sync points: control DVC + // (server -> client ADD_VIRTUAL_CHANNEL); device DVC (server -> client + // ADD_DEVICE). + self.ready = true; + (self.on_capability_exchanged)() + } else { + Ok(Vec::new()) + } + } + } + } +} + +impl_as_any!(UrbdrcControlClient); + +impl DvcClientProcessor for UrbdrcControlClient {} + +pub trait UrbdrcDeviceBackend: Send { + /// Get the USB device information. + fn device_info(&mut self, channel_id: u32) -> PduResult; + /// [Processing a Cancel Request Message][3.3.5.3.1]: + /// + /// The client MUST attempt to stop processing the request identified by the RequestId field in + /// the CANCEL_REQUEST message. If the current request has not been completed it MUST be + /// canceled. If the request has been completed, the client MUST ignore this CANCEL_REQUEST + /// message. + /// + /// [3.3.5.3.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/d5315234-d9ba-42dc-bc1b-b421c57a21ae + fn cancel_request(&mut self, request_id: RequestId, channel_id: u32); + /// [Processing a Query Device Text Message][3.3.5.3.5]: + /// + /// After receiving the QUERY_DEVICE_TEXT message, the client forwards the request to the + /// physical device. When the physical device completes the request, the client sends the result + /// of the request to the server via QUERY_DEVICE_TEXT_RSP message and the RequestId field in + /// the message MUST match the RequestId in the QUERY_DEVICE_TEXT message. + /// + /// [3.3.5.3.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/834f56cc-cfed-4649-8952-0b6486638c28 + fn query_device_text(&mut self, channel_id: u32, text_type: u32, locale_id: u32) -> PduResult>; + /// Process an [`IoControl`] request. + /// + /// Returning [`None`] means the request remains pending and no immediate completion is sent. + fn io_control( + &mut self, + channel_id: u32, + request_id: RequestId, + request: IoControl, + ) -> PduResult>; + /// Process an [`InternalIoControl`] request. + /// + /// Returning [`None`] means the request remains pending and no immediate completion is sent. + fn internal_io_control( + &mut self, + channel_id: u32, + request_id: RequestId, + request: InternalIoControl, + ) -> PduResult>; + /// Process a [`TransferInRequest`]. + /// + /// Returning [`None`] means the request remains pending and no immediate completion is sent. + fn transfer_in( + &mut self, + channel_id: u32, + request_id: RequestId, + request: TransferInRequest, + ) -> PduResult>; + /// Process a [`TransferOutRequest`]. + /// + /// Returning [`None`] means the request remains pending and no immediate completion is sent. + fn transfer_out( + &mut self, + channel_id: u32, + request_id: RequestId, + request: TransferOutRequest, + ) -> PduResult>; + /// [Processing a Retract Device Message][3.3.5.3.8]: + /// + /// After receiving the RETRACT_DEVICE message, the client SHOULD terminate the dynamic channel + /// and stop redirecting the physical USB device. + /// + /// [3.3.5.3.8]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/77dc8e12-ddd6-4cb8-a3cc-247aacea7d6f + fn retract(&mut self, channel_id: u32) -> PduResult<()>; +} + +#[derive(Debug, Clone)] +pub struct DeviceText { + pub hresult: u32, + pub description: String, +} + +#[derive(Debug, Clone)] +pub struct IoControlResponse { + pub hresult: HResult, + pub information: u32, + pub output_buffer: Vec, +} + +#[derive(Debug, Clone)] +pub struct UrbInResponse { + pub ts_urb_result: TsUrbResult, + pub hresult: HResult, + pub output_buffer: Vec, +} + +#[derive(Debug, Clone)] +pub struct UrbOutResponse { + pub ts_urb_result: TsUrbResult, + pub hresult: HResult, + pub output_buffer_size: u32, +} + +/// A client for the URBDRC Device Virtual Channel. +pub struct UrbdrcDeviceClient { + /// Indicates whether the channel is ready for handling IO request. + ready_for_io: bool, + /// Per-device USB interface ID allocated by the DVC layer. This is intentionally kept out of + /// `DeviceInfo`, which only describes backend USB facts. + udev_iface: InterfaceId, + request_completion: Option, + backend: Box, + pending_io: BTreeMap, +} + +impl UrbdrcDeviceClient { + pub fn new(udev_iface: InterfaceId, backend: Box) -> Self { + Self { + ready_for_io: false, + udev_iface, + request_completion: None, + backend, + pending_io: BTreeMap::new(), + } + } + + pub const fn ready_for_io(&self) -> bool { + self.ready_for_io + } + + pub const fn udev_iface(&self) -> InterfaceId { + self.udev_iface + } + + pub fn io_ctl_completion(&mut self, request_id: RequestId, response: IoControlResponse) -> PduResult { + let Some(completion_iface) = self.request_completion else { + return Err(pdu_other_err!("request completion uninitialized")); + }; + let Entry::Occupied(entry) = self.pending_io.entry(request_id) else { + return Err(pdu_other_err!("completion mismatch")); + }; + let (msg_id, max_output_buf_size) = match entry.get() { + Pending::IoCtl { + msg_id, + max_output_buf_size, + } => (*msg_id, *max_output_buf_size), + _ => return Err(pdu_other_err!("completion mismatch")), + }; + entry.remove(); + + let output_buffer_size = + u32::try_from(response.output_buffer.len()).map_err(|_| pdu_other_err!("convert usize to u32 failed"))?; + if output_buffer_size > max_output_buf_size { + return Err(pdu_other_err!("output buffer exceeds maximum amount")); + } + Ok(Box::new(IoControlCompletion { + msg_id, + completion_iface, + hresult: response.hresult, + request_id, + information: response.information, + output_buffer_size, + output_buffer: response.output_buffer, + })) + } + + pub fn internal_io_ctl_completion( + &mut self, + request_id: RequestId, + response: IoControlResponse, + ) -> PduResult { + let Some(completion_iface) = self.request_completion else { + return Err(pdu_other_err!("request completion uninitialized")); + }; + let Entry::Occupied(entry) = self.pending_io.entry(request_id) else { + return Err(pdu_other_err!("completion mismatch")); + }; + let (msg_id, max_output_buf_size) = match entry.get() { + Pending::InternalIoCtl { + msg_id, + max_output_buf_size, + } => (*msg_id, *max_output_buf_size), + _ => return Err(pdu_other_err!("completion mismatch")), + }; + entry.remove(); + + let output_buffer_size = + u32::try_from(response.output_buffer.len()).map_err(|_| pdu_other_err!("convert usize to u32 failed"))?; + if output_buffer_size > max_output_buf_size { + return Err(pdu_other_err!("output buffer exceeds maximum amount")); + } + Ok(Box::new(IoControlCompletion { + msg_id, + completion_iface, + hresult: response.hresult, + request_id, + information: response.information, + output_buffer_size, + output_buffer: response.output_buffer, + })) + } + + pub fn transfer_in_completion(&mut self, request_id: RequestId, response: UrbInResponse) -> PduResult { + let Some(completion_iface) = self.request_completion else { + return Err(pdu_other_err!("request completion uninitialized")); + }; + let Entry::Occupied(entry) = self.pending_io.entry(request_id) else { + return Err(pdu_other_err!("completion mismatch")); + }; + let (msg_id, max_output_buf_size) = match entry.get() { + Pending::TransferIn { + msg_id, + max_output_buf_size, + } => (*msg_id, *max_output_buf_size), + _ => return Err(pdu_other_err!("completion mismatch")), + }; + entry.remove(); + let output_buffer_size = + u32::try_from(response.output_buffer.len()).map_err(|_| pdu_other_err!("convert usize to u32 failed"))?; + if output_buffer_size > max_output_buf_size { + return Err(pdu_other_err!("output buffer exceeds maximum amount")); + } + let request_id = + RequestIdTransferInOut::try_from(request_id).map_err(|_| pdu_other_err!("invalid transfer request id"))?; + if response.output_buffer.is_empty() { + Ok(Box::new(UrbCompletionNoData { + msg_id, + completion_iface, + req_id: request_id, + ts_urb_result: response.ts_urb_result, + hresult: response.hresult, + output_buffer_size, + })) + } else { + Ok(Box::new(UrbCompletion { + msg_id, + completion_iface, + req_id: request_id, + ts_urb_result: response.ts_urb_result, + hresult: response.hresult, + output_buffer: response.output_buffer, + })) + } + } + + pub fn transfer_out_completion( + &mut self, + request_id: RequestId, + response: UrbOutResponse, + ) -> PduResult { + let Some(completion_iface) = self.request_completion else { + return Err(pdu_other_err!("request completion uninitialized")); + }; + let Entry::Occupied(entry) = self.pending_io.entry(request_id) else { + return Err(pdu_other_err!("completion mismatch")); + }; + let (msg_id, transfer_request_id, max_output_buf_size) = match entry.get() { + Pending::TransferOut { + msg_id, + request_id, + max_output_buf_size, + } => (*msg_id, *request_id, *max_output_buf_size), + _ => return Err(pdu_other_err!("completion mismatch")), + }; + entry.remove(); + if response.output_buffer_size > max_output_buf_size { + return Err(pdu_other_err!("output buffer exceeds maximum amount")); + } + Ok(Box::new(UrbCompletionNoData { + msg_id, + completion_iface, + req_id: transfer_request_id, + ts_urb_result: response.ts_urb_result, + hresult: response.hresult, + output_buffer_size: response.output_buffer_size, + })) + } +} + +impl DvcProcessor for UrbdrcDeviceClient { + fn channel_name(&self) -> &str { + CHANNEL_NAME + } + + fn start(&mut self, _channel_id: u32) -> PduResult> { + Ok(Vec::new()) + } + + fn process(&mut self, channel_id: u32, payload: &[u8]) -> PduResult> { + let pdu = UrbdrcServerDevicePdu::decode(&mut ReadCursor::new(payload)).map_err(|e| decode_err!(e))?; + + use UrbdrcServerDevicePdu::*; + match pdu { + ChanCreated(chan_created_pdu) => Ok(vec![Box::new(ChannelCreated { + msg_id: chan_created_pdu.msg_id, + direction: crate::pdu::notify::Direction::ToServer, + })]), + QueryIfaceReq(query_face_pdu) => Ok(vec![Box::new(QueryInterfaceFailureResponse { + iface_id: query_face_pdu.iface_id, + msg_id: query_face_pdu.msg_id, + })]), + IfaceRelease(iface_release_pdu) => { + if iface_release_pdu.iface_id == InterfaceId::NOTIFY_CLIENT.with_mask(Mask::Proxy) && !self.ready_for_io + { + // NOTE: MS-RDPEUSB does not normatively define RIMCALL_RELEASE as a + // server-ready-proceed barrier; the semantic comes from observed Windows + // urbdrc-server behavior. Pattern matches FreeRDP urbdrc_main.c since 2012 + // (commit fa4d8fca1be, Atrust contribution). Two sync points: control DVC + // (server -> client ADD_VIRTUAL_CHANNEL); device DVC (server -> client + // ADD_DEVICE). + let device_info = self.backend.device_info(channel_id)?; + let add_device = add_device_from_info(self.udev_iface, &device_info)?; + self.ready_for_io = true; + + Ok(vec![Box::new(add_device)]) + } else { + Ok(Vec::new()) + } + } + // SPEC [3.1.5]: Out-of-sequence packets are packets that do not adhere to the rules in + // sections 3.2.5 and 3.3.5. Malformed and out-of-sequence packets MUST be ignored by + // the server and the client. + // + // [3.1.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeusb/f31cc9ef-a8c3-4a4d-b64d-f027ed0752b0 + CancelReq(cancel_req_pdu) => { + if !self.ready_for_io || cancel_req_pdu.udev_iface != self.udev_iface { + return Ok(Vec::new()); + } + if self.pending_io.remove(&cancel_req_pdu.req_id).is_some() { + self.backend.cancel_request(cancel_req_pdu.req_id, channel_id); + } + Ok(Vec::new()) + } + RegReqCb(register_request_callback_pdu) => { + if !self.ready_for_io || register_request_callback_pdu.udev_iface != self.udev_iface { + return Ok(Vec::new()); + } + self.request_completion = register_request_callback_pdu.request_completion; + Ok(Vec::new()) + } + Retract(retract_pdu) => { + if !self.ready_for_io || retract_pdu.udev_iface != self.udev_iface { + return Ok(Vec::new()); + } + self.backend.retract(channel_id)?; + self.ready_for_io = false; + self.request_completion = None; + self.pending_io.clear(); + Ok(Vec::new()) + } + DevText(dev_text_pdu) => { + if !self.ready_for_io || dev_text_pdu.udev_iface != self.udev_iface { + return Ok(Vec::new()); + } + if let Some(device_text) = + self.backend + .query_device_text(channel_id, dev_text_pdu.text_type, dev_text_pdu.locale_id)? + { + Ok(vec![Box::new(QueryDeviceTextRsp { + msg_id: dev_text_pdu.msg_id, + udev_iface: dev_text_pdu.udev_iface, + hresult: device_text.hresult, + device_description: device_text.description.into(), + })]) + } else { + Ok(Vec::new()) + } + } + IoCtl(io_ctl_pdu) => { + if !self.ready_for_io || io_ctl_pdu.udev_iface != self.udev_iface { + return Ok(Vec::new()); + } + let msg_id = io_ctl_pdu.msg_id; + let request_id = io_ctl_pdu.req_id; + let max_output_buf_size = io_ctl_pdu.output_buffer_size; + if self.pending_io.contains_key(&request_id) { + return Ok(Vec::new()); + } + let Some(completion_iface) = self.request_completion else { + return Ok(Vec::new()); + }; + if let Some(io_ctl_response) = self.backend.io_control(channel_id, request_id, io_ctl_pdu)? { + let output_buffer_size = u32::try_from(io_ctl_response.output_buffer.len()) + .map_err(|_| pdu_other_err!("convert usize to u32 failed"))?; + if output_buffer_size > max_output_buf_size { + return Err(pdu_other_err!("output buffer exceeds maximum amount")); + } + Ok(vec![Box::new(IoControlCompletion { + msg_id, + completion_iface, + hresult: io_ctl_response.hresult, + request_id, + information: io_ctl_response.information, + output_buffer_size, + output_buffer: io_ctl_response.output_buffer, + })]) + } else { + self.pending_io.insert( + request_id, + Pending::IoCtl { + msg_id, + max_output_buf_size, + }, + ); + Ok(Vec::new()) + } + } + InternalIoCtl(internal_io_ctl_pdu) => { + if !self.ready_for_io || internal_io_ctl_pdu.udev_iface != self.udev_iface { + return Ok(Vec::new()); + } + let msg_id = internal_io_ctl_pdu.msg_id; + let request_id = internal_io_ctl_pdu.req_id; + let max_output_buf_size = internal_io_ctl_pdu.output_buffer_size; + if self.pending_io.contains_key(&request_id) { + return Ok(Vec::new()); + } + let Some(completion_iface) = self.request_completion else { + return Ok(Vec::new()); + }; + if let Some(internal_io_ctl_response) = + self.backend + .internal_io_control(channel_id, request_id, internal_io_ctl_pdu)? + { + let output_buffer_size = u32::try_from(internal_io_ctl_response.output_buffer.len()) + .map_err(|_| pdu_other_err!("convert usize to u32 failed"))?; + if output_buffer_size > max_output_buf_size { + return Err(pdu_other_err!("output buffer exceeds maximum amount")); + } + Ok(vec![Box::new(IoControlCompletion { + msg_id, + completion_iface, + hresult: internal_io_ctl_response.hresult, + request_id, + information: internal_io_ctl_response.information, + output_buffer_size, + output_buffer: internal_io_ctl_response.output_buffer, + })]) + } else { + self.pending_io.insert( + request_id, + Pending::InternalIoCtl { + msg_id, + max_output_buf_size, + }, + ); + Ok(Vec::new()) + } + } + TransferIn(transfer_in_pdu) => { + if !self.ready_for_io || transfer_in_pdu.udev_iface != self.udev_iface { + return Ok(Vec::new()); + } + let msg_id = transfer_in_pdu.msg_id; + let max_output_buf_size = transfer_in_pdu.output_buffer_size; + let request_id = transfer_in_pdu.request_id(); + if self.pending_io.contains_key(&request_id.into()) { + return Ok(Vec::new()); + } + let Some(completion_iface) = self.request_completion else { + return Ok(Vec::new()); + }; + if let Some(urb_response) = self + .backend + .transfer_in(channel_id, request_id.into(), transfer_in_pdu)? + { + let output_buffer_size = u32::try_from(urb_response.output_buffer.len()) + .map_err(|_| pdu_other_err!("convert usize to u32 failed"))?; + if output_buffer_size > max_output_buf_size { + return Err(pdu_other_err!("output buffer exceeds maximum amount")); + } + if urb_response.output_buffer.is_empty() { + Ok(vec![Box::new(UrbCompletionNoData { + msg_id, + completion_iface, + req_id: request_id, + ts_urb_result: urb_response.ts_urb_result, + hresult: urb_response.hresult, + output_buffer_size, + })]) + } else { + Ok(vec![Box::new(UrbCompletion { + msg_id, + completion_iface, + req_id: request_id, + ts_urb_result: urb_response.ts_urb_result, + hresult: urb_response.hresult, + output_buffer: urb_response.output_buffer, + })]) + } + } else { + self.pending_io.insert( + request_id.into(), + Pending::TransferIn { + msg_id, + max_output_buf_size, + }, + ); + Ok(Vec::new()) + } + } + TransferOut(transfer_out_pdu) => { + if !self.ready_for_io || transfer_out_pdu.udev_iface != self.udev_iface { + return Ok(Vec::new()); + } + let msg_id = transfer_out_pdu.msg_id; + let output_buffer_size = u32::try_from(transfer_out_pdu.output_buffer.len()) + .map_err(|_| pdu_other_err!("convert usize to u32 failed"))?; + let (request_id, no_ack) = match &transfer_out_pdu.ts_urb { + TsUrbOut::CtlTransfer(urb) => (urb.header.req_id, urb.header.no_ack), + TsUrbOut::BulkInterruptTransfer(urb) => (urb.header.req_id, urb.header.no_ack), + TsUrbOut::IsochTransfer(urb) => (urb.header.req_id, urb.header.no_ack), + TsUrbOut::CtlDescReq(urb) => (urb.header.req_id, urb.header.no_ack), + TsUrbOut::VendorClassReq(urb) => (urb.header.req_id, urb.header.no_ack), + TsUrbOut::CtlTransferEx(urb) => (urb.header.req_id, urb.header.no_ack), + }; + if self.pending_io.contains_key(&request_id.into()) { + return Ok(Vec::new()); + } + + if no_ack { + self.backend + .transfer_out(channel_id, request_id.into(), transfer_out_pdu)?; + Ok(Vec::new()) + } else { + let Some(completion_iface) = self.request_completion else { + return Ok(Vec::new()); + }; + if let Some(urb_response) = + self.backend + .transfer_out(channel_id, request_id.into(), transfer_out_pdu)? + { + if urb_response.output_buffer_size > output_buffer_size { + return Err(pdu_other_err!("output buffer exceeds maximum amount")); + } + Ok(vec![Box::new(UrbCompletionNoData { + msg_id, + completion_iface, + req_id: request_id, + ts_urb_result: urb_response.ts_urb_result, + hresult: urb_response.hresult, + output_buffer_size: urb_response.output_buffer_size, + })]) + } else { + self.pending_io.insert( + request_id.into(), + Pending::TransferOut { + msg_id, + request_id, + max_output_buf_size: output_buffer_size, + }, + ); + Ok(Vec::new()) + } + } + } + } + } +} + +impl_as_any!(UrbdrcDeviceClient); + +impl DvcClientProcessor for UrbdrcDeviceClient {} + +enum Pending { + IoCtl { + msg_id: MessageId, + max_output_buf_size: u32, + }, + InternalIoCtl { + msg_id: MessageId, + max_output_buf_size: u32, + }, + TransferIn { + msg_id: MessageId, + max_output_buf_size: u32, + }, + TransferOut { + msg_id: MessageId, + request_id: RequestIdTransferInOut, + max_output_buf_size: u32, + }, +} diff --git a/crates/ironrdp-rdpeusb/src/lib.rs b/crates/ironrdp-rdpeusb/src/lib.rs index d1ce420ea..c1a57ff9b 100644 --- a/crates/ironrdp-rdpeusb/src/lib.rs +++ b/crates/ironrdp-rdpeusb/src/lib.rs @@ -3,4 +3,7 @@ extern crate alloc; +pub const CHANNEL_NAME: &str = "URBDRC"; + +pub mod client; pub mod pdu; diff --git a/crates/ironrdp-rdpeusb/src/pdu/caps.rs b/crates/ironrdp-rdpeusb/src/pdu/caps.rs index b61f5d2c6..5cc00a15e 100644 --- a/crates/ironrdp-rdpeusb/src/pdu/caps.rs +++ b/crates/ironrdp-rdpeusb/src/pdu/caps.rs @@ -7,6 +7,7 @@ use ironrdp_core::{ DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_fixed_part_size, ensure_size, invalid_field_err, }; +use ironrdp_dvc::DvcEncode; use crate::pdu::header::{FunctionId, InterfaceId, Mask, MessageId, SharedMsgHeader}; use crate::pdu::utils::HResult; @@ -151,3 +152,6 @@ impl Encode for RimExchangeCapabilityResponse { Self::FIXED_PART_SIZE } } + +impl DvcEncode for RimExchangeCapabilityRequest {} +impl DvcEncode for RimExchangeCapabilityResponse {} diff --git a/crates/ironrdp-rdpeusb/src/pdu/completion/mod.rs b/crates/ironrdp-rdpeusb/src/pdu/completion/mod.rs index c9139b1b9..6addf9fcc 100644 --- a/crates/ironrdp-rdpeusb/src/pdu/completion/mod.rs +++ b/crates/ironrdp-rdpeusb/src/pdu/completion/mod.rs @@ -11,6 +11,7 @@ use alloc::vec::Vec; use ironrdp_core::{ Decode as _, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_size, invalid_field_err, other_err, }; +use ironrdp_dvc::DvcEncode; use ironrdp_pdu::utils::strict_sum; use crate::pdu::completion::ts_urb_result::{TsUrbIsochTransferResult, TsUrbResult, TsUrbResultPayload}; @@ -163,6 +164,8 @@ impl Encode for IoControlCompletion { } } +impl DvcEncode for IoControlCompletion {} + /// [\[MS-RDPEUSB\] 2.2.7.2 URB Completion (URB_COMPLETION)][1] packet. /// /// Sent from the client to the server as the final result of a [`TransferInRequest`] that contains @@ -265,6 +268,8 @@ impl Encode for UrbCompletion { } } +impl DvcEncode for UrbCompletion {} + /// [\[MS-RDPEUSB\] 2.2.7.3 URB Completion No Data (URB_COMPLETION_NO_DATA)][1] packet. /// /// Sent from the client to the server as the final result of a [`TransferInRequest`] that contains @@ -343,3 +348,5 @@ impl Encode for UrbCompletionNoData { + size_of::(/* OutputBufferSize */) } } + +impl DvcEncode for UrbCompletionNoData {} diff --git a/crates/ironrdp-rdpeusb/src/pdu/header.rs b/crates/ironrdp-rdpeusb/src/pdu/header.rs index 38174fbb9..0f54711b9 100644 --- a/crates/ironrdp-rdpeusb/src/pdu/header.rs +++ b/crates/ironrdp-rdpeusb/src/pdu/header.rs @@ -64,7 +64,7 @@ impl TryFrom for Mask { /// Max value for interface ID's: `0x3F_FF_FF_FF` (30 bits). #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct InterfaceId(pub(in crate::pdu) u32); +pub struct InterfaceId(pub(crate) u32); impl InterfaceId { pub const FIXED_PART_SIZE: usize = size_of::(); diff --git a/crates/ironrdp-rdpeusb/src/pdu/iface_manipulation.rs b/crates/ironrdp-rdpeusb/src/pdu/iface_manipulation.rs index 4624c1241..19ea06d08 100644 --- a/crates/ironrdp-rdpeusb/src/pdu/iface_manipulation.rs +++ b/crates/ironrdp-rdpeusb/src/pdu/iface_manipulation.rs @@ -7,6 +7,7 @@ //! [2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpexps/ebe401f0-f22e-4de4-9cd3-2a55e5493500 use ironrdp_core::{Decode, Encode, ensure_fixed_part_size, ensure_size, invalid_field_err}; +use ironrdp_dvc::DvcEncode; use crate::pdu::header::{FunctionId, MessageId, SharedMsgHeader}; @@ -177,3 +178,6 @@ impl Encode for QueryInterfaceFailureResponse { Self::FIXED_PART_SIZE } } + +impl DvcEncode for QueryInterfaceRequest {} +impl DvcEncode for QueryInterfaceFailureResponse {} diff --git a/crates/ironrdp-rdpeusb/src/pdu/notify.rs b/crates/ironrdp-rdpeusb/src/pdu/notify.rs index a01fae3de..2399dfd79 100644 --- a/crates/ironrdp-rdpeusb/src/pdu/notify.rs +++ b/crates/ironrdp-rdpeusb/src/pdu/notify.rs @@ -12,6 +12,7 @@ use ironrdp_core::{ DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_fixed_part_size, ensure_size, unsupported_value_err, }; +use ironrdp_dvc::DvcEncode; use crate::pdu::header::{FunctionId, InterfaceId, Mask, MessageId, SharedMsgHeader, unpack}; @@ -113,3 +114,5 @@ impl Encode for ChannelCreated { Self::FIXED_PART_SIZE } } + +impl DvcEncode for ChannelCreated {} diff --git a/crates/ironrdp-rdpeusb/src/pdu/sink.rs b/crates/ironrdp-rdpeusb/src/pdu/sink.rs index e28148dea..b6cc66d50 100644 --- a/crates/ironrdp-rdpeusb/src/pdu/sink.rs +++ b/crates/ironrdp-rdpeusb/src/pdu/sink.rs @@ -11,6 +11,7 @@ use ironrdp_core::{ Decode, DecodeOwned as _, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_fixed_part_size, ensure_size, invalid_field_err, unsupported_value_err, }; +use ironrdp_dvc::DvcEncode; use ironrdp_pdu::utils::strict_sum; use ironrdp_str::multi_sz::MultiSzString; use ironrdp_str::prefixed::Cch32String; @@ -58,6 +59,8 @@ impl Encode for AddVirtualChannel { } } +impl DvcEncode for AddVirtualChannel {} + /// [\[MS-RDPEUSB\] 2.2.4.2 Add Device Message (ADD_DEVICE)][1] packet. /// /// Sent from the client to the server in order to create a redirected USB device on the server. @@ -187,6 +190,8 @@ impl Encode for AddDevice { } } +impl DvcEncode for AddDevice {} + /// [\[MS-RDPEUSB\] 2.2.11 USB_DEVICE_CAPABILITIES][1] packet. /// /// Defines the capabilities of a USB device. diff --git a/crates/ironrdp-rdpeusb/src/pdu/usb_dev/mod.rs b/crates/ironrdp-rdpeusb/src/pdu/usb_dev/mod.rs index 7fa1ec056..11f09607f 100644 --- a/crates/ironrdp-rdpeusb/src/pdu/usb_dev/mod.rs +++ b/crates/ironrdp-rdpeusb/src/pdu/usb_dev/mod.rs @@ -11,11 +11,12 @@ use ironrdp_core::{ Decode as _, DecodeOwned as _, DecodeResult, Encode, EncodeResult, ReadCursor, WriteCursor, ensure_fixed_part_size, ensure_size, invalid_field_err, other_err, unsupported_value_err, }; +use ironrdp_dvc::DvcEncode; use ironrdp_str::prefixed::Cch32String; use crate::pdu::header::{FunctionId, InterfaceId, Mask, MessageId, SharedMsgHeader}; use crate::pdu::usb_dev::ts_urb::{TsUrbIn, TsUrbOut}; -use crate::pdu::utils::{HResult, RequestId, RequestIdIoctl}; +use crate::pdu::utils::{HResult, RequestId, RequestIdIoctl, RequestIdTransferInOut}; #[cfg(doc)] use crate::pdu::{ completion::{IoControlCompletion, UrbCompletion, UrbCompletionNoData}, @@ -79,6 +80,8 @@ impl Encode for CancelRequest { } } +impl DvcEncode for CancelRequest {} + /// [\[MS-RDPEUSB\] 2.2.6.2 Register Request Callback Message (REGISTER_REQUEST_CALLBACK)][1] message. /// /// Sent from the server to the client in order to provide an interface ID for Request Completion @@ -152,6 +155,8 @@ impl Encode for RegisterRequestCallback { } } +impl DvcEncode for RegisterRequestCallback {} + /// [\[MS-RDPEUSB\] 2.2.6.3 IO Control Message (IO_CONTROL)][1] message. /// /// Sent from the server to the client to submit an IO control request to the USB device. @@ -490,6 +495,8 @@ impl Encode for InternalIoControl { } } +impl DvcEncode for InternalIoControl {} + /// [\[MS-RDPEUSB\] 2.2.6.5 Query Device Text Message (QUERY_DEVICE_TEXT)][1] message. /// /// Sent from the server to the client in order to query the USB's device text (like description or @@ -556,6 +563,8 @@ impl Encode for QueryDeviceText { } } +impl DvcEncode for QueryDeviceText {} + /// [\[MS-RDPEUSB\] 2.2.6.6 Query Device Text Response Message (QUERY_DEVICE_TEXT_RSP)][1] message. /// /// Sent from the client in response to a [`QueryDeviceText`] message sent by the server. @@ -617,23 +626,7 @@ impl Encode for QueryDeviceTextRsp { } } -// macro_rules! check_output_buffer_size { -// ($ts_urb:expr, $output_buffer_size:expr) => {{ -// }}; -// } - -// #[derive(Debug)] -// pub struct TransferInRequestOutputBufferSizeErr { -// is: u32, -// expected: u32, -// ts_urb: &'static str, -// } -// -// impl core::fmt::Display for TransferInRequestOutputBufferSizeErr { -// fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { -// write!(f, "") -// } -// } +impl DvcEncode for QueryDeviceTextRsp {} /// [\[MS-RDPEUSB\] 2.2.6.7 Transfer In Request (TRANSFER_IN_REQUEST)][1] message. /// @@ -658,6 +651,26 @@ impl TransferInRequest { } } + pub fn request_id(&self) -> RequestIdTransferInOut { + match &self.ts_urb { + TsUrbIn::SelectConfig(urb) => urb.header.req_id, + TsUrbIn::SelectIface(urb) => urb.header.req_id, + TsUrbIn::PipeReq(urb) => urb.header.req_id, + TsUrbIn::GetCurFrameNum(urb) => urb.header.req_id, + TsUrbIn::CtlTransfer(urb) => urb.header.req_id, + TsUrbIn::BulkInterruptTransfer(urb) => urb.header.req_id, + TsUrbIn::IsochTransfer(urb) => urb.header.req_id, + TsUrbIn::CtlDescReq(urb) => urb.header.req_id, + TsUrbIn::CtlFeatReq(urb) => urb.header.req_id, + TsUrbIn::CtlGetStatus(urb) => urb.header.req_id, + TsUrbIn::VendorClassReq(urb) => urb.header.req_id, + TsUrbIn::CtlGetConfig(urb) => urb.header.req_id, + TsUrbIn::CtlGetIface(urb) => urb.header.req_id, + TsUrbIn::OsFeatDescReq(urb) => urb.header.req_id, + TsUrbIn::CtlTransferEx(urb) => urb.header.req_id, + } + } + pub fn check_output_buffer_size(&self) -> Result<(), &'static str> { use TsUrbIn::*; @@ -745,6 +758,8 @@ impl Encode for TransferInRequest { } } +impl DvcEncode for TransferInRequest {} + /// [\[MS-RDPEUSB\] 2.2.6.8 Transfer Out Request (TRANSFER_OUT_REQUEST)][1] message. /// /// Sent from the server to the client in order to submit data to the USB device. @@ -822,6 +837,8 @@ impl Encode for TransferOutRequest { } } +impl DvcEncode for TransferOutRequest {} + /// [\[MS-RDPEUSB\] 2.2.6.9 Retract Device (RETRACT_DEVICE)][1] message. /// /// Sent from the server to the client in order to stop redirecting the USB device. @@ -895,3 +912,5 @@ pub enum UsbRetractReason { /// server's (group) policy. BlockedByPolicy = 0x1, } + +impl DvcEncode for RetractDevice {}