diff --git a/Cargo.lock b/Cargo.lock index 1571e7fbd..b0832f4fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,6 +1702,7 @@ dependencies = [ "ironrdp-cliprdr", "ironrdp-cliprdr-native", "ironrdp-connector", + "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-graphics", "ironrdp-input", @@ -1990,6 +1991,7 @@ name = "ironrdp-session" version = "0.1.0" dependencies = [ "ironrdp-connector", + "ironrdp-displaycontrol", "ironrdp-dvc", "ironrdp-error", "ironrdp-graphics", diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index 1ee7e436e..d611a6e70 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -28,7 +28,7 @@ native-tls = ["ironrdp-tls/native-tls"] [dependencies] # Protocols -ironrdp = { workspace = true, features = ["input", "graphics", "dvc", "rdpdr", "rdpsnd", "cliprdr"] } +ironrdp = { workspace = true, features = ["input", "graphics", "dvc", "svc", "rdpdr", "rdpsnd", "cliprdr", "displaycontrol"] } ironrdp-cliprdr-native.workspace = true ironrdp-tls.workspace = true ironrdp-tokio.workspace = true diff --git a/crates/ironrdp-client/src/config.rs b/crates/ironrdp-client/src/config.rs index fe5c5dec4..e31cb21ce 100644 --- a/crates/ironrdp-client/src/config.rs +++ b/crates/ironrdp-client/src/config.rs @@ -296,6 +296,7 @@ impl Config { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, }, + desktop_scale_factor: 0, // Default to 0 per FreeRDP bitmap, client_build: semver::Version::parse(env!("CARGO_PKG_VERSION")) .map(|version| version.major * 100 + version.minor * 10 + version.patch) diff --git a/crates/ironrdp-client/src/gui.rs b/crates/ironrdp-client/src/gui.rs index f8e0f5f51..4b4b7c680 100644 --- a/crates/ironrdp-client/src/gui.rs +++ b/crates/ironrdp-client/src/gui.rs @@ -63,9 +63,17 @@ impl GuiContext { match event { Event::WindowEvent { window_id, event } if window_id == window.id() => match event { WindowEvent::Resized(size) => { + let scale_factor = (window.scale_factor() * 100.0) as u32; + // TODO: it should be possible to get the physical size here, however winit doesn't make it straightforward. + // FreeRDP does it based on DPI reading grabbed via [`SDL_GetDisplayDPI`](https://wiki.libsdl.org/SDL2/SDL_GetDisplayDPI): + // https://github.com/FreeRDP/FreeRDP/blob/ba8cf8cf2158018fb7abbedb51ab245f369be813/client/SDL/sdl_monitor.cpp#L250-L262 + let (physical_width, physical_height) = (0, 0); let _ = input_event_sender.send(RdpInputEvent::Resize { width: u16::try_from(size.width).unwrap(), height: u16::try_from(size.height).unwrap(), + scale_factor, + physical_width, + physical_height, }); } WindowEvent::CloseRequested => { @@ -225,6 +233,8 @@ impl GuiContext { // TODO: is there something we should handle here? } Event::UserEvent(RdpOutputEvent::Image { buffer, width, height }) => { + trace!(width = ?width, height = ?height, "Received image with size"); + trace!(window_physical_size = ?window.inner_size(), "Drawing image to the window with size"); surface .resize( NonZeroU32::new(u32::from(width)).unwrap(), diff --git a/crates/ironrdp-client/src/main.rs b/crates/ironrdp-client/src/main.rs index 7cd5cecb9..63ffee0ca 100644 --- a/crates/ironrdp-client/src/main.rs +++ b/crates/ironrdp-client/src/main.rs @@ -19,6 +19,7 @@ fn main() -> anyhow::Result<()> { debug!("GUI context initialized"); let window_size = gui.window().inner_size(); + config.connector.desktop_scale_factor = 0; // TODO: should this be `(gui.window().scale_factor() * 100.0) as u32`? config.connector.desktop_size.width = u16::try_from(window_size.width).unwrap(); config.connector.desktop_size.height = u16::try_from(window_size.height).unwrap(); diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index 4e9c32951..50c3b82fe 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -1,6 +1,8 @@ use ironrdp::cliprdr::backend::{ClipboardMessage, CliprdrBackendFactory}; use ironrdp::connector::connection_activation::ConnectionActivationState; use ironrdp::connector::{ConnectionResult, ConnectorResult}; +use ironrdp::displaycontrol::client::DisplayControlClient; +use ironrdp::displaycontrol::pdu::MonitorLayoutEntry; use ironrdp::graphics::image_processing::PixelFormat; use ironrdp::pdu::input::fast_path::FastPathInputEvent; use ironrdp::pdu::write_buf::WriteBuf; @@ -28,7 +30,13 @@ pub enum RdpOutputEvent { #[derive(Debug)] pub enum RdpInputEvent { - Resize { width: u16, height: u16 }, + Resize { + width: u16, + height: u16, + scale_factor: u32, + physical_width: u32, + physical_height: u32, + }, FastPath(SmallVec<[FastPathInputEvent; 2]>), Close, Clipboard(ClipboardMessage), @@ -107,7 +115,9 @@ async fn connect( let mut connector = connector::ClientConnector::new(config.connector.clone()) .with_server_addr(server_addr) - .with_static_channel(ironrdp::dvc::DrdynvcClient::new()) + .with_static_channel( + ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))), + ) .with_static_channel(rdpsnd::Rdpsnd::new()) .with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0)); @@ -177,24 +187,30 @@ async fn active_session( let input_event = input_event.ok_or_else(|| session::general_err!("GUI is stopped"))?; match input_event { - RdpInputEvent::Resize { mut width, mut height } => { - // TODO(#105): Add support for Display Update Virtual Channel Extension - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/d2954508-f487-48bc-8731-39743e0854a9 - // One approach when this extension is not available is to perform a connection from scratch again. - + RdpInputEvent::Resize { mut width, mut height, .. } => { // Find the last resize event while let Ok(newer_event) = input_event_receiver.try_recv() { - if let RdpInputEvent::Resize { width: newer_width, height: newer_height } = newer_event { + if let RdpInputEvent::Resize { + width: newer_width, + height: newer_height, + .. + } = newer_event { width = newer_width; height = newer_height; } } - // TODO(#271): use the "auto-reconnect cookie": https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/15b0d1c9-2891-4adb-a45e-deb4aeeeab7c - info!(width, height, "resize event"); - - return Ok(RdpControlFlow::ReconnectWithNewSize { width, height }) + let (width, height) = MonitorLayoutEntry::adjust_display_size(width.into(), height.into()); + debug!(width, height, "Adjusted display size"); + + if let Some(response_frame) = active_stage.encode_resize(width, height, None, Some((width, height))) { // Set physical width and height to the same as the pixel width and heighbbt per FreeRDP + vec![ActiveStageOutput::ResponseFrame(response_frame?)] + } else { + // TODO(#271): use the "auto-reconnect cookie": https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/15b0d1c9-2891-4adb-a45e-deb4aeeeab7c + debug!("Reconnecting with new size"); + return Ok(RdpControlFlow::ReconnectWithNewSize { width: width.try_into().unwrap(), height: height.try_into().unwrap() }) + } }, RdpInputEvent::FastPath(events) => { trace!(?events); @@ -307,10 +323,10 @@ async fn active_session( pointer_software_rendering, } = connection_activation.state { - debug!("Deactivation-Reactivation Sequence completed"); + debug!(?desktop_size, "Deactivation-Reactivation Sequence completed"); + // Update image size with the new desktop size. image = DecodedImage::new(PixelFormat::RgbA32, desktop_size.width, desktop_size.height); - // Create a new [`FastPathProcessor`] with potentially updated - // io/user channel ids. + // Update the active stage with the new channel IDs and pointer settings. active_stage.set_fastpath_processor( fast_path::ProcessorBuilder { io_channel_id, @@ -320,6 +336,7 @@ async fn active_session( } .build(), ); + active_stage.set_no_server_pointer(no_server_pointer); break 'activation_seq; } } diff --git a/crates/ironrdp-connector/src/connection.rs b/crates/ironrdp-connector/src/connection.rs index 99c114158..8c11f1c5b 100644 --- a/crates/ironrdp-connector/src/connection.rs +++ b/crates/ironrdp-connector/src/connection.rs @@ -660,11 +660,19 @@ fn create_gcc_blocks<'a>( dig_product_id: Some(config.dig_product_id.clone()), connection_type: Some(ConnectionType::Lan), server_selected_protocol: Some(selected_protocol), - desktop_physical_width: None, - desktop_physical_height: None, - desktop_orientation: None, - desktop_scale_factor: None, - device_scale_factor: None, + desktop_physical_width: Some(0), // 0 per FreeRDP + desktop_physical_height: Some(0), // 0 per FreeRDP + desktop_orientation: if config.desktop_size.width > config.desktop_size.height { + Some(MonitorOrientation::Landscape as u16) + } else { + Some(MonitorOrientation::Portrait as u16) + }, + desktop_scale_factor: Some(config.desktop_scale_factor), + device_scale_factor: if config.desktop_scale_factor >= 100 && config.desktop_scale_factor <= 500 { + Some(100) + } else { + Some(0) + }, }, }, security: ClientSecurityData { @@ -700,7 +708,9 @@ fn create_client_info_pdu(config: &Config, routing_addr: &SocketAddr) -> rdp::Cl }; // Default flags for all sessions - let mut flags = ClientInfoFlags::UNICODE + let mut flags = ClientInfoFlags::MOUSE + | ClientInfoFlags::MOUSE_HAS_WHEEL + | ClientInfoFlags::UNICODE | ClientInfoFlags::DISABLE_CTRL_ALT_DEL | ClientInfoFlags::LOGON_NOTIFY | ClientInfoFlags::LOGON_ERRORS diff --git a/crates/ironrdp-connector/src/connection_activation.rs b/crates/ironrdp-connector/src/connection_activation.rs index 0ad904eaa..708931465 100644 --- a/crates/ironrdp-connector/src/connection_activation.rs +++ b/crates/ironrdp-connector/src/connection_activation.rs @@ -19,7 +19,7 @@ use crate::{legacy, Config, ConnectionFinalizationSequence, ConnectorResult, Des #[derive(Debug, Clone)] pub struct ConnectionActivationSequence { pub state: ConnectionActivationState, - pub config: Config, + config: Config, } impl ConnectionActivationSequence { @@ -127,6 +127,15 @@ impl Sequence for ConnectionActivationSequence { } } + // At this point we have already sent a requested desktop size to the server -- either as a part of the + // [`TS_UD_CS_CORE`] (on initial connection) or the [`DISPLAYCONTROL_MONITOR_LAYOUT`] (on resize event). + // + // The server is therefore responding with a desktop size here, which will be close to the requested size but + // may be slightly different due to server-side constraints. We should use this negotiated size for the rest of + // the session. + // + // [TS_UD_CS_CORE]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/00f1da4a-ee9c-421a-852f-c19f92343d73 + // [DISPLAYCONTROL_MONITOR_LAYOUT]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c let desktop_size = capability_sets .iter() .find_map(|c| match c { @@ -142,7 +151,7 @@ impl Sequence for ConnectionActivationSequence { }); let client_confirm_active = rdp::headers::ShareControlPdu::ClientConfirmActive( - create_client_confirm_active(&self.config, capability_sets), + create_client_confirm_active(&self.config, capability_sets, desktop_size), ); debug!(message = ?client_confirm_active, "Send"); @@ -249,6 +258,7 @@ const DEFAULT_POINTER_CACHE_SIZE: u16 = 32; fn create_client_confirm_active( config: &Config, mut server_capability_sets: Vec, + desktop_size: DesktopSize, ) -> rdp::capability_sets::ClientConfirmActive { use ironrdp_pdu::rdp::capability_sets::*; @@ -276,8 +286,8 @@ fn create_client_confirm_active( }), CapabilitySet::Bitmap(Bitmap { pref_bits_per_pix: 32, - desktop_width: config.desktop_size.width, - desktop_height: config.desktop_size.height, + desktop_width: desktop_size.width, + desktop_height: desktop_size.height, // This is required to be true in order for the Microsoft::Windows::RDS::DisplayControl DVC to work. desktop_resize_flag: true, drawing_flags, @@ -355,7 +365,10 @@ fn create_client_confirm_active( })), }])), CapabilitySet::FrameAcknowledge(FrameAcknowledge { - max_unacknowledged_frame_count: 2, + // FIXME(#447): Revert this to 2 per FreeRDP. + // This is a temporary hack to fix a resize bug, see: + // https://github.com/Devolutions/IronRDP/issues/447 + max_unacknowledged_frame_count: 20, }), ]); @@ -364,7 +377,7 @@ fn create_client_confirm_active( .any(|c| matches!(&c, CapabilitySet::MultiFragmentUpdate(_))) { server_capability_sets.push(CapabilitySet::MultiFragmentUpdate(MultifragmentUpdate { - max_request_size: 1024, + max_request_size: 8 * 1024 * 1024, // 8 MB })); } diff --git a/crates/ironrdp-connector/src/lib.rs b/crates/ironrdp-connector/src/lib.rs index 7ecf2cd4c..724fdf47e 100644 --- a/crates/ironrdp-connector/src/lib.rs +++ b/crates/ironrdp-connector/src/lib.rs @@ -72,6 +72,10 @@ impl Credentials { pub struct Config { /// The initial desktop size to request pub desktop_size: DesktopSize, + /// The initial desktop scale factor to request. + /// + /// This becomes the `desktop_scale_factor` in the [`TS_UD_CS_CORE`](gcc::ClientCoreOptionalData) structure. + pub desktop_scale_factor: u32, /// TLS + Graphical login (legacy) /// /// Also called SSL or TLS security protocol. diff --git a/crates/ironrdp-displaycontrol/src/client.rs b/crates/ironrdp-displaycontrol/src/client.rs index cca27f699..b53885304 100644 --- a/crates/ironrdp-displaycontrol/src/client.rs +++ b/crates/ironrdp-displaycontrol/src/client.rs @@ -37,13 +37,29 @@ impl DisplayControlClient { /// Builds a [`DisplayControlPdu::MonitorLayout`] with a single primary monitor /// with the given `width` and `height`, and wraps it as an [`SvcMessage`]. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent. + /// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm. + /// + /// Use [`crate::pdu::MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function + /// to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c pub fn encode_single_primary_monitor( &self, channel_id: u32, width: u32, height: u32, + scale_factor: Option, + physical_dims: Option<(u32, u32)>, ) -> PduResult> { - let pdu: DisplayControlPdu = DisplayControlMonitorLayout::new_single_primary_monitor(width, height)?.into(); + // TODO: prevent resolution with values greater than max monitor area received in caps. + let pdu: DisplayControlPdu = + DisplayControlMonitorLayout::new_single_primary_monitor(width, height, scale_factor, physical_dims)?.into(); + debug!(?pdu, "Sending monitor layout"); encode_dvc_messages(channel_id, vec![Box::new(pdu)], ChannelFlags::empty()) } } diff --git a/crates/ironrdp-displaycontrol/src/pdu/mod.rs b/crates/ironrdp-displaycontrol/src/pdu/mod.rs index fc394d5fe..6df6aeec8 100644 --- a/crates/ironrdp-displaycontrol/src/pdu/mod.rs +++ b/crates/ironrdp-displaycontrol/src/pdu/mod.rs @@ -236,16 +236,44 @@ impl DisplayControlMonitorLayout { } /// Creates a new [`DisplayControlMonitorLayout`] with a single primary monitor - /// with the given `width` and `height`. - pub fn new_single_primary_monitor(width: u32, height: u32) -> PduResult { - let monitors = vec![ - MonitorLayoutEntry::new_primary(width, height)?.with_orientation(if width > height { - MonitorOrientation::Landscape - } else { - MonitorOrientation::Portrait - }), - ]; - Ok(DisplayControlMonitorLayout::new(&monitors).unwrap()) + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent. + /// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm. + /// + /// Use [`MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function + /// to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn new_single_primary_monitor( + width: u32, + height: u32, + scale_factor: Option, + physical_dims: Option<(u32, u32)>, + ) -> PduResult { + let entry = MonitorLayoutEntry::new_primary(width, height)?.with_orientation(if width > height { + MonitorOrientation::Landscape + } else { + MonitorOrientation::Portrait + }); + + let entry = if let Some(scale_factor) = scale_factor { + entry + .with_desktop_scale_factor(scale_factor)? + .with_device_scale_factor(DeviceScaleFactor::Scale100Percent) + } else { + entry + }; + + let entry = if let Some((physical_width, physical_height)) = physical_dims { + entry.with_physical_dimensions(physical_width, physical_height)? + } else { + entry + }; + + Ok(DisplayControlMonitorLayout::new(&[entry]).unwrap()) } pub fn monitors(&self) -> &[MonitorLayoutEntry] { @@ -350,18 +378,21 @@ impl MonitorLayoutEntry { /// Creates a new [`MonitorLayoutEntry`]. /// - /// - `width` and `height` MUST be >= 200 and <= 8192. - /// - `width` SHOULD be even. If it is odd, it will be adjusted - /// to the nearest even number by subtracting 1. + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c fn new_impl(mut width: u32, height: u32) -> PduResult { if width % 2 != 0 { let prev_width = width; width = width.saturating_sub(1); warn!( - "Monitor width cannot be odd, adjusting from {} to {}", + "Monitor width cannot be odd, adjusting from [{}] to [{}]", prev_width, width ) } + validate_dimensions(width, height)?; Ok(Self { @@ -373,19 +404,63 @@ impl MonitorLayoutEntry { physical_width: 0, physical_height: 0, orientation: 0, - desktop_scale_factor: 100, - device_scale_factor: 100, + desktop_scale_factor: 0, + device_scale_factor: 0, }) } - /// Creates a new primary monitor layout entry. + /// Adjusts the display size to be within the valid range. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// + /// Functions that create [`MonitorLayoutEntry`] should typically use this function to adjust the display size first. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn adjust_display_size(width: u32, height: u32) -> (u32, u32) { + fn constrain(value: u32) -> u32 { + if value < 200 { + 200 + } else if value > 8192 { + 8192 + } else { + value + } + } + + let mut width = width; + if width % 2 != 0 { + width = width.saturating_sub(1); + } + + (constrain(width), constrain(height)) + } + + /// Creates a new primary [`MonitorLayoutEntry`]. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// + /// Use [`MonitorLayoutEntry::adjust_display_size`] before calling this function to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c pub fn new_primary(width: u32, height: u32) -> PduResult { let mut entry = Self::new_impl(width, height)?; entry.is_primary = true; Ok(entry) } - /// Creates a new secondary monitor layout entry. + /// Creates a new primary [`MonitorLayoutEntry`]. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// + /// Use [`MonitorLayoutEntry::adjust_display_size`] before calling this function to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c pub fn new_secondary(width: u32, height: u32) -> PduResult { Self::new_impl(width, height) } @@ -416,7 +491,7 @@ impl MonitorLayoutEntry { self } - /// Sets the monitor's desktop scale factor in percent. (Default is `100`) + /// Sets the monitor's desktop scale factor in percent. /// /// NOTE: As specified in [MS-RDPEDISP], if the desktop scale factor is not in the valid range /// (100..=500 percent), the monitor desktop scale factor is considered invalid and should be ignored. diff --git a/crates/ironrdp-dvc/src/client.rs b/crates/ironrdp-dvc/src/client.rs index 096a4df1f..6f7a13186 100644 --- a/crates/ironrdp-dvc/src/client.rs +++ b/crates/ironrdp-dvc/src/client.rs @@ -2,7 +2,7 @@ use crate::pdu::{ CapabilitiesResponsePdu, CapsVersion, ClosePdu, CreateResponsePdu, CreationStatus, DrdynvcClientPdu, DrdynvcServerPdu, }; -use crate::{encode_dvc_messages, DvcProcessor, DynamicChannelId, DynamicChannelSet}; +use crate::{encode_dvc_messages, DvcProcessor, DynamicChannelSet, DynamicVirtualChannel}; use alloc::vec::Vec; use core::any::TypeId; use core::fmt; @@ -61,17 +61,11 @@ impl DrdynvcClient { self } - pub fn get_dynamic_channel_by_type_id(&self) -> Option<(&T, Option)> + pub fn get_dvc_by_type_id(&self) -> Option<&DynamicVirtualChannel> where T: DvcProcessor, { - self.dynamic_channels - .get_by_type_id(TypeId::of::()) - .and_then(|(channel, channel_id)| { - channel - .channel_processor_downcast_ref() - .map(|channel| (channel as &T, channel_id)) - }) + self.dynamic_channels.get_by_type_id(TypeId::of::()) } fn create_capabilities_response(&mut self) -> SvcMessage { @@ -128,7 +122,7 @@ impl SvcProcessor for DrdynvcClient { self.dynamic_channels .attach_channel_id(channel_name.clone(), channel_id); let dynamic_channel = self.dynamic_channels.get_by_channel_name_mut(&channel_name).unwrap(); - (CreationStatus::OK, dynamic_channel.start(channel_id)?) + (CreationStatus::OK, dynamic_channel.start()?) } else { (CreationStatus::NO_LISTENER, Vec::new()) }; diff --git a/crates/ironrdp-dvc/src/lib.rs b/crates/ironrdp-dvc/src/lib.rs index 8f7600746..730b9ed03 100644 --- a/crates/ironrdp-dvc/src/lib.rs +++ b/crates/ironrdp-dvc/src/lib.rs @@ -101,6 +101,10 @@ pub fn encode_dvc_messages( pub struct DynamicVirtualChannel { channel_processor: Box, complete_data: CompleteData, + /// The channel ID assigned by the server. + /// + /// This field is `None` until the server assigns a channel ID. + channel_id: Option, } impl DynamicVirtualChannel { @@ -108,11 +112,28 @@ impl DynamicVirtualChannel { Self { channel_processor: Box::new(handler), complete_data: CompleteData::new(), + channel_id: None, } } - fn start(&mut self, channel_id: DynamicChannelId) -> PduResult> { - self.channel_processor.start(channel_id) + pub fn is_open(&self) -> bool { + self.channel_id.is_some() + } + + pub fn channel_id(&self) -> Option { + self.channel_id + } + + pub fn channel_processor_downcast_ref(&self) -> Option<&T> { + self.channel_processor.as_any().downcast_ref() + } + + fn start(&mut self) -> PduResult> { + if let Some(channel_id) = self.channel_id { + self.channel_processor.start(channel_id) + } else { + Err(other_err!("DynamicVirtualChannel::start", "channel ID not set")) + } } fn process(&mut self, pdu: DrdynvcDataPdu) -> PduResult> { @@ -128,10 +149,6 @@ impl DynamicVirtualChannel { fn channel_name(&self) -> &str { self.channel_processor.channel_name() } - - fn channel_processor_downcast_ref(&self) -> Option<&T> { - self.channel_processor.as_any().downcast_ref() - } } struct DynamicChannelSet { @@ -160,15 +177,17 @@ impl DynamicChannelSet { fn attach_channel_id(&mut self, name: DynamicChannelName, id: DynamicChannelId) -> Option { self.channel_id_to_name.insert(id, name.clone()); - self.name_to_channel_id.insert(name, id) + self.name_to_channel_id.insert(name.clone(), id); + let dvc = self.get_by_channel_name_mut(&name)?; + let old_id = dvc.channel_id; + dvc.channel_id = Some(id); + old_id } - fn get_by_type_id(&self, type_id: TypeId) -> Option<(&DynamicVirtualChannel, Option)> { - self.type_id_to_name.get(&type_id).and_then(|name| { - self.channels - .get(name) - .map(|channel| (channel, self.name_to_channel_id.get(name).copied())) - }) + fn get_by_type_id(&self, type_id: TypeId) -> Option<&DynamicVirtualChannel> { + self.type_id_to_name + .get(&type_id) + .and_then(|name| self.channels.get(name)) } fn get_by_channel_name(&self, name: &DynamicChannelName) -> Option<&DynamicVirtualChannel> { diff --git a/crates/ironrdp-pdu/src/codecs/rfx/header_messages.rs b/crates/ironrdp-pdu/src/codecs/rfx/header_messages.rs index 67dc717af..98c650f6c 100644 --- a/crates/ironrdp-pdu/src/codecs/rfx/header_messages.rs +++ b/crates/ironrdp-pdu/src/codecs/rfx/header_messages.rs @@ -170,11 +170,8 @@ pub struct RfxChannel { pub struct RfxChannelWidth(i16); impl RfxChannelWidth { - pub fn new(value: i16) -> Result { - (1..=4096) - .contains(&value) - .then_some(Self(value)) - .ok_or(RfxError::InvalidChannelWidth(value)) + pub fn new(value: i16) -> Self { + Self(value) } pub fn as_u16(self) -> u16 { @@ -192,11 +189,8 @@ impl RfxChannelWidth { pub struct RfxChannelHeight(i16); impl RfxChannelHeight { - pub fn new(value: i16) -> Result { - (1..=2048) - .contains(&value) - .then_some(Self(value)) - .ok_or(RfxError::InvalidChannelWidth(value)) + pub fn new(value: i16) -> Self { + Self(value) } pub fn as_u16(self) -> u16 { @@ -218,10 +212,10 @@ impl PduBufferParsing<'_> for RfxChannel { } let width = buffer.read_i16::()?; - let width = RfxChannelWidth::new(width)?; + let width = RfxChannelWidth::new(width); let height = buffer.read_i16::()?; - let height = RfxChannelHeight::new(height)?; + let height = RfxChannelHeight::new(height); Ok(Self { width, height }) } diff --git a/crates/ironrdp-pdu/src/gcc.rs b/crates/ironrdp-pdu/src/gcc.rs index 70a691288..d22ec9067 100644 --- a/crates/ironrdp-pdu/src/gcc.rs +++ b/crates/ironrdp-pdu/src/gcc.rs @@ -51,6 +51,9 @@ macro_rules! user_header_try { const USER_DATA_HEADER_SIZE: usize = 4; +/// 2.2.1.3 Client MCS Connect Initial PDU with GCC Conference Create Request +/// +/// [2.2.1.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/db6713ee-1c0e-4064-a3b3-0fac30b4037b #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientGccBlocks { pub core: ClientCoreData, diff --git a/crates/ironrdp-pdu/src/gcc/core_data/client.rs b/crates/ironrdp-pdu/src/gcc/core_data/client.rs index 52e91d86b..1af6ac05f 100644 --- a/crates/ironrdp-pdu/src/gcc/core_data/client.rs +++ b/crates/ironrdp-pdu/src/gcc/core_data/client.rs @@ -37,7 +37,9 @@ const DESKTOP_ORIENTATION_SIZE: usize = 2; const DESKTOP_SCALE_FACTOR_SIZE: usize = 4; const DEVICE_SCALE_FACTOR_SIZE: usize = 4; -/// TS_UD_CS_CORE (required part) +/// 2.2.1.3.2 Client Core Data (TS_UD_CS_CORE) (required part) +/// +/// [2.2.1.3.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/00f1da4a-ee9c-421a-852f-c19f92343d73 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientCoreData { pub version: RdpVersion, @@ -182,7 +184,12 @@ impl<'de> PduDecode<'de> for ClientCoreData { } } -/// TS_UD_CS_CORE (optional part) +/// 2.2.1.3.2 Client Core Data (TS_UD_CS_CORE) (optional part) +/// +/// For every field in this structure, the previous fields MUST be present in order to be a valid structure. +/// It is incumbent on the user of this structure to ensure that the structure is valid. +/// +/// [2.2.1.3.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/00f1da4a-ee9c-421a-852f-c19f92343d73 #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ClientCoreOptionalData { /// The requested color depth. Values in this field MUST be ignored if the highColorDepth field is present. @@ -217,26 +224,56 @@ impl PduEncode for ClientCoreOptionalData { } if let Some(value) = self.client_product_id { + if self.post_beta2_color_depth.is_none() { + return Err(invalid_message_err!( + "postBeta2ColorDepth", + "postBeta2ColorDepth must be present" + )); + } dst.write_u16(value); } if let Some(value) = self.serial_number { + if self.client_product_id.is_none() { + return Err(invalid_message_err!( + "clientProductId", + "clientProductId must be present" + )); + } dst.write_u32(value); } if let Some(value) = self.high_color_depth { + if self.serial_number.is_none() { + return Err(invalid_message_err!("serialNumber", "serialNumber must be present")); + } dst.write_u16(value.to_u16().unwrap()); } if let Some(value) = self.supported_color_depths { + if self.high_color_depth.is_none() { + return Err(invalid_message_err!("highColorDepth", "highColorDepth must be present")); + } dst.write_u16(value.bits()); } if let Some(value) = self.early_capability_flags { + if self.supported_color_depths.is_none() { + return Err(invalid_message_err!( + "supportedColorDepths", + "supportedColorDepths must be present" + )); + } dst.write_u16(value.bits()); } if let Some(ref value) = self.dig_product_id { + if self.early_capability_flags.is_none() { + return Err(invalid_message_err!( + "earlyCapabilityFlags", + "earlyCapabilityFlags must be present" + )); + } let mut dig_product_id_buffer = utils::to_utf16_bytes(value); dig_product_id_buffer.resize(DIG_PRODUCT_ID_SIZE - 2, 0); dig_product_id_buffer.extend_from_slice([0; 2].as_ref()); // UTF-16 null terminator @@ -245,31 +282,67 @@ impl PduEncode for ClientCoreOptionalData { } if let Some(value) = self.connection_type { + if self.dig_product_id.is_none() { + return Err(invalid_message_err!("digProductId", "digProductId must be present")); + } dst.write_u8(value.to_u8().unwrap()); write_padding!(dst, 1); } if let Some(value) = self.server_selected_protocol { + if self.connection_type.is_none() { + return Err(invalid_message_err!("connectionType", "connectionType must be present")); + } dst.write_u32(value.bits()) } if let Some(value) = self.desktop_physical_width { + if self.server_selected_protocol.is_none() { + return Err(invalid_message_err!( + "serverSelectedProtocol", + "serverSelectedProtocol must be present" + )); + } dst.write_u32(value); } if let Some(value) = self.desktop_physical_height { + if self.desktop_physical_width.is_none() { + return Err(invalid_message_err!( + "desktopPhysicalWidth", + "desktopPhysicalWidth must be present" + )); + } dst.write_u32(value); } if let Some(value) = self.desktop_orientation { + if self.desktop_physical_height.is_none() { + return Err(invalid_message_err!( + "desktopPhysicalHeight", + "desktopPhysicalHeight must be present" + )); + } dst.write_u16(value); } if let Some(value) = self.desktop_scale_factor { + if self.desktop_orientation.is_none() { + return Err(invalid_message_err!( + "desktopOrientation", + "desktopOrientation must be present" + )); + } dst.write_u32(value); } if let Some(value) = self.device_scale_factor { + if self.desktop_scale_factor.is_none() { + return Err(invalid_message_err!( + "desktopScaleFactor", + "desktopScaleFactor must be present" + )); + } dst.write_u32(value); } diff --git a/crates/ironrdp-pdu/src/per.rs b/crates/ironrdp-pdu/src/per.rs index e95a495e0..a58cfcbae 100644 --- a/crates/ironrdp-pdu/src/per.rs +++ b/crates/ironrdp-pdu/src/per.rs @@ -122,6 +122,10 @@ pub(crate) fn sizeof_length(length: u16) -> usize { } } +pub(crate) fn sizeof_long_length() -> usize { + 2 +} + pub(crate) fn sizeof_u32(value: u32) -> usize { if value <= 0xff { 2 diff --git a/crates/ironrdp-server/src/encoder/rfx.rs b/crates/ironrdp-server/src/encoder/rfx.rs index 989339faa..3a8a981f5 100644 --- a/crates/ironrdp-server/src/encoder/rfx.rs +++ b/crates/ironrdp-server/src/encoder/rfx.rs @@ -37,8 +37,8 @@ impl RfxEncoder { }; let context = rfx::Headers::Context(context); let channels = rfx::ChannelsPdu(vec![RfxChannel { - width: RfxChannelWidth::new(width).map_err(|e| custom_err!("width", e))?, - height: RfxChannelHeight::new(height).map_err(|e| custom_err!("height", e))?, + width: RfxChannelWidth::new(width), + height: RfxChannelHeight::new(height), }]); let channels = rfx::Headers::Channels(channels); let version = rfx::CodecVersionsPdu; diff --git a/crates/ironrdp-session/Cargo.toml b/crates/ironrdp-session/Cargo.toml index 11995b539..a4a178880 100644 --- a/crates/ironrdp-session/Cargo.toml +++ b/crates/ironrdp-session/Cargo.toml @@ -22,4 +22,5 @@ ironrdp-dvc.workspace = true ironrdp-error.workspace = true ironrdp-graphics.workspace = true ironrdp-pdu = { workspace = true, features = ["std"] } +ironrdp-displaycontrol.workspace = true tracing.workspace = true diff --git a/crates/ironrdp-session/src/active_stage.rs b/crates/ironrdp-session/src/active_stage.rs index 881dde6c9..12000c80a 100644 --- a/crates/ironrdp-session/src/active_stage.rs +++ b/crates/ironrdp-session/src/active_stage.rs @@ -2,6 +2,8 @@ use std::rc::Rc; use ironrdp_connector::connection_activation::ConnectionActivationSequence; use ironrdp_connector::ConnectionResult; +use ironrdp_displaycontrol::client::DisplayControlClient; +use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel}; use ironrdp_graphics::pointer::DecodedPointer; use ironrdp_pdu::geometry::InclusiveRectangle; use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent}; @@ -150,6 +152,10 @@ impl ActiveStage { self.fast_path_processor = processor; } + pub fn set_no_server_pointer(&mut self, no_server_pointer: bool) { + self.no_server_pointer = no_server_pointer; + } + /// Encodes client-side graceful shutdown request. Note that upon sending this request, /// client should wait for server's ShutdownDenied PDU before closing the connection. /// @@ -177,6 +183,10 @@ impl ActiveStage { self.x224_processor.get_svc_processor_mut() } + pub fn get_dvc(&mut self) -> Option<&DynamicVirtualChannel> { + self.x224_processor.get_dvc::() + } + /// Completes user's SVC request with data, required to sent it over the network and returns /// a buffer with encoded data. pub fn process_svc_processor_messages( @@ -185,6 +195,56 @@ impl ActiveStage { ) -> SessionResult> { self.x224_processor.process_svc_processor_messages(messages) } + + /// Fully encodes a resize request for sending over the Display Control Virtual Channel. + /// + /// If the Display Control Virtual Channel is not available, or not yet connected, this method + /// will return `None`. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent. + /// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm. + /// + /// Use [`ironrdp_displaycontrol::pdu::MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function + /// to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn encode_resize( + &mut self, + width: u32, + height: u32, + scale_factor: Option, + physical_dims: Option<(u32, u32)>, + ) -> Option>> { + if let Some(dvc) = self.get_dvc::() { + if dvc.is_open() { + let display_control = dvc.channel_processor_downcast_ref::()?; + let channel_id = dvc.channel_id().unwrap(); // Safe to unwrap, as we checked if the channel is open + let svc_messages = match display_control.encode_single_primary_monitor( + channel_id, + width, + height, + scale_factor, + physical_dims, + ) { + Ok(messages) => messages, + Err(e) => return Some(Err(SessionError::pdu(e))), + }; + + return Some( + self.process_svc_processor_messages(SvcProcessorMessages::::new(svc_messages)), + ); + } else { + debug!("Could not encode a resize: Display Control Virtual Channel is not yet connected"); + } + } else { + debug!("Could not encode a resize: Display Control Virtual Channel is not available"); + } + + None + } } #[derive(Debug)] diff --git a/crates/ironrdp-session/src/x224/mod.rs b/crates/ironrdp-session/src/x224/mod.rs index 752ab90b2..86a64dfc6 100644 --- a/crates/ironrdp-session/src/x224/mod.rs +++ b/crates/ironrdp-session/src/x224/mod.rs @@ -1,6 +1,6 @@ use ironrdp_connector::connection_activation::ConnectionActivationSequence; use ironrdp_connector::legacy::SendDataIndicationCtx; -use ironrdp_dvc::DynamicChannelId; +use ironrdp_dvc::DynamicVirtualChannel; use ironrdp_dvc::{DrdynvcClient, DvcProcessor}; use ironrdp_pdu::mcs::{DisconnectProviderUltimatum, DisconnectReason, McsMessage}; use ironrdp_pdu::rdp::headers::ShareDataPdu; @@ -72,9 +72,8 @@ impl Processor { process_svc_messages(messages.into(), channel_id, self.user_channel_id) } - pub fn get_dvc_processor(&self) -> Option<(&T, Option)> { - self.get_svc_processor::()? - .get_dynamic_channel_by_type_id::() + pub fn get_dvc(&self) -> Option<&DynamicVirtualChannel> { + self.get_svc_processor::()?.get_dvc_by_type_id::() } /// Processes a received PDU. Returns a vector of [`ProcessorOutput`] that must be processed diff --git a/crates/ironrdp-testsuite-core/tests/pdu/rfx.rs b/crates/ironrdp-testsuite-core/tests/pdu/rfx.rs index a621af286..7d183e107 100644 --- a/crates/ironrdp-testsuite-core/tests/pdu/rfx.rs +++ b/crates/ironrdp-testsuite-core/tests/pdu/rfx.rs @@ -253,8 +253,8 @@ const FRAME_END_PDU: FrameEndPdu = FrameEndPdu; lazy_static::lazy_static! { static ref CHANNELS_PDU: ChannelsPdu = ChannelsPdu(vec![ - RfxChannel { width: RfxChannelWidth::new(64).unwrap(), height: RfxChannelHeight::new(64).unwrap() }, - RfxChannel { width: RfxChannelWidth::new(32).unwrap(), height: RfxChannelHeight::new(32).unwrap() } + RfxChannel { width: RfxChannelWidth::new(64), height: RfxChannelHeight::new(64) }, + RfxChannel { width: RfxChannelWidth::new(32), height: RfxChannelHeight::new(32) } ]); static ref REGION_PDU: RegionPdu = RegionPdu { rectangles: vec![ diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index f2f675ea0..43c0e8839 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -643,6 +643,7 @@ impl Session { } .build(), ); + active_stage.set_no_server_pointer(no_server_pointer); break 'activation_seq; } } @@ -802,6 +803,7 @@ fn build_config( autologon: false, pointer_software_rendering: false, performance_flags: PerformanceFlags::default(), + desktop_scale_factor: 0, } } diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml index d5af798c7..799f54853 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -29,6 +29,7 @@ svc = ["dep:ironrdp-svc"] dvc = ["dep:ironrdp-dvc"] rdpdr = ["dep:ironrdp-rdpdr"] rdpsnd = ["dep:ironrdp-rdpsnd"] +displaycontrol = ["dep:ironrdp-displaycontrol"] [dependencies] ironrdp-pdu = { workspace = true, optional = true } @@ -43,6 +44,7 @@ ironrdp-svc = { workspace = true, optional = true } ironrdp-dvc = { workspace = true, optional = true } ironrdp-rdpdr = { workspace = true, optional = true } ironrdp-rdpsnd = { workspace = true, optional = true } +ironrdp-displaycontrol = { workspace = true, optional = true } [dev-dependencies] ironrdp-blocking.workspace = true diff --git a/crates/ironrdp/examples/screenshot.rs b/crates/ironrdp/examples/screenshot.rs index f38bf88f3..3bbca4289 100644 --- a/crates/ironrdp/examples/screenshot.rs +++ b/crates/ironrdp/examples/screenshot.rs @@ -217,6 +217,7 @@ fn build_config(username: String, password: String, domain: Option) -> c autologon: false, pointer_software_rendering: true, performance_flags: PerformanceFlags::default(), + desktop_scale_factor: 0, } } diff --git a/crates/ironrdp/src/lib.rs b/crates/ironrdp/src/lib.rs index a9d26db0c..6262e297c 100644 --- a/crates/ironrdp/src/lib.rs +++ b/crates/ironrdp/src/lib.rs @@ -8,6 +8,8 @@ pub use ironrdp_acceptor as acceptor; pub use ironrdp_cliprdr as cliprdr; #[cfg(feature = "connector")] pub use ironrdp_connector as connector; +#[cfg(feature = "displaycontrol")] +pub use ironrdp_displaycontrol as displaycontrol; #[cfg(feature = "dvc")] pub use ironrdp_dvc as dvc; #[cfg(feature = "graphics")] diff --git a/ffi/src/connector/config.rs b/ffi/src/connector/config.rs index c3804b279..dc55f0ca9 100644 --- a/ffi/src/connector/config.rs +++ b/ffi/src/connector/config.rs @@ -188,6 +188,7 @@ pub mod ffi { autologon: self.autologon.unwrap_or(false), pointer_software_rendering: self.pointer_software_rendering.unwrap_or(false), performance_flags: self.performance_flags.ok_or("performance flag is missing")?, + desktop_scale_factor: 0, }; Ok(Box::new(Config(inner_config)))