mirror of
https://github.com/netbirdio/IronRDP.git
synced 2026-05-22 18:43:12 -07:00
feat(ironrdp-web): iron-remote-desktop helper crate for remote desktop WASM modules (#755)
This commit is contained in:
Generated
+13
-3
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#[derive(Debug)]
|
||||
pub enum CursorStyle {
|
||||
Default,
|
||||
Hidden,
|
||||
Url {
|
||||
data: String,
|
||||
hotspot_x: u16,
|
||||
hotspot_y: u16,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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::<tracing::Level>() {
|
||||
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<SessionTerminationInfo, IronError> {
|
||||
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<u32>,
|
||||
physical_width: Option<u32>,
|
||||
physical_height: Option<u32>,
|
||||
) {
|
||||
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<JsValue, IronError> {
|
||||
<<$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<Session, IronError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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<Self::Session, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait Session {
|
||||
type SessionTerminationInfo: SessionTerminationInfo;
|
||||
type InputTransaction: InputTransaction;
|
||||
type ClipboardTransaction: ClipboardTransaction;
|
||||
type Error: IronError;
|
||||
|
||||
fn run(&self) -> impl core::future::Future<Output = Result<Self::SessionTerminationInfo, Self::Error>>;
|
||||
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<Output = Result<(), Self::Error>>;
|
||||
fn resize(
|
||||
&self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale_factor: Option<u32>,
|
||||
physical_width: Option<u32>,
|
||||
physical_height: Option<u32>,
|
||||
);
|
||||
fn supports_unicode_keyboard_shortcuts(&self) -> bool;
|
||||
fn extension_call(value: JsValue) -> Result<JsValue, Self::Error>;
|
||||
}
|
||||
|
||||
pub trait SessionTerminationInfo {
|
||||
fn reason(&self) -> String;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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<ClipboardFormat>),
|
||||
@@ -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<ClipboardTransaction>,
|
||||
remote_clipboard: ClipboardTransaction,
|
||||
local_clipboard: Option<RdpClipboardTransaction>,
|
||||
remote_clipboard: RdpClipboardTransaction,
|
||||
|
||||
remote_mapping: HashMap<ClipboardFormatId, String>,
|
||||
remote_formats_to_read: Vec<ClipboardFormatId>,
|
||||
@@ -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<Vec<ClipboardFormat>> {
|
||||
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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ClipboardContent>,
|
||||
pub(crate) struct RdpClipboardTransaction {
|
||||
contents: Vec<RdpClipboardContent>,
|
||||
}
|
||||
|
||||
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<ClipboardContent> for ClipboardTransaction {
|
||||
fn from_iter<T: IntoIterator<Item = ClipboardContent>>(iter: T) -> Self {
|
||||
impl FromIterator<RdpClipboardContent> for RdpClipboardTransaction {
|
||||
fn from_iter<T: IntoIterator<Item = RdpClipboardContent>>(iter: T) -> Self {
|
||||
Self {
|
||||
contents: iter.into_iter().collect(),
|
||||
}
|
||||
@@ -50,13 +51,13 @@ impl FromIterator<ClipboardContent> for ClipboardTransaction {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClipboardContentValue {
|
||||
pub(crate) enum ClipboardContentValue {
|
||||
Text(String),
|
||||
Binary(Vec<u8>),
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<tracing::Level>() {
|
||||
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);
|
||||
|
||||
@@ -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<RefCell<SessionBuilderInner>>);
|
||||
pub(crate) struct SessionBuilder(Rc<RefCell<SessionBuilderInner>>);
|
||||
|
||||
struct SessionBuilderInner {
|
||||
username: Option<String>,
|
||||
@@ -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::<Extension>(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<Session, IronError> {
|
||||
async fn connect(&self) -> Result<Self::Session, Self::Error> {
|
||||
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<ironrdp::input::Database>,
|
||||
writer_tx: mpsc::UnboundedSender<Vec<u8>>,
|
||||
@@ -415,9 +404,53 @@ pub struct Session {
|
||||
clipboard: RefCell<Option<Option<WasmClipboard>>>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Session {
|
||||
pub async fn run(&self) -> Result<SessionTerminationInfo, IronError> {
|
||||
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<Self::SessionTerminationInfo, Self::Error> {
|
||||
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<JsValue, IronError> {
|
||||
fn extension_call(_value: JsValue) -> Result<JsValue, Self::Error> {
|
||||
Ok(JsValue::null())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user