From 100765f98ff59d33e3ee0664e189bf34ccd6574a Mon Sep 17 00:00:00 2001 From: Gabriel Bauman <967743+gabrielbauman@users.noreply.github.com> Date: Thu, 31 Jul 2025 04:18:36 -0700 Subject: [PATCH] feat(web): add outbound WebSocket message size limit extension (#889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for chunking outbound WebSocket messages when they exceed a configurable size limit. This helps avoid browser- or proxy-specific WebSocket message size restrictions while maintaining wire compatibility. Changes: - Add outbound_message_size_limit field to SessionBuilderInner - Implement extension handler with safe f64->u32 casting and validation - Update writer_task to chunk large messages when limit is set - Add outboundMessageSizeLimit() helper function to JavaScript API --------- Co-authored-by: Benoît Cortier <3809077+CBenoit@users.noreply.github.com> --- crates/ironrdp-web/src/session.rs | 49 +++++++++++++++---- .../iron-remote-desktop-rdp/src/main.ts | 4 ++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 6a5416d6..9613c532 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -69,6 +69,7 @@ struct SessionBuilderInner { use_display_control: bool, enable_credssp: bool, + outbound_message_size_limit: Option, } impl Default for SessionBuilderInner { @@ -97,6 +98,7 @@ impl Default for SessionBuilderInner { use_display_control: false, enable_credssp: true, + outbound_message_size_limit: None, } } } @@ -219,6 +221,16 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { |kdc_proxy_url: String| { self.0.borrow_mut().kdc_proxy_url = Some(kdc_proxy_url) }; |display_control: bool| { self.0.borrow_mut().use_display_control = display_control }; |enable_credssp: bool| { self.0.borrow_mut().enable_credssp = enable_credssp }; + |outbound_message_size_limit: f64| { + let limit = if outbound_message_size_limit >= 0.0 && outbound_message_size_limit <= f64::from(u32::MAX) { + #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + { outbound_message_size_limit as u32 } + } else { + warn!(outbound_message_size_limit, "Invalid outbound message size limit; fallback to unlimited"); + 0 // Fallback to no limit for invalid values. + }; + self.0.borrow_mut().outbound_message_size_limit = if limit > 0 { Some(limit) } else { None }; + }; } self.clone() @@ -242,6 +254,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { remote_clipboard_changed_callback, remote_received_format_list_callback, force_clipboard_update_callback, + outbound_message_size_limit, ); { @@ -271,6 +284,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { remote_clipboard_changed_callback = inner.remote_clipboard_changed_callback.clone(); remote_received_format_list_callback = inner.remote_received_format_list_callback.clone(); force_clipboard_update_callback = inner.force_clipboard_update_callback.clone(); + outbound_message_size_limit = inner.outbound_message_size_limit; } info!("Connect to RDP host"); @@ -293,9 +307,9 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { ) }); - let ws = WebSocket::open(&proxy_address).context("Couldn’t open WebSocket")?; + let ws = WebSocket::open(&proxy_address).context("couldn't open WebSocket")?; - // NOTE: ideally, when the WebSocket can’t be opened, the above call should fail with details on why is that + // NOTE: ideally, when the WebSocket can't be opened, the above call should fail with details on why is that // (e.g., the proxy hostname could not be resolved, proxy service is not running), but errors are neved // bubbled up in practice, so instead we poll the WebSocket state until we know its connected (i.e., the // WebSocket handshake is a success and user data can be exchanged). @@ -339,7 +353,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder { let (writer_tx, writer_rx) = mpsc::unbounded(); - spawn_local(writer_task(writer_rx, rdp_writer)); + spawn_local(writer_task(writer_rx, rdp_writer, outbound_message_size_limit)); Ok(Session { desktop_size: connection_result.desktop_size, @@ -885,22 +899,39 @@ fn build_config( } } -async fn writer_task(rx: mpsc::UnboundedReceiver>, rdp_writer: WriteHalf) { +async fn writer_task( + rx: mpsc::UnboundedReceiver>, + rdp_writer: WriteHalf, + outbound_limit: Option, +) { debug!("writer task started"); async fn inner( mut rx: mpsc::UnboundedReceiver>, mut rdp_writer: WriteHalf, + outbound_limit: Option, ) -> anyhow::Result<()> { while let Some(frame) = rx.next().await { - rdp_writer.write_all(&frame).await.context("Couldn’t write frame")?; - rdp_writer.flush().await.context("Couldn’t flush")?; + match outbound_limit { + Some(max_size) if frame.len() > max_size as usize => { + // Send in chunks. + for chunk in frame.chunks(max_size as usize) { + rdp_writer.write_all(chunk).await.context("couldn't write chunk")?; + rdp_writer.flush().await.context("couldn't flush chunk")?; + } + } + _ => { + // Send complete frame (default case). + rdp_writer.write_all(&frame).await.context("couldn't write frame")?; + rdp_writer.flush().await.context("couldn't flush frame")?; + } + } } Ok(()) } - match inner(rx, rdp_writer).await { + match inner(rx, rdp_writer, outbound_limit).await { Ok(()) => debug!("writer task ended gracefully"), Err(e) => error!("writer task ended unexpectedly: {e:#}"), } @@ -960,7 +991,7 @@ async fn connect( .ok() .map(|url| KerberosConfig { kdc_proxy_url: Some(url), - // HACK: It’s supposed to be the computer name of the client, but since it’s not easy to retrieve this information in the browser, + // HACK: It's supposed to be the computer name of the client, but since it's not easy to retrieve this information in the browser, // we set the destination hostname instead because it happens to work. hostname: Some(destination), }), @@ -1030,7 +1061,7 @@ where framed .write_all(&rdcleanpath_req) .await - .context("couldn’t write RDCleanPath request")?; + .context("couldn't write RDCleanPath request")?; } { diff --git a/web-client/iron-remote-desktop-rdp/src/main.ts b/web-client/iron-remote-desktop-rdp/src/main.ts index 7c9f0727..b5b0200e 100644 --- a/web-client/iron-remote-desktop-rdp/src/main.ts +++ b/web-client/iron-remote-desktop-rdp/src/main.ts @@ -36,6 +36,10 @@ export function kdcProxyUrl(url: string): Extension { return new Extension('kdc_proxy_url', url); } +export function outboundMessageSizeLimit(limit: number): Extension { + return new Extension('outbound_message_size_limit', limit); +} + export function enableCredssp(enable: boolean): Extension { return new Extension('enable_credssp', enable); }