mirror of
https://github.com/Devolutions/IronRDP.git
synced 2025-12-23 12:26:46 +00:00
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 <3809077+CBenoit@users.noreply.github.com>
This commit is contained in:
parent
32b0e40eca
commit
100765f98f
2 changed files with 44 additions and 9 deletions
|
|
@ -69,6 +69,7 @@ struct SessionBuilderInner {
|
|||
|
||||
use_display_control: bool,
|
||||
enable_credssp: bool,
|
||||
outbound_message_size_limit: Option<u32>,
|
||||
}
|
||||
|
||||
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<Vec<u8>>, rdp_writer: WriteHalf<WebSocket>) {
|
||||
async fn writer_task(
|
||||
rx: mpsc::UnboundedReceiver<Vec<u8>>,
|
||||
rdp_writer: WriteHalf<WebSocket>,
|
||||
outbound_limit: Option<u32>,
|
||||
) {
|
||||
debug!("writer task started");
|
||||
|
||||
async fn inner(
|
||||
mut rx: mpsc::UnboundedReceiver<Vec<u8>>,
|
||||
mut rdp_writer: WriteHalf<WebSocket>,
|
||||
outbound_limit: Option<u32>,
|
||||
) -> 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")?;
|
||||
}
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue