Skip to content

Commit 100765f

Browse files
feat(web): add outbound WebSocket message size limit extension (#889)
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 <[email protected]>
1 parent 32b0e40 commit 100765f

File tree

2 files changed

+44
-9
lines changed

2 files changed

+44
-9
lines changed

crates/ironrdp-web/src/session.rs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ struct SessionBuilderInner {
6969

7070
use_display_control: bool,
7171
enable_credssp: bool,
72+
outbound_message_size_limit: Option<u32>,
7273
}
7374

7475
impl Default for SessionBuilderInner {
@@ -97,6 +98,7 @@ impl Default for SessionBuilderInner {
9798

9899
use_display_control: false,
99100
enable_credssp: true,
101+
outbound_message_size_limit: None,
100102
}
101103
}
102104
}
@@ -219,6 +221,16 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
219221
|kdc_proxy_url: String| { self.0.borrow_mut().kdc_proxy_url = Some(kdc_proxy_url) };
220222
|display_control: bool| { self.0.borrow_mut().use_display_control = display_control };
221223
|enable_credssp: bool| { self.0.borrow_mut().enable_credssp = enable_credssp };
224+
|outbound_message_size_limit: f64| {
225+
let limit = if outbound_message_size_limit >= 0.0 && outbound_message_size_limit <= f64::from(u32::MAX) {
226+
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
227+
{ outbound_message_size_limit as u32 }
228+
} else {
229+
warn!(outbound_message_size_limit, "Invalid outbound message size limit; fallback to unlimited");
230+
0 // Fallback to no limit for invalid values.
231+
};
232+
self.0.borrow_mut().outbound_message_size_limit = if limit > 0 { Some(limit) } else { None };
233+
};
222234
}
223235

224236
self.clone()
@@ -242,6 +254,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
242254
remote_clipboard_changed_callback,
243255
remote_received_format_list_callback,
244256
force_clipboard_update_callback,
257+
outbound_message_size_limit,
245258
);
246259

247260
{
@@ -271,6 +284,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
271284
remote_clipboard_changed_callback = inner.remote_clipboard_changed_callback.clone();
272285
remote_received_format_list_callback = inner.remote_received_format_list_callback.clone();
273286
force_clipboard_update_callback = inner.force_clipboard_update_callback.clone();
287+
outbound_message_size_limit = inner.outbound_message_size_limit;
274288
}
275289

276290
info!("Connect to RDP host");
@@ -293,9 +307,9 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
293307
)
294308
});
295309

296-
let ws = WebSocket::open(&proxy_address).context("Couldn’t open WebSocket")?;
310+
let ws = WebSocket::open(&proxy_address).context("couldn't open WebSocket")?;
297311

298-
// NOTE: ideally, when the WebSocket cant be opened, the above call should fail with details on why is that
312+
// NOTE: ideally, when the WebSocket can't be opened, the above call should fail with details on why is that
299313
// (e.g., the proxy hostname could not be resolved, proxy service is not running), but errors are neved
300314
// bubbled up in practice, so instead we poll the WebSocket state until we know its connected (i.e., the
301315
// WebSocket handshake is a success and user data can be exchanged).
@@ -339,7 +353,7 @@ impl iron_remote_desktop::SessionBuilder for SessionBuilder {
339353

340354
let (writer_tx, writer_rx) = mpsc::unbounded();
341355

342-
spawn_local(writer_task(writer_rx, rdp_writer));
356+
spawn_local(writer_task(writer_rx, rdp_writer, outbound_message_size_limit));
343357

344358
Ok(Session {
345359
desktop_size: connection_result.desktop_size,
@@ -885,22 +899,39 @@ fn build_config(
885899
}
886900
}
887901

888-
async fn writer_task(rx: mpsc::UnboundedReceiver<Vec<u8>>, rdp_writer: WriteHalf<WebSocket>) {
902+
async fn writer_task(
903+
rx: mpsc::UnboundedReceiver<Vec<u8>>,
904+
rdp_writer: WriteHalf<WebSocket>,
905+
outbound_limit: Option<u32>,
906+
) {
889907
debug!("writer task started");
890908

891909
async fn inner(
892910
mut rx: mpsc::UnboundedReceiver<Vec<u8>>,
893911
mut rdp_writer: WriteHalf<WebSocket>,
912+
outbound_limit: Option<u32>,
894913
) -> anyhow::Result<()> {
895914
while let Some(frame) = rx.next().await {
896-
rdp_writer.write_all(&frame).await.context("Couldn’t write frame")?;
897-
rdp_writer.flush().await.context("Couldn’t flush")?;
915+
match outbound_limit {
916+
Some(max_size) if frame.len() > max_size as usize => {
917+
// Send in chunks.
918+
for chunk in frame.chunks(max_size as usize) {
919+
rdp_writer.write_all(chunk).await.context("couldn't write chunk")?;
920+
rdp_writer.flush().await.context("couldn't flush chunk")?;
921+
}
922+
}
923+
_ => {
924+
// Send complete frame (default case).
925+
rdp_writer.write_all(&frame).await.context("couldn't write frame")?;
926+
rdp_writer.flush().await.context("couldn't flush frame")?;
927+
}
928+
}
898929
}
899930

900931
Ok(())
901932
}
902933

903-
match inner(rx, rdp_writer).await {
934+
match inner(rx, rdp_writer, outbound_limit).await {
904935
Ok(()) => debug!("writer task ended gracefully"),
905936
Err(e) => error!("writer task ended unexpectedly: {e:#}"),
906937
}
@@ -960,7 +991,7 @@ async fn connect(
960991
.ok()
961992
.map(|url| KerberosConfig {
962993
kdc_proxy_url: Some(url),
963-
// HACK: Its supposed to be the computer name of the client, but since its not easy to retrieve this information in the browser,
994+
// 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,
964995
// we set the destination hostname instead because it happens to work.
965996
hostname: Some(destination),
966997
}),
@@ -1030,7 +1061,7 @@ where
10301061
framed
10311062
.write_all(&rdcleanpath_req)
10321063
.await
1033-
.context("couldnt write RDCleanPath request")?;
1064+
.context("couldn't write RDCleanPath request")?;
10341065
}
10351066

10361067
{

web-client/iron-remote-desktop-rdp/src/main.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export function kdcProxyUrl(url: string): Extension {
3636
return new Extension('kdc_proxy_url', url);
3737
}
3838

39+
export function outboundMessageSizeLimit(limit: number): Extension {
40+
return new Extension('outbound_message_size_limit', limit);
41+
}
42+
3943
export function enableCredssp(enable: boolean): Extension {
4044
return new Extension('enable_credssp', enable);
4145
}

0 commit comments

Comments
 (0)