diff --git a/crates/ironrdp-acceptor/src/connection.rs b/crates/ironrdp-acceptor/src/connection.rs index 269c3377a..b3e9b52ea 100644 --- a/crates/ironrdp-acceptor/src/connection.rs +++ b/crates/ironrdp-acceptor/src/connection.rs @@ -36,6 +36,37 @@ pub struct Acceptor { pub(crate) creds: Option, received_credentials: Option, reactivation: bool, + honor_client_desktop_size: bool, +} + +/// Minimum and maximum desktop dimension accepted from a client. +/// +/// A desktop dimension in RDP is a `u16`; [MS-RDPBCGR] caps it at 8192, and +/// 200 is a conservative floor below which a request is treated as malformed. +const MIN_DESKTOP_DIM: u16 = 200; +const MAX_DESKTOP_DIM: u16 = 8192; + +/// Returns the client-requested desktop size if both dimensions are within the +/// protocol-legal range, otherwise `None`. +fn validate_desktop_size(width: u16, height: u16) -> Option { + if (MIN_DESKTOP_DIM..=MAX_DESKTOP_DIM).contains(&width) && (MIN_DESKTOP_DIM..=MAX_DESKTOP_DIM).contains(&height) { + Some(DesktopSize { width, height }) + } else { + None + } +} + +/// Writes `size` into every Bitmap capability set in `capabilities`. +/// +/// The server advertises its desktop size in the Bitmap capability set of the +/// Demand Active PDU; this keeps that advertisement in sync with `size`. +fn set_bitmap_desktop_size(capabilities: &mut [CapabilitySet], size: DesktopSize) { + for cap in capabilities.iter_mut() { + if let CapabilitySet::Bitmap(cap) = cap { + cap.desktop_width = size.width; + cap.desktop_height = size.height; + } + } } #[derive(Debug)] @@ -76,9 +107,29 @@ impl Acceptor { creds, received_credentials: None, reactivation: false, + honor_client_desktop_size: false, } } + /// Adopt the desktop size requested by the client in its Client Core Data + /// instead of the size this acceptor was constructed with. + /// + /// The client's requested resolution is only carried in the GCC Client + /// Core Data of the MCS Connect Initial PDU; the desktop size echoed back + /// later in the client's Confirm Active is, per [MS-RDPBCGR] 2.2.1.13.2, + /// the value the client copied from the *server's* Demand Active, so it + /// cannot be used to discover what the client originally asked for. When + /// this is enabled and the client's request is within the protocol-legal + /// range, the acceptor negotiates that size from the start (it is written + /// into the server's Bitmap capability set before Demand Active is sent), + /// avoiding a Deactivation-Reactivation resize round trip. + /// + /// Disabled by default, preserving the previous behavior of always + /// enforcing the server-provided size. + pub fn set_honor_client_desktop_size(&mut self, honor: bool) { + self.honor_client_desktop_size = honor; + } + pub fn new_deactivation_reactivation( mut consumed: Acceptor, static_channels: StaticChannelSet, @@ -92,12 +143,7 @@ impl Acceptor { return Err(general_err!("invalid acceptor state")); }; - for cap in consumed.server_capabilities.iter_mut() { - if let CapabilitySet::Bitmap(cap) = cap { - cap.desktop_width = desktop_size.width; - cap.desktop_height = desktop_size.height; - } - } + set_bitmap_desktop_size(&mut consumed.server_capabilities, desktop_size); let state = AcceptorState::CapabilitiesSendServer { early_capability, channels: channels.clone(), @@ -118,6 +164,7 @@ impl Acceptor { creds: consumed.creds, received_credentials: consumed.received_credentials, reactivation: true, + honor_client_desktop_size: consumed.honor_client_desktop_size, }) } @@ -427,6 +474,26 @@ impl Sequence for Acceptor { let gcc_blocks = settings_initial.conference_create_request.into_gcc_blocks(); let early_capability = gcc_blocks.core.optional_data.early_capability_flags; + // Adopt the client's requested desktop size (from its Client + // Core Data) before Demand Active is sent, so the session is + // negotiated at that size without a Deactivation-Reactivation + // resize. See `set_honor_client_desktop_size`. + if self.honor_client_desktop_size { + if let Some(client_size) = + validate_desktop_size(gcc_blocks.core.desktop_width, gcc_blocks.core.desktop_height) + { + if client_size != self.desktop_size { + debug!( + requested = ?client_size, + previous = ?self.desktop_size, + "Honoring client-requested desktop size" + ); + self.desktop_size = client_size; + set_bitmap_desktop_size(&mut self.server_capabilities, client_size); + } + } + } + let joined: Vec<_> = gcc_blocks .network .map(|network| { diff --git a/crates/ironrdp-server/src/builder.rs b/crates/ironrdp-server/src/builder.rs index fb959830c..c4fbb783a 100644 --- a/crates/ironrdp-server/src/builder.rs +++ b/crates/ironrdp-server/src/builder.rs @@ -41,6 +41,7 @@ pub struct BuilderDone { #[cfg(feature = "egfx")] gfx_factory: Option>, display_suppressed: Option>, + honor_client_desktop_size: bool, } pub struct RdpServerBuilder { @@ -140,6 +141,7 @@ impl RdpServerBuilder { #[cfg(feature = "egfx")] gfx_factory: None, display_suppressed: None, + honor_client_desktop_size: false, }, } } @@ -160,6 +162,7 @@ impl RdpServerBuilder { #[cfg(feature = "egfx")] gfx_factory: None, display_suppressed: None, + honor_client_desktop_size: false, }, } } @@ -226,6 +229,25 @@ impl RdpServerBuilder { self } + /// Negotiate each session at the desktop size the client requests in its + /// Client Core Data, rather than the size reported by the display handler. + /// + /// The client's requested resolution is only carried in the GCC Client + /// Core Data of the connection handshake; the size echoed back in the + /// client's Confirm Active is the value it copied from the server's Demand + /// Active (per [MS-RDPBCGR] 2.2.1.13.2) and so cannot reveal what the + /// client asked for. With this enabled the acceptor adopts the requested + /// size (when within the protocol-legal range) before Demand Active is + /// sent, so the session starts at that size with no Deactivation- + /// Reactivation resize. The display handler observes the negotiated size + /// through [`RdpServerDisplay::request_initial_size`]. + /// + /// Defaults to `false`, enforcing the size reported by the display handler. + pub fn with_honor_client_desktop_size(mut self, honor: bool) -> Self { + self.state.honor_client_desktop_size = honor; + self + } + /// Set a credential validator for TLS-mode connections. /// /// When set, credentials received from the client during @@ -248,6 +270,7 @@ impl RdpServerBuilder { security: self.state.security, codecs: self.state.codecs, max_request_size: self.state.max_request_size, + honor_client_desktop_size: self.state.honor_client_desktop_size, }, self.state.handler, self.state.display, diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index 0be2a0707..cba560b83 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -223,6 +223,12 @@ pub struct RdpServerOptions { pub security: RdpServerSecurity, pub codecs: BitmapCodecs, pub max_request_size: u32, + /// When `true`, each connection's acceptor adopts the desktop size the + /// client requests in its Client Core Data (instead of the size reported + /// by the display handler), negotiating that size from the start without a + /// Deactivation-Reactivation resize. Defaults to `false`. Set via + /// [`RdpServerBuilder::with_honor_client_desktop_size`](crate::RdpServerBuilder::with_honor_client_desktop_size). + pub honor_client_desktop_size: bool, } impl RdpServerOptions { @@ -692,6 +698,7 @@ impl RdpServer { let size = self.display.lock().await.size().await; let capabilities = capabilities::capabilities(&self.opts, size); let mut acceptor = Acceptor::new(self.opts.security.flag(), size, capabilities, self.creds.clone()); + acceptor.set_honor_client_desktop_size(self.opts.honor_client_desktop_size); self.attach_channels(&mut acceptor);