From ec1832bba06726a1e4aa2970136e8e2c19a0f411 Mon Sep 17 00:00:00 2001 From: Alex Yusiuk <55661041+RRRadicalEdward@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:30:45 +0300 Subject: [PATCH] feat(ironrdp-web): iron-remote-desktop helper crate for remote desktop WASM modules (#755) --- Cargo.lock | 16 +- crates/iron-remote-desktop/Cargo.toml | 32 ++ crates/iron-remote-desktop/src/clipboard.rs | 18 + crates/iron-remote-desktop/src/cursor.rs | 10 + crates/iron-remote-desktop/src/error.rs | 23 ++ crates/iron-remote-desktop/src/input.rs | 17 + crates/iron-remote-desktop/src/lib.rs | 366 ++++++++++++++++++ crates/iron-remote-desktop/src/session.rs | 82 ++++ crates/ironrdp-web/Cargo.toml | 11 +- crates/ironrdp-web/src/clipboard/mod.rs | 32 +- .../ironrdp-web/src/clipboard/transaction.rs | 76 ++-- crates/ironrdp-web/src/error.rs | 31 +- crates/ironrdp-web/src/input.rs | 35 +- crates/ironrdp-web/src/lib.rs | 65 +--- crates/ironrdp-web/src/session.rs | 180 +++++---- .../iron-remote-desktop-rdp/package.json | 2 +- .../public/package.json | 2 +- 17 files changed, 747 insertions(+), 251 deletions(-) create mode 100644 crates/iron-remote-desktop/Cargo.toml create mode 100644 crates/iron-remote-desktop/src/clipboard.rs create mode 100644 crates/iron-remote-desktop/src/cursor.rs create mode 100644 crates/iron-remote-desktop/src/error.rs create mode 100644 crates/iron-remote-desktop/src/input.rs create mode 100644 crates/iron-remote-desktop/src/lib.rs create mode 100644 crates/iron-remote-desktop/src/session.rs diff --git a/Cargo.lock b/Cargo.lock index a45169d7..fe812e43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2329,6 +2329,18 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iron-remote-desktop" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "tracing", + "tracing-subscriber", + "tracing-web", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "ironrdp" version = "0.9.1" @@ -2798,12 +2810,12 @@ dependencies = [ "anyhow", "base64", "chrono", - "console_error_panic_hook", "futures-channel", "futures-util", "getrandom 0.2.15", "gloo-net", "gloo-timers", + "iron-remote-desktop", "ironrdp", "ironrdp-cliprdr-format", "ironrdp-core", @@ -2821,8 +2833,6 @@ dependencies = [ "tap", "time", "tracing", - "tracing-subscriber", - "tracing-web", "url", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/crates/iron-remote-desktop/Cargo.toml b/crates/iron-remote-desktop/Cargo.toml new file mode 100644 index 00000000..769f60d0 --- /dev/null +++ b/crates/iron-remote-desktop/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "iron-remote-desktop" +version = "0.1.0" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[features] +panic_hook = ["dep:console_error_panic_hook"] + +[dependencies] +# WASM +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["HtmlCanvasElement"] } +tracing-web = "0.1" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1", optional = true } + +# Logging +tracing = { version = "0.1", features = ["log"] } +tracing-subscriber = { version = "0.3", features = ["time"] } + +[lints] +workspace = true diff --git a/crates/iron-remote-desktop/src/clipboard.rs b/crates/iron-remote-desktop/src/clipboard.rs new file mode 100644 index 00000000..08e9a771 --- /dev/null +++ b/crates/iron-remote-desktop/src/clipboard.rs @@ -0,0 +1,18 @@ +use wasm_bindgen::JsValue; +use web_sys::js_sys; + +pub trait ClipboardTransaction { + type ClipboardContent: ClipboardContent; + + fn init() -> Self; + fn add_content(&mut self, content: Self::ClipboardContent); + fn is_empty(&self) -> bool; + fn contents(&self) -> js_sys::Array; +} + +pub trait ClipboardContent { + fn new_text(mime_type: &str, text: &str) -> Self; + fn new_binary(mime_type: &str, binary: &[u8]) -> Self; + fn mime_type(&self) -> &str; + fn value(&self) -> JsValue; +} diff --git a/crates/iron-remote-desktop/src/cursor.rs b/crates/iron-remote-desktop/src/cursor.rs new file mode 100644 index 00000000..1d760692 --- /dev/null +++ b/crates/iron-remote-desktop/src/cursor.rs @@ -0,0 +1,10 @@ +#[derive(Debug)] +pub enum CursorStyle { + Default, + Hidden, + Url { + data: String, + hotspot_x: u16, + hotspot_y: u16, + }, +} diff --git a/crates/iron-remote-desktop/src/error.rs b/crates/iron-remote-desktop/src/error.rs new file mode 100644 index 00000000..6cf3b77a --- /dev/null +++ b/crates/iron-remote-desktop/src/error.rs @@ -0,0 +1,23 @@ +use wasm_bindgen::prelude::*; + +pub trait IronError { + fn backtrace(&self) -> String; + fn kind(&self) -> IronErrorKind; +} + +#[derive(Clone, Copy)] +#[wasm_bindgen] +pub enum IronErrorKind { + /// Catch-all error kind + General, + /// Incorrect password used + WrongPassword, + /// Unable to login to machine + LogonFailure, + /// Insufficient permission, server denied access + AccessDenied, + /// Something wrong happened when sending or receiving the RDCleanPath message + RDCleanPath, + /// Couldn’t connect to proxy + ProxyConnect, +} diff --git a/crates/iron-remote-desktop/src/input.rs b/crates/iron-remote-desktop/src/input.rs new file mode 100644 index 00000000..f30973c3 --- /dev/null +++ b/crates/iron-remote-desktop/src/input.rs @@ -0,0 +1,17 @@ +pub trait DeviceEvent { + fn mouse_button_pressed(button: u8) -> Self; + fn mouse_button_released(button: u8) -> Self; + fn mouse_move(x: u16, y: u16) -> Self; + fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self; + fn key_pressed(scancode: u16) -> Self; + fn key_released(scancode: u16) -> Self; + fn unicode_pressed(unicode: char) -> Self; + fn unicode_released(unicode: char) -> Self; +} + +pub trait InputTransaction { + type DeviceEvent: DeviceEvent; + + fn init() -> Self; + fn add_event(&mut self, event: Self::DeviceEvent); +} diff --git a/crates/iron-remote-desktop/src/lib.rs b/crates/iron-remote-desktop/src/lib.rs new file mode 100644 index 00000000..ad19efb2 --- /dev/null +++ b/crates/iron-remote-desktop/src/lib.rs @@ -0,0 +1,366 @@ +mod clipboard; +mod cursor; +mod error; +mod input; +mod session; + +pub use clipboard::{ClipboardContent, ClipboardTransaction}; +pub use cursor::CursorStyle; +pub use error::{IronError, IronErrorKind}; +pub use input::{DeviceEvent, InputTransaction}; +pub use session::{Session, SessionBuilder, SessionTerminationInfo}; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +#[derive(Clone, Copy)] +pub struct DesktopSize { + pub width: u16, + pub height: u16, +} + +#[wasm_bindgen] +impl DesktopSize { + pub fn init(width: u16, height: u16) -> Self { + DesktopSize { width, height } + } +} + +pub fn iron_init(log_level: &str) { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "panic_hook")] + console_error_panic_hook::set_once(); + + if let Ok(level) = log_level.parse::() { + set_logger_once(level); + } +} + +fn set_logger_once(level: tracing::Level) { + use tracing_subscriber::filter::LevelFilter; + use tracing_subscriber::fmt::time::UtcTime; + use tracing_subscriber::prelude::*; + use tracing_web::MakeConsoleWriter; + + static INIT: std::sync::Once = std::sync::Once::new(); + + INIT.call_once(|| { + let fmt_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers + .with_writer(MakeConsoleWriter); + + let level_filter = LevelFilter::from_level(level); + + tracing_subscriber::registry().with(fmt_layer).with(level_filter).init(); + }) +} + +pub trait RemoteDesktopApi { + type Session: Session; + type SessionBuilder: SessionBuilder; + type SessionTerminationInfo: SessionTerminationInfo; + type DeviceEvent: DeviceEvent; + type InputTransaction: InputTransaction; + type ClipboardTransaction: ClipboardTransaction; + type ClipboardContent: ClipboardContent; + type Error: IronError; + + /// Called before the logger is set. + fn pre_init() {} + + /// Called after the logger is set. + fn post_init() {} +} + +#[macro_export] +macro_rules! export { + ($api:ty) => { + mod __wasm_ffi { + use wasm_bindgen::prelude::*; + use web_sys::{js_sys, HtmlCanvasElement}; + use $crate::{ + ClipboardContent as _, ClipboardTransaction as _, DeviceEvent as _, InputTransaction as _, + IronError as _, RemoteDesktopApi, Session as _, SessionBuilder as _, SessionTerminationInfo as _, + }; + + #[wasm_bindgen] + pub fn iron_init(log_level: &str) { + <$api as RemoteDesktopApi>::pre_init(); + $crate::iron_init(log_level); + <$api as RemoteDesktopApi>::post_init(); + } + + #[wasm_bindgen] + pub struct DeviceEvent(<$api as RemoteDesktopApi>::DeviceEvent); + + #[wasm_bindgen] + impl DeviceEvent { + pub fn mouse_button_pressed(button: u8) -> Self { + Self(<<$api as RemoteDesktopApi>::DeviceEvent>::mouse_button_pressed( + button, + )) + } + + pub fn mouse_button_released(button: u8) -> Self { + Self(<<$api as RemoteDesktopApi>::DeviceEvent>::mouse_button_released( + button, + )) + } + + pub fn mouse_move(x: u16, y: u16) -> Self { + Self(<<$api as RemoteDesktopApi>::DeviceEvent>::mouse_move(x, y)) + } + + pub fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self { + Self(<<$api as RemoteDesktopApi>::DeviceEvent>::wheel_rotations( + vertical, + rotation_units, + )) + } + + pub fn key_pressed(scancode: u16) -> Self { + Self(<<$api as RemoteDesktopApi>::DeviceEvent>::key_pressed(scancode)) + } + + pub fn key_released(scancode: u16) -> Self { + Self(<<$api as RemoteDesktopApi>::DeviceEvent>::key_released(scancode)) + } + + pub fn unicode_pressed(unicode: char) -> Self { + Self(<<$api as RemoteDesktopApi>::DeviceEvent>::unicode_pressed( + unicode, + )) + } + + pub fn unicode_released(unicode: char) -> Self { + Self(<<$api as RemoteDesktopApi>::DeviceEvent>::unicode_released( + unicode, + )) + } + } + + #[wasm_bindgen] + pub struct InputTransaction(<$api as RemoteDesktopApi>::InputTransaction); + + #[wasm_bindgen] + impl InputTransaction { + pub fn init() -> Self { + Self(<<$api as RemoteDesktopApi>::InputTransaction>::init()) + } + + pub fn add_event(&mut self, event: DeviceEvent) { + self.0.add_event(event.0); + } + } + + #[wasm_bindgen] + pub struct IronError(<$api as RemoteDesktopApi>::Error); + + #[wasm_bindgen] + impl IronError { + pub fn backtrace(&self) -> String { + self.0.backtrace() + } + + pub fn kind(&self) -> $crate::IronErrorKind { + self.0.kind() + } + } + + #[wasm_bindgen] + pub struct Session(<$api as RemoteDesktopApi>::Session); + + #[wasm_bindgen] + impl Session { + pub async fn run(&self) -> Result { + self.0.run().await.map(SessionTerminationInfo).map_err(IronError) + } + + pub fn desktop_size(&self) -> $crate::DesktopSize { + self.0.desktop_size() + } + + pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), IronError> { + self.0.apply_inputs(transaction.0).map_err(IronError) + } + + pub fn release_all_inputs(&self) -> Result<(), IronError> { + self.0.release_all_inputs().map_err(IronError) + } + + pub fn synchronize_lock_keys( + &self, + scroll_lock: bool, + num_lock: bool, + caps_lock: bool, + kana_lock: bool, + ) -> Result<(), IronError> { + self.0 + .synchronize_lock_keys(scroll_lock, num_lock, caps_lock, kana_lock) + .map_err(IronError) + } + + pub fn shutdown(&self) -> Result<(), IronError> { + self.0.shutdown().map_err(IronError) + } + + pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), IronError> { + self.0.on_clipboard_paste(content.0).await.map_err(IronError) + } + + pub fn resize( + &self, + width: u32, + height: u32, + scale_factor: Option, + physical_width: Option, + physical_height: Option, + ) { + self.0 + .resize(width, height, scale_factor, physical_width, physical_height); + } + + pub fn supports_unicode_keyboard_shortcuts(&self) -> bool { + self.0.supports_unicode_keyboard_shortcuts() + } + + pub fn extension_call(value: JsValue) -> Result { + <<$api as RemoteDesktopApi>::Session>::extension_call(value).map_err(IronError) + } + } + + #[wasm_bindgen] + pub struct SessionBuilder(<$api as RemoteDesktopApi>::SessionBuilder); + + #[wasm_bindgen] + impl SessionBuilder { + pub fn init() -> Self { + Self(<<$api as RemoteDesktopApi>::SessionBuilder>::init()) + } + + pub fn username(&self, username: String) -> Self { + Self(self.0.username(username)) + } + + pub fn destination(&self, destination: String) -> Self { + Self(self.0.destination(destination)) + } + + pub fn server_domain(&self, server_domain: String) -> Self { + Self(self.0.server_domain(server_domain)) + } + + pub fn password(&self, password: String) -> Self { + Self(self.0.password(password)) + } + + pub fn proxy_address(&self, address: String) -> Self { + Self(self.0.proxy_address(address)) + } + + pub fn auth_token(&self, token: String) -> Self { + Self(self.0.auth_token(token)) + } + + pub fn desktop_size(&self, desktop_size: $crate::DesktopSize) -> Self { + Self(self.0.desktop_size(desktop_size)) + } + + pub fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self { + Self(self.0.render_canvas(canvas)) + } + + pub fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self { + Self(self.0.set_cursor_style_callback(callback)) + } + + pub fn set_cursor_style_callback_context(&self, context: JsValue) -> Self { + Self(self.0.set_cursor_style_callback_context(context)) + } + + pub fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> Self { + Self(self.0.remote_clipboard_changed_callback(callback)) + } + + pub fn remote_received_format_list_callback(&self, callback: js_sys::Function) -> Self { + Self(self.0.remote_received_format_list_callback(callback)) + } + + pub fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self { + Self(self.0.force_clipboard_update_callback(callback)) + } + + pub fn extension(&self, value: JsValue) -> Self { + Self(self.0.extension(value)) + } + + pub async fn connect(&self) -> Result { + self.0.connect().await.map(Session).map_err(IronError) + } + } + + #[wasm_bindgen] + pub struct SessionTerminationInfo(<$api as RemoteDesktopApi>::SessionTerminationInfo); + + #[wasm_bindgen] + impl SessionTerminationInfo { + pub fn reason(&self) -> String { + self.0.reason() + } + } + + #[wasm_bindgen] + pub struct ClipboardTransaction(<$api as RemoteDesktopApi>::ClipboardTransaction); + + #[wasm_bindgen] + impl ClipboardTransaction { + pub fn init() -> Self { + Self(<<$api as RemoteDesktopApi>::ClipboardTransaction>::init()) + } + + pub fn add_content(&mut self, content: ClipboardContent) { + self.0.add_content(content.0); + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn content(&self) -> js_sys::Array { + iron_remote_desktop::ClipboardTransaction::contents(&self.0) + } + } + + #[wasm_bindgen] + pub struct ClipboardContent(<$api as RemoteDesktopApi>::ClipboardContent); + + #[wasm_bindgen] + impl ClipboardContent { + pub fn new_text(mime_type: &str, text: &str) -> Self { + Self(<<$api as RemoteDesktopApi>::ClipboardContent>::new_text( + mime_type, text, + )) + } + + pub fn new_binary(mime_type: &str, binary: &[u8]) -> Self { + Self(<<$api as RemoteDesktopApi>::ClipboardContent>::new_binary( + mime_type, binary, + )) + } + + pub fn mime_type(&self) -> String { + self.0.mime_type().to_owned() + } + + pub fn value(&self) -> JsValue { + iron_remote_desktop::ClipboardContent::value(&self.0) + } + } + } + }; +} diff --git a/crates/iron-remote-desktop/src/session.rs b/crates/iron-remote-desktop/src/session.rs new file mode 100644 index 00000000..65709513 --- /dev/null +++ b/crates/iron-remote-desktop/src/session.rs @@ -0,0 +1,82 @@ +use wasm_bindgen::JsValue; +use web_sys::{js_sys, HtmlCanvasElement}; + +use crate::clipboard::ClipboardTransaction; +use crate::error::IronError; +use crate::input::InputTransaction; +use crate::DesktopSize; + +pub trait SessionBuilder { + type Session: Session; + type Error: IronError; + + fn init() -> Self; + #[must_use] + fn username(&self, username: String) -> Self; + #[must_use] + fn destination(&self, destination: String) -> Self; + #[must_use] + fn server_domain(&self, server_domain: String) -> Self; + #[must_use] + fn password(&self, password: String) -> Self; + #[must_use] + fn proxy_address(&self, address: String) -> Self; + #[must_use] + fn auth_token(&self, token: String) -> Self; + #[must_use] + fn desktop_size(&self, desktop_size: DesktopSize) -> Self; + #[must_use] + fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self; + #[must_use] + fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self; + #[must_use] + fn set_cursor_style_callback_context(&self, context: JsValue) -> Self; + #[must_use] + fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> Self; + #[must_use] + fn remote_received_format_list_callback(&self, callback: js_sys::Function) -> Self; + #[must_use] + fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self; + #[must_use] + fn extension(&self, value: JsValue) -> Self; + #[expect(async_fn_in_trait)] + async fn connect(&self) -> Result; +} + +pub trait Session { + type SessionTerminationInfo: SessionTerminationInfo; + type InputTransaction: InputTransaction; + type ClipboardTransaction: ClipboardTransaction; + type Error: IronError; + + fn run(&self) -> impl core::future::Future>; + fn desktop_size(&self) -> DesktopSize; + fn apply_inputs(&self, transaction: Self::InputTransaction) -> Result<(), Self::Error>; + fn release_all_inputs(&self) -> Result<(), Self::Error>; + fn synchronize_lock_keys( + &self, + scroll_lock: bool, + num_lock: bool, + caps_lock: bool, + kana_lock: bool, + ) -> Result<(), Self::Error>; + fn shutdown(&self) -> Result<(), Self::Error>; + fn on_clipboard_paste( + &self, + content: Self::ClipboardTransaction, + ) -> impl core::future::Future>; + fn resize( + &self, + width: u32, + height: u32, + scale_factor: Option, + physical_width: Option, + physical_height: Option, + ); + fn supports_unicode_keyboard_shortcuts(&self) -> bool; + fn extension_call(value: JsValue) -> Result; +} + +pub trait SessionTerminationInfo { + fn reason(&self) -> String; +} diff --git a/crates/ironrdp-web/Cargo.toml b/crates/ironrdp-web/Cargo.toml index 8bb6e483..927db28e 100644 --- a/crates/ironrdp-web/Cargo.toml +++ b/crates/ironrdp-web/Cargo.toml @@ -19,7 +19,7 @@ crate-type = ["cdylib", "rlib"] [features] default = ["panic_hook"] -panic_hook = ["dep:console_error_panic_hook"] +panic_hook = ["iron-remote-desktop/panic_hook"] [dependencies] # Protocols @@ -37,6 +37,7 @@ ironrdp-core.path = "../ironrdp-core" ironrdp-cliprdr-format.path = "../ironrdp-cliprdr-format" ironrdp-futures.path = "../ironrdp-futures" ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" +iron-remote-desktop.path = "../iron-remote-desktop" # WASM wasm-bindgen = "0.2" @@ -47,7 +48,6 @@ gloo-net = { version = "0.6", default-features = false, features = ["websocket", gloo-timers = { version = "0.3", default-features = false, features = ["futures"] } serde = { version = "1.0", features = ["derive"] } serde-wasm-bindgen = "0.6" -tracing-web = "0.1" # Rendering softbuffer = { version = "0.4", default-features = false } @@ -60,19 +60,12 @@ getrandom = { version = "0.2", features = ["js"] } chrono = { version = "0.4", features = ["wasmbind"] } time = { version = "0.3", features = ["wasm-bindgen"] } -# The `console_error_panic_hook` crate provides better debugging of panics by -# logging them with `console.error`. This is great for development, but requires -# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for -# code size when deploying. -console_error_panic_hook = { version = "0.1", optional = true } - # Async futures-util = { version = "0.3", features = ["sink", "io"] } futures-channel = "0.3" # Logging tracing = { version = "0.1", features = ["log"] } -tracing-subscriber = { version = "0.3", features = ["time"] } # Utils anyhow = "1" diff --git a/crates/ironrdp-web/src/clipboard/mod.rs b/crates/ironrdp-web/src/clipboard/mod.rs index 965b8e3d..dbee02e8 100644 --- a/crates/ironrdp-web/src/clipboard/mod.rs +++ b/crates/ironrdp-web/src/clipboard/mod.rs @@ -16,6 +16,7 @@ mod transaction; use std::collections::HashMap; use futures_channel::mpsc; +use iron_remote_desktop::{ClipboardContent as _, ClipboardTransaction as _}; use ironrdp::cliprdr::backend::{ClipboardMessage, CliprdrBackend}; use ironrdp::cliprdr::pdu::{ ClipboardFormat, ClipboardFormatId, ClipboardFormatName, ClipboardGeneralCapabilityFlags, FileContentsRequest, @@ -24,13 +25,13 @@ use ironrdp::cliprdr::pdu::{ use ironrdp_cliprdr_format::bitmap::{dib_to_png, dibv5_to_png, png_to_cf_dibv5}; use ironrdp_cliprdr_format::html::{cf_html_to_plain_html, plain_html_to_cf_html}; use ironrdp_core::{impl_as_any, IntoOwned}; -use transaction::{ClipboardContent, ClipboardContentValue}; +use transaction::ClipboardContentValue; use wasm_bindgen::prelude::*; use crate::session::RdpInputEvent; #[rustfmt::skip] -pub(crate) use transaction::ClipboardTransaction; +pub(crate) use transaction::{RdpClipboardTransaction, RdpClipboardContent}; const MIME_TEXT: &str = "text/plain"; const MIME_HTML: &str = "text/html"; @@ -103,7 +104,7 @@ impl WasmClipboardMessageProxy { /// Messages sent by the JS code or CLIPRDR to the backend implementation. #[derive(Debug)] pub(crate) enum WasmClipboardBackendMessage { - LocalClipboardChanged(ClipboardTransaction), + LocalClipboardChanged(RdpClipboardTransaction), RemoteDataRequest(ClipboardFormatId), RemoteClipboardChanged(Vec), @@ -116,8 +117,8 @@ pub(crate) enum WasmClipboardBackendMessage { /// Clipboard backend implementation for web. This object should be created once per session and /// kept alive until session is terminated. pub(crate) struct WasmClipboard { - local_clipboard: Option, - remote_clipboard: ClipboardTransaction, + local_clipboard: Option, + remote_clipboard: RdpClipboardTransaction, remote_mapping: HashMap, remote_formats_to_read: Vec, @@ -137,7 +138,7 @@ impl WasmClipboard { pub(crate) fn new(message_proxy: WasmClipboardMessageProxy, js_callbacks: JsClipboardCallbacks) -> Self { Self { local_clipboard: None, - remote_clipboard: ClipboardTransaction::init(), + remote_clipboard: RdpClipboardTransaction::init(), proxy: message_proxy, js_callbacks, @@ -155,7 +156,7 @@ impl WasmClipboard { fn handle_local_clipboard_changed( &mut self, - transaction: ClipboardTransaction, + transaction: RdpClipboardTransaction, ) -> anyhow::Result> { let mut formats = Vec::new(); transaction.contents().iter().for_each(|content| { @@ -371,21 +372,21 @@ impl WasmClipboard { let content = match pending_format { ClipboardFormatId::CF_UNICODETEXT => match response.to_unicode_string() { - Ok(text) => Some(ClipboardContent::new_text(MIME_TEXT, &text)), + Ok(text) => Some(RdpClipboardContent::new_text(MIME_TEXT, &text)), Err(err) => { error!("CF_UNICODETEXT decode error: {}", err); None } }, ClipboardFormatId::CF_DIB => match dib_to_png(response.data()) { - Ok(png) => Some(ClipboardContent::new_binary(MIME_PNG, &png)), + Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)), Err(err) => { warn!("DIB decode error: {}", err); None } }, ClipboardFormatId::CF_DIBV5 => match dibv5_to_png(response.data()) { - Ok(png) => Some(ClipboardContent::new_binary(MIME_PNG, &png)), + Ok(png) => Some(RdpClipboardContent::new_binary(MIME_PNG, &png)), Err(err) => { warn!("DIBv5 decode error: {}", err); None @@ -395,21 +396,21 @@ impl WasmClipboard { let format_name = self.remote_mapping.get(®istered).map(|s| s.as_str()); match format_name { Some(FORMAT_WIN_HTML_NAME) => match cf_html_to_plain_html(response.data()) { - Ok(text) => Some(ClipboardContent::new_text(MIME_HTML, text)), + Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, text)), Err(err) => { warn!("CF_HTML decode error: {}", err); None } }, Some(FORMAT_MIME_HTML_NAME) => match response.to_string() { - Ok(text) => Some(ClipboardContent::new_text(MIME_HTML, &text)), + Ok(text) => Some(RdpClipboardContent::new_text(MIME_HTML, &text)), Err(err) => { warn!("text/html decode error: {}", err); None } }, Some(FORMAT_MIME_PNG_NAME) | Some(FORMAT_PNG_NAME) => { - Some(ClipboardContent::new_binary(MIME_PNG, response.data())) + Some(RdpClipboardContent::new_binary(MIME_PNG, response.data())) } _ => { // Not supported format @@ -433,11 +434,12 @@ impl WasmClipboard { if transaction.is_empty() { return Ok(()); } + // Set clipboard when all formats were read self.js_callbacks .on_remote_clipboard_changed .call1(&JsValue::NULL, &JsValue::from(transaction)) - .expect("Failed to call JS callback"); + .expect("failed to call JS callback"); } Ok(()) @@ -505,7 +507,7 @@ impl WasmClipboard { } else { // If no initial clipboard callback was set, send empty format list instead return self.process_event(WasmClipboardBackendMessage::LocalClipboardChanged( - ClipboardTransaction::init(), + RdpClipboardTransaction::init(), )); } } diff --git a/crates/ironrdp-web/src/clipboard/transaction.rs b/crates/ironrdp-web/src/clipboard/transaction.rs index 8bf83256..09073d3d 100644 --- a/crates/ironrdp-web/src/clipboard/transaction.rs +++ b/crates/ironrdp-web/src/clipboard/transaction.rs @@ -1,48 +1,49 @@ -use wasm_bindgen::prelude::*; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; /// Object which represents complete clipboard transaction with multiple MIME types. #[wasm_bindgen] #[derive(Debug, Default, Clone)] -pub struct ClipboardTransaction { - contents: Vec, +pub(crate) struct RdpClipboardTransaction { + contents: Vec, } -impl ClipboardTransaction { - pub fn contents(&self) -> &[ClipboardContent] { +impl RdpClipboardTransaction { + pub(crate) fn contents(&self) -> &[RdpClipboardContent] { &self.contents } - pub fn clear(&mut self) { + pub(crate) fn clear(&mut self) { self.contents.clear(); } } -#[wasm_bindgen] -impl ClipboardTransaction { - pub fn init() -> Self { +impl iron_remote_desktop::ClipboardTransaction for RdpClipboardTransaction { + type ClipboardContent = RdpClipboardContent; + + fn init() -> Self { Self { contents: Vec::new() } } - pub fn add_content(&mut self, content: ClipboardContent) { + fn add_content(&mut self, content: Self::ClipboardContent) { self.contents.push(content); } - pub fn is_empty(&self) -> bool { + fn is_empty(&self) -> bool { self.contents.is_empty() } - #[wasm_bindgen(js_name = content)] - pub fn js_contents(&self) -> js_sys::Array { + fn contents(&self) -> js_sys::Array { js_sys::Array::from_iter( self.contents .iter() - .map(|content: &ClipboardContent| JsValue::from(content.clone())), + .map(|content: &RdpClipboardContent| JsValue::from(content.clone())), ) } } -impl FromIterator for ClipboardTransaction { - fn from_iter>(iter: T) -> Self { +impl FromIterator for RdpClipboardTransaction { + fn from_iter>(iter: T) -> Self { Self { contents: iter.into_iter().collect(), } @@ -50,13 +51,13 @@ impl FromIterator for ClipboardTransaction { } #[derive(Debug, Clone)] -pub enum ClipboardContentValue { +pub(crate) enum ClipboardContentValue { Text(String), Binary(Vec), } impl ClipboardContentValue { - pub fn js_value(&self) -> JsValue { + pub(crate) fn value(&self) -> JsValue { match self { ClipboardContentValue::Text(text) => JsValue::from_str(text), ClipboardContentValue::Binary(binary) => js_sys::Uint8Array::from(binary.as_slice()).into(), @@ -67,44 +68,41 @@ impl ClipboardContentValue { /// Object which represents single clipboard format represented standard MIME type. #[wasm_bindgen] #[derive(Debug, Clone)] -pub struct ClipboardContent { +pub(crate) struct RdpClipboardContent { mime_type: String, value: ClipboardContentValue, } -#[wasm_bindgen] -impl ClipboardContent { - pub fn new_text(mime_type: &str, text: &str) -> Self { +impl RdpClipboardContent { + pub(crate) fn mime_type(&self) -> &str { + &self.mime_type + } + + pub(crate) fn value(&self) -> &ClipboardContentValue { + &self.value + } +} + +impl iron_remote_desktop::ClipboardContent for RdpClipboardContent { + fn new_text(mime_type: &str, text: &str) -> Self { Self { mime_type: mime_type.into(), value: ClipboardContentValue::Text(text.to_owned()), } } - pub fn new_binary(mime_type: &str, binary: &[u8]) -> Self { + fn new_binary(mime_type: &str, binary: &[u8]) -> Self { Self { mime_type: mime_type.into(), value: ClipboardContentValue::Binary(binary.to_vec()), } } - #[wasm_bindgen(js_name = mime_type)] - pub fn js_mime_type(&self) -> String { - self.mime_type.clone() + fn mime_type(&self) -> &str { + self.mime_type.as_str() } - #[wasm_bindgen(js_name = value)] - pub fn js_value(&self) -> JsValue { - self.value.js_value() - } -} - -impl ClipboardContent { - pub fn mime_type(&self) -> &str { - &self.mime_type - } - - pub fn value(&self) -> &ClipboardContentValue { - &self.value + fn value(&self) -> JsValue { + self.value.value() } } diff --git a/crates/ironrdp-web/src/error.rs b/crates/ironrdp-web/src/error.rs index 186381eb..927354b5 100644 --- a/crates/ironrdp-web/src/error.rs +++ b/crates/ironrdp-web/src/error.rs @@ -1,43 +1,24 @@ +use iron_remote_desktop::IronErrorKind; use ironrdp::connector::{self, sspi, ConnectorErrorKind}; -use wasm_bindgen::prelude::*; -#[wasm_bindgen] -#[derive(Clone, Copy)] -pub enum IronErrorKind { - /// Catch-all error kind - General, - /// Incorrect password used - WrongPassword, - /// Unable to login to machine - LogonFailure, - /// Insufficient permission, server denied access - AccessDenied, - /// Something wrong happened when sending or receiving the RDCleanPath message - RDCleanPath, - /// Couldn’t connect to proxy - ProxyConnect, -} - -#[wasm_bindgen] -pub struct IronError { +pub(crate) struct IronError { kind: IronErrorKind, source: anyhow::Error, } impl IronError { - pub fn with_kind(mut self, kind: IronErrorKind) -> Self { + pub(crate) fn with_kind(mut self, kind: IronErrorKind) -> Self { self.kind = kind; self } } -#[wasm_bindgen] -impl IronError { - pub fn backtrace(&self) -> String { +impl iron_remote_desktop::IronError for IronError { + fn backtrace(&self) -> String { format!("{:?}", self.source) } - pub fn kind(&self) -> IronErrorKind { + fn kind(&self) -> IronErrorKind { self.kind } } diff --git a/crates/ironrdp-web/src/input.rs b/crates/ironrdp-web/src/input.rs index 0f5585bb..9b3539a8 100644 --- a/crates/ironrdp-web/src/input.rs +++ b/crates/ironrdp-web/src/input.rs @@ -1,14 +1,11 @@ use ironrdp::input::{MouseButton, MousePosition, Operation, Scancode, WheelRotations}; use smallvec::SmallVec; -use wasm_bindgen::prelude::*; -#[wasm_bindgen] #[derive(Clone)] -pub struct DeviceEvent(pub(crate) Operation); +pub(crate) struct DeviceEvent(pub(crate) Operation); -#[wasm_bindgen] -impl DeviceEvent { - pub fn mouse_button_pressed(button: u8) -> Self { +impl iron_remote_desktop::DeviceEvent for DeviceEvent { + fn mouse_button_pressed(button: u8) -> Self { match MouseButton::from_web_button(button) { Some(button) => Self(Operation::MouseButtonPressed(button)), None => { @@ -18,7 +15,7 @@ impl DeviceEvent { } } - pub fn mouse_button_released(button: u8) -> Self { + fn mouse_button_released(button: u8) -> Self { match MouseButton::from_web_button(button) { Some(button) => Self(Operation::MouseButtonReleased(button)), None => { @@ -28,44 +25,44 @@ impl DeviceEvent { } } - pub fn mouse_move(x: u16, y: u16) -> Self { + fn mouse_move(x: u16, y: u16) -> Self { Self(Operation::MouseMove(MousePosition { x, y })) } - pub fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self { + fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self { Self(Operation::WheelRotations(WheelRotations { is_vertical: vertical, rotation_units, })) } - pub fn key_pressed(scancode: u16) -> Self { + fn key_pressed(scancode: u16) -> Self { Self(Operation::KeyPressed(Scancode::from_u16(scancode))) } - pub fn key_released(scancode: u16) -> Self { + fn key_released(scancode: u16) -> Self { Self(Operation::KeyReleased(Scancode::from_u16(scancode))) } - pub fn unicode_pressed(unicode: char) -> Self { + fn unicode_pressed(unicode: char) -> Self { Self(Operation::UnicodeKeyPressed(unicode)) } - pub fn unicode_released(unicode: char) -> Self { + fn unicode_released(unicode: char) -> Self { Self(Operation::UnicodeKeyReleased(unicode)) } } -#[wasm_bindgen] -pub struct InputTransaction(pub(crate) SmallVec<[Operation; 3]>); +pub(crate) struct InputTransaction(pub(crate) SmallVec<[Operation; 3]>); -#[wasm_bindgen] -impl InputTransaction { - pub fn init() -> Self { +impl iron_remote_desktop::InputTransaction for InputTransaction { + type DeviceEvent = DeviceEvent; + + fn init() -> Self { Self(SmallVec::new()) } - pub fn add_event(&mut self, event: DeviceEvent) { + fn add_event(&mut self, event: Self::DeviceEvent) { self.0.push(event.0); } } diff --git a/crates/ironrdp-web/src/lib.rs b/crates/ironrdp-web/src/lib.rs index 01dd6a4a..5c5c17df 100644 --- a/crates/ironrdp-web/src/lib.rs +++ b/crates/ironrdp-web/src/lib.rs @@ -12,6 +12,8 @@ extern crate time as _; #[macro_use] extern crate tracing; +use iron_remote_desktop::RemoteDesktopApi; + mod canvas; mod clipboard; mod error; @@ -20,56 +22,27 @@ mod input; mod network_client; mod session; -use wasm_bindgen::prelude::*; +struct Api; -#[wasm_bindgen] -pub fn iron_init(log_level: &str) { - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function at least once during initialization, and then - // we will get better error messages if our code ever panics. - // - // For more details see - // https://github.com/rustwasm/console_error_panic_hook#readme - #[cfg(feature = "panic_hook")] - console_error_panic_hook::set_once(); - - if let Ok(level) = log_level.parse::() { - set_logger_once(level); - } +impl RemoteDesktopApi for Api { + type Session = session::Session; + type SessionBuilder = session::SessionBuilder; + type SessionTerminationInfo = session::SessionTerminationInfo; + type DeviceEvent = input::DeviceEvent; + type InputTransaction = input::InputTransaction; + type ClipboardTransaction = clipboard::RdpClipboardTransaction; + type ClipboardContent = clipboard::RdpClipboardContent; + type Error = error::IronError; } -fn set_logger_once(level: tracing::Level) { - use tracing_subscriber::filter::LevelFilter; - use tracing_subscriber::fmt::time::UtcTime; - use tracing_subscriber::prelude::*; - use tracing_web::MakeConsoleWriter; - - static INIT: std::sync::Once = std::sync::Once::new(); - - INIT.call_once(|| { - let fmt_layer = tracing_subscriber::fmt::layer() - .with_ansi(false) - .with_timer(UtcTime::rfc_3339()) // std::time is not available in browsers - .with_writer(MakeConsoleWriter); - - let level_filter = LevelFilter::from_level(level); - - tracing_subscriber::registry().with(fmt_layer).with(level_filter).init(); +#[doc(hidden)] +pub mod internal { + #[allow(dead_code)] + fn iron_init(log_level: &str) { + iron_remote_desktop::iron_init(log_level); debug!("IronRDP is ready"); - }) -} - -#[wasm_bindgen] -#[derive(Clone)] -pub struct DesktopSize { - pub width: u16, - pub height: u16, -} - -#[wasm_bindgen] -impl DesktopSize { - pub fn init(width: u16, height: u16) -> Self { - DesktopSize { width, height } } } + +iron_remote_desktop::export!(crate::Api); diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index e0c8aa76..d0230c3b 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -14,6 +14,7 @@ use futures_util::io::{ReadHalf, WriteHalf}; use futures_util::{select, AsyncWriteExt as _, FutureExt as _, StreamExt as _}; use gloo_net::websocket; use gloo_net::websocket::futures::WebSocket; +use iron_remote_desktop::{CursorStyle, DesktopSize, IronErrorKind}; use ironrdp::cliprdr::backend::ClipboardMessage; use ironrdp::cliprdr::CliprdrClient; use ironrdp::connector::connection_activation::ConnectionActivationState; @@ -31,24 +32,23 @@ use ironrdp_futures::{single_sequence_step_read, FramedWrite}; use rgb::AsPixels as _; use serde::{Deserialize, Serialize}; use tap::prelude::*; -use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; use wasm_bindgen_futures::spawn_local; use web_sys::HtmlCanvasElement; use crate::canvas::Canvas; -use crate::clipboard::{ClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage}; -use crate::error::{IronError, IronErrorKind}; +use crate::clipboard; +use crate::clipboard::{RdpClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage}; +use crate::error::IronError; use crate::image::extract_partial_image; use crate::input::InputTransaction; use crate::network_client::WasmNetworkClient; -use crate::{clipboard, DesktopSize}; const DEFAULT_WIDTH: u16 = 1280; const DEFAULT_HEIGHT: u16 = 720; -#[wasm_bindgen] #[derive(Clone, Default)] -pub struct SessionBuilder(Rc>); +pub(crate) struct SessionBuilder(Rc>); struct SessionBuilderInner { username: Option, @@ -101,26 +101,28 @@ impl Default for SessionBuilderInner { } } -#[wasm_bindgen] -impl SessionBuilder { - pub fn init() -> SessionBuilder { +impl iron_remote_desktop::SessionBuilder for SessionBuilder { + type Session = Session; + type Error = IronError; + + fn init() -> Self { Self(Rc::new(RefCell::new(SessionBuilderInner::default()))) } /// Required - pub fn username(&self, username: String) -> SessionBuilder { + fn username(&self, username: String) -> Self { self.0.borrow_mut().username = Some(username); self.clone() } /// Required - pub fn destination(&self, destination: String) -> SessionBuilder { + fn destination(&self, destination: String) -> Self { self.0.borrow_mut().destination = Some(destination); self.clone() } /// Optional - pub fn server_domain(&self, server_domain: String) -> SessionBuilder { + fn server_domain(&self, server_domain: String) -> Self { self.0.borrow_mut().server_domain = if server_domain.is_empty() { None } else { @@ -130,31 +132,31 @@ impl SessionBuilder { } /// Required - pub fn password(&self, password: String) -> SessionBuilder { + fn password(&self, password: String) -> Self { self.0.borrow_mut().password = Some(password); self.clone() } /// Required - pub fn proxy_address(&self, address: String) -> SessionBuilder { + fn proxy_address(&self, address: String) -> Self { self.0.borrow_mut().proxy_address = Some(address); self.clone() } /// Required - pub fn auth_token(&self, token: String) -> SessionBuilder { + fn auth_token(&self, token: String) -> Self { self.0.borrow_mut().auth_token = Some(token); self.clone() } /// Optional - pub fn desktop_size(&self, desktop_size: DesktopSize) -> SessionBuilder { + fn desktop_size(&self, desktop_size: DesktopSize) -> Self { self.0.borrow_mut().desktop_size = desktop_size; self.clone() } /// Optional - pub fn render_canvas(&self, canvas: HtmlCanvasElement) -> SessionBuilder { + fn render_canvas(&self, canvas: HtmlCanvasElement) -> Self { self.0.borrow_mut().render_canvas = Some(canvas); self.clone() } @@ -176,36 +178,36 @@ impl SessionBuilder { /// - `none` (hide cursor); other arguments are `UNDEFINED` /// - `url` (custom cursor data URL); `cursor_data` contains the data URL with Base64-encoded /// cursor bitmap; `hotspot_x` and `hotspot_y` are set to the cursor hotspot coordinates. - pub fn set_cursor_style_callback(&self, callback: js_sys::Function) -> SessionBuilder { + fn set_cursor_style_callback(&self, callback: js_sys::Function) -> Self { self.0.borrow_mut().set_cursor_style_callback = Some(callback); self.clone() } /// Required. - pub fn set_cursor_style_callback_context(&self, context: JsValue) -> SessionBuilder { + fn set_cursor_style_callback_context(&self, context: JsValue) -> Self { self.0.borrow_mut().set_cursor_style_callback_context = Some(context); self.clone() } /// Optional - pub fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> SessionBuilder { + fn remote_clipboard_changed_callback(&self, callback: js_sys::Function) -> Self { self.0.borrow_mut().remote_clipboard_changed_callback = Some(callback); self.clone() } /// Optional - pub fn remote_received_format_list_callback(&self, callback: js_sys::Function) -> SessionBuilder { + fn remote_received_format_list_callback(&self, callback: js_sys::Function) -> Self { self.0.borrow_mut().remote_received_format_list_callback = Some(callback); self.clone() } /// Optional - pub fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> SessionBuilder { + fn force_clipboard_update_callback(&self, callback: js_sys::Function) -> Self { self.0.borrow_mut().force_clipboard_update_callback = Some(callback); self.clone() } - pub fn extension(&self, value: JsValue) -> SessionBuilder { + fn extension(&self, value: JsValue) -> Self { match serde_wasm_bindgen::from_value::(value) { Ok(value) => match value { Extension::KdcProxyUrl(kdc_proxy_url) => self.0.borrow_mut().kdc_proxy_url = Some(kdc_proxy_url), @@ -220,7 +222,7 @@ impl SessionBuilder { self.clone() } - pub async fn connect(&self) -> Result { + async fn connect(&self) -> Result { let ( username, destination, @@ -252,7 +254,7 @@ impl SessionBuilder { pcb = inner.pcb.clone(); kdc_proxy_url = inner.kdc_proxy_url.clone(); client_name = inner.client_name.clone(); - desktop_size = inner.desktop_size.clone(); + desktop_size = inner.desktop_size; render_canvas = inner.render_canvas.clone().context("render_canvas missing")?; @@ -375,30 +377,17 @@ pub(crate) enum RdpInputEvent { TerminateSession, } -enum CursorStyle { - Default, - Hidden, - Url { - data: String, - hotspot_x: u16, - hotspot_y: u16, - }, -} - -#[wasm_bindgen] -pub struct SessionTerminationInfo { +pub(crate) struct SessionTerminationInfo { reason: GracefulDisconnectReason, } -#[wasm_bindgen] -impl SessionTerminationInfo { - pub fn reason(&self) -> String { +impl iron_remote_desktop::SessionTerminationInfo for SessionTerminationInfo { + fn reason(&self) -> String { self.reason.to_string() } } -#[wasm_bindgen] -pub struct Session { +pub(crate) struct Session { desktop_size: connector::DesktopSize, input_database: RefCell, writer_tx: mpsc::UnboundedSender>, @@ -415,9 +404,53 @@ pub struct Session { clipboard: RefCell>>, } -#[wasm_bindgen] impl Session { - pub async fn run(&self) -> Result { + fn h_send_inputs(&self, inputs: smallvec::SmallVec<[FastPathInputEvent; 2]>) -> Result<(), IronError> { + if !inputs.is_empty() { + trace!("Inputs: {inputs:?}"); + + self.input_events_tx + .unbounded_send(RdpInputEvent::FastPath(inputs)) + .context("Send input events to writer task")?; + } + + Ok(()) + } + + fn set_cursor_style(&self, style: CursorStyle) -> Result<(), IronError> { + let (kind, data, hotspot_x, hotspot_y) = match style { + CursorStyle::Default => ("default", None, None, None), + CursorStyle::Hidden => ("hidden", None, None, None), + CursorStyle::Url { + data, + hotspot_x, + hotspot_y, + } => ("url", Some(data), Some(hotspot_x), Some(hotspot_y)), + }; + + let args = js_sys::Array::from_iter([ + JsValue::from_str(kind), + JsValue::from(data), + JsValue::from_f64(hotspot_x.unwrap_or_default().into()), + JsValue::from_f64(hotspot_y.unwrap_or_default().into()), + ]); + + let _ret = self + .set_cursor_style_callback + .apply(&self.set_cursor_style_callback_context, &args) + .map_err(|e| anyhow::Error::msg(format!("set cursor style callback failed: {e:?}")))?; + + Ok(()) + } +} + +impl iron_remote_desktop::Session for Session { + type SessionTerminationInfo = SessionTerminationInfo; + type InputTransaction = InputTransaction; + type ClipboardTransaction = RdpClipboardTransaction; + type Error = IronError; + + async fn run(&self) -> Result { let rdp_reader = self .rdp_reader .borrow_mut() @@ -705,42 +738,30 @@ impl Session { }) } - pub fn desktop_size(&self) -> DesktopSize { + fn desktop_size(&self) -> DesktopSize { DesktopSize { width: self.desktop_size.width, height: self.desktop_size.height, } } - pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), IronError> { + fn apply_inputs(&self, transaction: Self::InputTransaction) -> Result<(), Self::Error> { let inputs = self.input_database.borrow_mut().apply(transaction); self.h_send_inputs(inputs) } - pub fn release_all_inputs(&self) -> Result<(), IronError> { + fn release_all_inputs(&self) -> Result<(), Self::Error> { let inputs = self.input_database.borrow_mut().release_all(); self.h_send_inputs(inputs) } - fn h_send_inputs(&self, inputs: smallvec::SmallVec<[FastPathInputEvent; 2]>) -> Result<(), IronError> { - if !inputs.is_empty() { - trace!("Inputs: {inputs:?}"); - - self.input_events_tx - .unbounded_send(RdpInputEvent::FastPath(inputs)) - .context("Send input events to writer task")?; - } - - Ok(()) - } - - pub fn synchronize_lock_keys( + fn synchronize_lock_keys( &self, scroll_lock: bool, num_lock: bool, caps_lock: bool, kana_lock: bool, - ) -> Result<(), IronError> { + ) -> Result<(), Self::Error> { use ironrdp::pdu::input::fast_path::FastPathInput; let event = ironrdp::input::synchronize_event(scroll_lock, num_lock, caps_lock, kana_lock); @@ -755,7 +776,7 @@ impl Session { Ok(()) } - pub fn shutdown(&self) -> Result<(), IronError> { + fn shutdown(&self) -> Result<(), Self::Error> { self.input_events_tx .unbounded_send(RdpInputEvent::TerminateSession) .context("failed to send terminate session event to writer task")?; @@ -763,7 +784,7 @@ impl Session { Ok(()) } - pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), IronError> { + async fn on_clipboard_paste(&self, content: Self::ClipboardTransaction) -> Result<(), Self::Error> { self.input_events_tx .unbounded_send(RdpInputEvent::ClipboardBackend( WasmClipboardBackendMessage::LocalClipboardChanged(content), @@ -773,33 +794,7 @@ impl Session { Ok(()) } - fn set_cursor_style(&self, style: CursorStyle) -> Result<(), IronError> { - let (kind, data, hotspot_x, hotspot_y) = match style { - CursorStyle::Default => ("default", None, None, None), - CursorStyle::Hidden => ("hidden", None, None, None), - CursorStyle::Url { - data, - hotspot_x, - hotspot_y, - } => ("url", Some(data), Some(hotspot_x), Some(hotspot_y)), - }; - - let args = js_sys::Array::from_iter([ - JsValue::from_str(kind), - JsValue::from(data), - JsValue::from_f64(hotspot_x.unwrap_or_default().into()), - JsValue::from_f64(hotspot_y.unwrap_or_default().into()), - ]); - - let _ret = self - .set_cursor_style_callback - .apply(&self.set_cursor_style_callback_context, &args) - .map_err(|e| anyhow::Error::msg(format!("set cursor style callback failed: {e:?}")))?; - - Ok(()) - } - - pub fn resize( + fn resize( &self, width: u32, height: u32, @@ -817,14 +812,13 @@ impl Session { .expect("send resize event to writer task"); } - #[allow(clippy::unused_self)] - pub fn supports_unicode_keyboard_shortcuts(&self) -> bool { + fn supports_unicode_keyboard_shortcuts(&self) -> bool { // RDP does not support Unicode keyboard shortcuts (When key combinations are executed, only // plain scancode events are allowed to function correctly). false } - pub fn extension_call(_value: JsValue) -> Result { + fn extension_call(_value: JsValue) -> Result { Ok(JsValue::null()) } } diff --git a/web-client/iron-remote-desktop-rdp/package.json b/web-client/iron-remote-desktop-rdp/package.json index 18a969ac..88a08831 100644 --- a/web-client/iron-remote-desktop-rdp/package.json +++ b/web-client/iron-remote-desktop-rdp/package.json @@ -9,7 +9,7 @@ "Zacharia Ellaham", "Alexandr Yusuk" ], - "description": "Web Component providing agnostic implementation for Iron Wasm base client", + "description": "RDP backend for iron-remote-desktop.", "version": "0.0.0", "type": "module", "private": true, diff --git a/web-client/iron-remote-desktop-rdp/public/package.json b/web-client/iron-remote-desktop-rdp/public/package.json index 650c34d2..0c39fead 100644 --- a/web-client/iron-remote-desktop-rdp/public/package.json +++ b/web-client/iron-remote-desktop-rdp/public/package.json @@ -8,7 +8,7 @@ "Vladislav Nikonov", "Zacharia Ellaham" ], - "description": "Web Component providing agnostic implementation for Iron Wasm base client.", + "description": "RDP backend for iron-remote-desktop.", "version": "0.13.1", "main": "iron-remote-desktop-rdp.js", "types": "index.d.ts",