diff --git a/crates/ironrdp-web/Cargo.toml b/crates/ironrdp-web/Cargo.toml index 8159b840..f5964a49 100644 --- a/crates/ironrdp-web/Cargo.toml +++ b/crates/ironrdp-web/Cargo.toml @@ -30,6 +30,7 @@ ironrdp = { workspace = true, features = [ "dvc", "cliprdr", "svc", + "displaycontrol" ] } ironrdp-core.workspace = true ironrdp-cliprdr-format = { workspace = true } diff --git a/crates/ironrdp-web/src/canvas.rs b/crates/ironrdp-web/src/canvas.rs index 963c594a..f1b02877 100644 --- a/crates/ironrdp-web/src/canvas.rs +++ b/crates/ironrdp-web/src/canvas.rs @@ -36,6 +36,11 @@ impl Canvas { Ok(Self { width, surface }) } + pub(crate) fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) { + self.surface.resize(width, height).expect("surface resize"); + self.width = width.get(); + } + pub(crate) fn draw(&mut self, buffer: &[u8], region: InclusiveRectangle) -> anyhow::Result<()> { let region_width = region.width(); let region_height = region.height(); diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index dca625a8..905a320f 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -3,6 +3,7 @@ use core::cell::RefCell; use std::borrow::Cow; +use std::num::NonZeroU32; use std::rc::Rc; use std::time::Duration; @@ -18,6 +19,8 @@ use ironrdp::cliprdr::CliprdrClient; use ironrdp::connector::connection_activation::ConnectionActivationState; use ironrdp::connector::credssp::KerberosConfig; use ironrdp::connector::{self, ClientConnector, Credentials}; +use ironrdp::displaycontrol::client::DisplayControlClient; +use ironrdp::dvc::DrdynvcClient; use ironrdp::graphics::image_processing::PixelFormat; use ironrdp::pdu::input::fast_path::FastPathInputEvent; use ironrdp::pdu::rdp::client_info::PerformanceFlags; @@ -64,6 +67,8 @@ struct SessionBuilderInner { remote_clipboard_changed_callback: Option, remote_received_format_list_callback: Option, force_clipboard_update_callback: Option, + + use_display_control: bool, } impl Default for SessionBuilderInner { @@ -89,6 +94,8 @@ impl Default for SessionBuilderInner { remote_clipboard_changed_callback: None, remote_received_format_list_callback: None, force_clipboard_update_callback: None, + + use_display_control: false, } } } @@ -209,6 +216,12 @@ impl SessionBuilder { self.clone() } + /// Optional + pub fn use_display_control(&self) -> SessionBuilder { + self.0.borrow_mut().use_display_control = true; + self.clone() + } + pub async fn connect(&self) -> Result { let ( username, @@ -301,15 +314,18 @@ impl SessionBuilder { } } - let (connection_result, ws) = connect( + let use_display_control = self.0.borrow().use_display_control; + + let (connection_result, ws) = connect(ConnectParams { ws, config, - auth_token, + proxy_auth_token: auth_token, destination, pcb, kdc_proxy_url, - clipboard.as_ref().map(|clip| clip.backend()), - ) + clipboard_backend: clipboard.as_ref().map(|clip| clip.backend()), + use_display_control, + }) .await?; info!("Connected!"); @@ -345,6 +361,12 @@ pub(crate) enum RdpInputEvent { Cliprdr(ClipboardMessage), ClipboardBackend(WasmClipboardBackendMessage), FastPath(FastPathInputEvents), + Resize { + width: u32, + height: u32, + scale_factor: Option, + physical_size: Option<(u32, u32)>, + }, TerminateSession, } @@ -489,6 +511,21 @@ impl Session { active_stage.process_fastpath_input(&mut image, &events) .context("fast path input events processing")? } + RdpInputEvent::Resize { width, height, scale_factor, physical_size } => { + debug!(width, height, scale_factor, "Resize event received"); + if width == 0 || height == 0 { + warn!("Resize event ignored: width or height is zero"); + Vec::new() + } else if let Some(response_frame) = active_stage.encode_resize(width, height, scale_factor, physical_size) { + self.render_canvas.set_width(width); + self.render_canvas.set_height(height); + gui.resize(NonZeroU32::new(width).unwrap(), NonZeroU32::new(height).unwrap()); + vec![ActiveStageOutput::ResponseFrame(response_frame?)] + } else { + debug!("Resize event ignored"); + Vec::new() + } + }, RdpInputEvent::TerminateSession => { active_stage.graceful_shutdown() .context("graceful shutdown")? @@ -757,6 +794,24 @@ impl Session { Ok(()) } + pub fn resize( + &self, + width: u32, + height: u32, + scale_factor: Option, + physical_width: Option, + physical_height: Option, + ) { + self.input_events_tx + .unbounded_send(RdpInputEvent::Resize { + width, + height, + scale_factor, + physical_size: physical_width.and_then(|width| physical_height.map(|height| (width, height))), + }) + .expect("send resize event to writer task"); + } + #[allow(clippy::unused_self)] pub fn supports_unicode_keyboard_shortcuts(&self) -> bool { // RDP does not support Unicode keyboard shortcuts (When key combinations are executed, only @@ -832,7 +887,7 @@ async fn writer_task(rx: mpsc::UnboundedReceiver>, rdp_writer: WriteHalf } } -async fn connect( +struct ConnectParams { ws: WebSocket, config: connector::Config, proxy_auth_token: String, @@ -840,6 +895,20 @@ async fn connect( pcb: Option, kdc_proxy_url: Option, clipboard_backend: Option, + use_display_control: bool, +} + +async fn connect( + ConnectParams { + ws, + config, + proxy_auth_token, + destination, + pcb, + kdc_proxy_url, + clipboard_backend, + use_display_control, + }: ConnectParams, ) -> Result<(connector::ConnectionResult, WebSocket), IronRdpError> { let mut framed = ironrdp_futures::LocalFuturesFramed::new(ws); @@ -849,6 +918,12 @@ async fn connect( connector.attach_static_channel(CliprdrClient::new(Box::new(clipboard_backend))); } + if use_display_control { + connector.attach_static_channel( + DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))), + ); + } + let (upgraded, server_public_key) = connect_rdcleanpath(&mut framed, &mut connector, destination.clone(), proxy_auth_token, pcb).await?; diff --git a/web-client/iron-remote-gui/src/interfaces/UserInteraction.ts b/web-client/iron-remote-gui/src/interfaces/UserInteraction.ts index 83c8e07e..015a9c5f 100644 --- a/web-client/iron-remote-gui/src/interfaces/UserInteraction.ts +++ b/web-client/iron-remote-gui/src/interfaces/UserInteraction.ts @@ -18,6 +18,7 @@ export interface UserInteraction { desktopSize?: DesktopSize, preConnectionBlob?: string, kdc_proxy_url?: string, + use_display_control?: boolean, ): Promise; setKeyboardUnicodeMode(use_unicode: boolean): void; @@ -31,4 +32,6 @@ export interface UserInteraction { setCursorStyleOverride(style: string | null): void; onSessionEvent(callback: (event: SessionEvent) => void): void; + + resize(width: number, height: number, scale?: number): void; } diff --git a/web-client/iron-remote-gui/src/iron-remote-gui.svelte b/web-client/iron-remote-gui/src/iron-remote-gui.svelte index 2f78c61b..d8533d81 100644 --- a/web-client/iron-remote-gui/src/iron-remote-gui.svelte +++ b/web-client/iron-remote-gui/src/iron-remote-gui.svelte @@ -445,6 +445,11 @@ scaleSession(s); }); + wasmService.dynamicResize.subscribe((evt) => { + loggingService.info(`Dynamic resize!, width: ${evt.width}, height: ${evt.height}`); + setViewerStyle(evt.width.toString(), evt.height.toString(), true); + }); + wasmService.changeVisibilityObservable.subscribe((val) => { isVisible = val; if (val) { diff --git a/web-client/iron-remote-gui/src/services/PublicAPI.ts b/web-client/iron-remote-gui/src/services/PublicAPI.ts index c485cc09..db5162cd 100644 --- a/web-client/iron-remote-gui/src/services/PublicAPI.ts +++ b/web-client/iron-remote-gui/src/services/PublicAPI.ts @@ -23,6 +23,7 @@ export class PublicAPI { desktopSize?: DesktopSize, preConnectionBlob?: string, kdc_proxy_url?: string, + use_display_control = false, ): Promise { loggingService.info('Initializing connection.'); const resultObservable = this.wasmService.connect( @@ -35,6 +36,7 @@ export class PublicAPI { desktopSize, preConnectionBlob, kdc_proxy_url, + use_display_control, ); return resultObservable.toPromise(); @@ -69,6 +71,10 @@ export class PublicAPI { this.wasmService.setCursorStyleOverride(style); } + private resize(width: number, height: number, scale?: number) { + this.wasmService.resizeDynamic(width, height, scale); + } + getExposedFunctions(): UserInteraction { return { setVisibility: this.setVisibility.bind(this), @@ -82,6 +88,7 @@ export class PublicAPI { shutdown: this.shutdown.bind(this), setKeyboardUnicodeMode: this.setKeyboardUnicodeMode.bind(this), setCursorStyleOverride: this.setCursorStyleOverride.bind(this), + resize: this.resize.bind(this), }; } } diff --git a/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts b/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts index 2d1d4cc0..1e749da6 100644 --- a/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts +++ b/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts @@ -57,6 +57,11 @@ export class WasmBridgeService { sessionObserver: Observable = this.sessionEvent.asObservable(); scaleObserver: Observable = this.scale.asObservable(); + dynamicResize = new Subject<{ + width: number; + height: number; + }>(); + constructor() { this.resize = this._resize.asObservable(); loggingService.info('Web bridge initialized.'); @@ -131,6 +136,7 @@ export class WasmBridgeService { desktopSize?: IDesktopSize, preConnectionBlob?: string, kdc_proxy_url?: string, + use_display_control = true, ): Observable { const sessionBuilder = SessionBuilder.new(); sessionBuilder.proxy_address(proxyAddress); @@ -143,6 +149,7 @@ export class WasmBridgeService { sessionBuilder.set_cursor_style_callback_context(this); sessionBuilder.set_cursor_style_callback(this.setCursorStyleCallback); sessionBuilder.kdc_proxy_url(kdc_proxy_url); + use_display_control && sessionBuilder.use_display_control(); if (preConnectionBlob != null) { sessionBuilder.pcb(preConnectionBlob); @@ -253,6 +260,11 @@ export class WasmBridgeService { this.canvas = canvas; } + resizeDynamic(width: number, height: number, scale?: number) { + this.dynamicResize.next({ width, height }); + this.session?.resize(width, height, scale); + } + /// Triggered by the browser when local clipboard is updated. Clipboard backend should /// cache the content and send it to the server when it is requested. onClipboardChanged(transaction: ClipboardTransaction): Promise { diff --git a/web-client/iron-svelte-client/src/app.html b/web-client/iron-svelte-client/src/app.html index 51f2a220..542d1e1e 100644 --- a/web-client/iron-svelte-client/src/app.html +++ b/web-client/iron-svelte-client/src/app.html @@ -1,5 +1,5 @@ - + @@ -13,7 +13,17 @@ %sveltekit.head% - +
%sveltekit.body%
+ + diff --git a/web-client/iron-svelte-client/src/lib/login/login.svelte b/web-client/iron-svelte-client/src/lib/login/login.svelte index af37a181..893e3ef3 100644 --- a/web-client/iron-svelte-client/src/lib/login/login.svelte +++ b/web-client/iron-svelte-client/src/lib/login/login.svelte @@ -20,6 +20,7 @@ height: 768, }; let pcb: string; + let pop_up = false; let userInteraction: UserInteraction; @@ -53,6 +54,28 @@ type: 'info', message: 'Connection in progress...', }); + + if (pop_up) { + const data = JSON.stringify({ + username, + password, + hostname, + gatewayAddress, + domain, + authtoken, + desktopSize, + pcb, + kdc_proxy_url, + }); + const base64Data = btoa(data); + window.open( + `/popup-session?data=${base64Data}`, + '_blank', + `width=${desktopSize.width},height=${desktopSize.height},resizable=yes,scrollbars=yes,status=yes`, + ); + return; + } + from( userInteraction.connect( username, @@ -64,6 +87,7 @@ desktopSize, pcb, kdc_proxy_url, + true, ), ) .pipe( @@ -149,6 +173,17 @@ +
+
+ + +
+