refactor(web-client): refactor iron-remote-gui into iron-remote-desktop (#722)

This commit is contained in:
Alex Yusiuk
2025-04-11 08:28:27 -04:00
committed by GitHub
parent 184cfd24ae
commit 0ff1ed8de5
86 changed files with 4950 additions and 330 deletions
+7 -1
View File
@@ -168,12 +168,18 @@ WebAssembly high-level bindings targeting web browsers.
This crate is an **API Boundary** (WASM module).
#### [`web-client/iron-remote-gui`](./web-client/iron-remote-gui)
#### [`web-client/iron-remote-desktop`](./web-client/iron-remote-desktop)
Core frontend UI used by `iron-svelte-client` as a Web Component.
This crate is an **API Boundary**.
#### [`web-client/iron-remote-desktop-rdp`](./web-client/iron-remote-desktop-rdp)
Implementation of the TypeScript interfaces exposed by WebAssembly bindings from `ironrdp-web` and used by `iron-svelte-client`.
This crate is an **API Boundary**.
#### [`web-client/iron-svelte-client`](./web-client/iron-svelte-client)
Web-based frontend using `Svelte` and `Material` frameworks.
Generated
+13
View File
@@ -2809,6 +2809,8 @@ dependencies = [
"resize",
"rgb",
"semver",
"serde",
"serde-wasm-bindgen",
"smallvec",
"softbuffer",
"tap",
@@ -4668,6 +4670,17 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_bytes"
version = "0.11.17"
+2
View File
@@ -45,6 +45,8 @@ web-sys = { version = "0.3", features = ["HtmlCanvasElement"] }
js-sys = "0.3"
gloo-net = { version = "0.6", default-features = false, features = ["websocket", "http", "io-util"] }
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
+2 -2
View File
@@ -137,7 +137,7 @@ impl WasmClipboard {
pub(crate) fn new(message_proxy: WasmClipboardMessageProxy, js_callbacks: JsClipboardCallbacks) -> Self {
Self {
local_clipboard: None,
remote_clipboard: ClipboardTransaction::new(),
remote_clipboard: ClipboardTransaction::construct(),
proxy: message_proxy,
js_callbacks,
@@ -505,7 +505,7 @@ impl WasmClipboard {
} else {
// If no initial clipboard callback was set, send empty format list instead
return self.process_event(WasmClipboardBackendMessage::LocalClipboardChanged(
ClipboardTransaction::new(),
ClipboardTransaction::construct(),
));
}
}
@@ -19,7 +19,7 @@ impl ClipboardTransaction {
#[wasm_bindgen]
impl ClipboardTransaction {
pub fn new() -> Self {
pub fn construct() -> Self {
Self { contents: Vec::new() }
}
+16 -16
View File
@@ -3,7 +3,7 @@ use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub enum IronRdpErrorKind {
pub enum RemoteDesktopErrorKind {
/// Catch-all error kind
General,
/// Incorrect password used
@@ -19,30 +19,30 @@ pub enum IronRdpErrorKind {
}
#[wasm_bindgen]
pub struct IronRdpError {
kind: IronRdpErrorKind,
pub struct RemoteDesktopError {
kind: RemoteDesktopErrorKind,
source: anyhow::Error,
}
impl IronRdpError {
pub fn with_kind(mut self, kind: IronRdpErrorKind) -> Self {
impl RemoteDesktopError {
pub fn with_kind(mut self, kind: RemoteDesktopErrorKind) -> Self {
self.kind = kind;
self
}
}
#[wasm_bindgen]
impl IronRdpError {
impl RemoteDesktopError {
pub fn backtrace(&self) -> String {
format!("{:?}", self.source)
}
pub fn kind(&self) -> IronRdpErrorKind {
pub fn kind(&self) -> RemoteDesktopErrorKind {
self.kind
}
}
impl From<connector::ConnectorError> for IronRdpError {
impl From<connector::ConnectorError> for RemoteDesktopError {
fn from(e: connector::ConnectorError) -> Self {
use sspi::credssp::NStatusCode;
@@ -50,13 +50,13 @@ impl From<connector::ConnectorError> for IronRdpError {
ConnectorErrorKind::Credssp(sspi::Error {
nstatus: Some(NStatusCode::WRONG_PASSWORD),
..
}) => IronRdpErrorKind::WrongPassword,
}) => RemoteDesktopErrorKind::WrongPassword,
ConnectorErrorKind::Credssp(sspi::Error {
nstatus: Some(NStatusCode::LOGON_FAILURE),
..
}) => IronRdpErrorKind::LogonFailure,
ConnectorErrorKind::AccessDenied => IronRdpErrorKind::AccessDenied,
_ => IronRdpErrorKind::General,
}) => RemoteDesktopErrorKind::LogonFailure,
ConnectorErrorKind::AccessDenied => RemoteDesktopErrorKind::AccessDenied,
_ => RemoteDesktopErrorKind::General,
};
Self {
@@ -66,19 +66,19 @@ impl From<connector::ConnectorError> for IronRdpError {
}
}
impl From<ironrdp::session::SessionError> for IronRdpError {
impl From<ironrdp::session::SessionError> for RemoteDesktopError {
fn from(e: ironrdp::session::SessionError) -> Self {
Self {
kind: IronRdpErrorKind::General,
kind: RemoteDesktopErrorKind::General,
source: anyhow::Error::new(e),
}
}
}
impl From<anyhow::Error> for IronRdpError {
impl From<anyhow::Error> for RemoteDesktopError {
fn from(e: anyhow::Error) -> Self {
Self {
kind: IronRdpErrorKind::General,
kind: RemoteDesktopErrorKind::General,
source: e,
}
}
+9 -9
View File
@@ -8,7 +8,7 @@ pub struct DeviceEvent(pub(crate) Operation);
#[wasm_bindgen]
impl DeviceEvent {
pub fn new_mouse_button_pressed(button: u8) -> Self {
pub fn mouse_button_pressed(button: u8) -> Self {
match MouseButton::from_web_button(button) {
Some(button) => Self(Operation::MouseButtonPressed(button)),
None => {
@@ -18,7 +18,7 @@ impl DeviceEvent {
}
}
pub fn new_mouse_button_released(button: u8) -> Self {
pub fn mouse_button_released(button: u8) -> Self {
match MouseButton::from_web_button(button) {
Some(button) => Self(Operation::MouseButtonReleased(button)),
None => {
@@ -28,30 +28,30 @@ impl DeviceEvent {
}
}
pub fn new_mouse_move(x: u16, y: u16) -> Self {
pub fn mouse_move(x: u16, y: u16) -> Self {
Self(Operation::MouseMove(MousePosition { x, y }))
}
pub fn new_wheel_rotations(vertical: bool, rotation_units: i16) -> Self {
pub fn wheel_rotations(vertical: bool, rotation_units: i16) -> Self {
Self(Operation::WheelRotations(WheelRotations {
is_vertical: vertical,
rotation_units,
}))
}
pub fn new_key_pressed(scancode: u16) -> Self {
pub fn key_pressed(scancode: u16) -> Self {
Self(Operation::KeyPressed(Scancode::from_u16(scancode)))
}
pub fn new_key_released(scancode: u16) -> Self {
pub fn key_released(scancode: u16) -> Self {
Self(Operation::KeyReleased(Scancode::from_u16(scancode)))
}
pub fn new_unicode_pressed(unicode: char) -> Self {
pub fn unicode_pressed(unicode: char) -> Self {
Self(Operation::UnicodeKeyPressed(unicode))
}
pub fn new_unicode_released(unicode: char) -> Self {
pub fn unicode_released(unicode: char) -> Self {
Self(Operation::UnicodeKeyReleased(unicode))
}
}
@@ -61,7 +61,7 @@ pub struct InputTransaction(pub(crate) SmallVec<[Operation; 3]>);
#[wasm_bindgen]
impl InputTransaction {
pub fn new() -> Self {
pub fn construct() -> Self {
Self(SmallVec::new())
}
+2 -2
View File
@@ -23,7 +23,7 @@ mod session;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn ironrdp_init(log_level: &str) {
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.
@@ -69,7 +69,7 @@ pub struct DesktopSize {
#[wasm_bindgen]
impl DesktopSize {
pub fn new(width: u16, height: u16) -> Self {
pub fn construct(width: u16, height: u16) -> Self {
DesktopSize { width, height }
}
}
+44 -35
View File
@@ -29,6 +29,7 @@ use ironrdp::session::{fast_path, ActiveStage, ActiveStageOutput, GracefulDiscon
use ironrdp_core::WriteBuf;
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_futures::spawn_local;
@@ -36,7 +37,7 @@ use web_sys::HtmlCanvasElement;
use crate::canvas::Canvas;
use crate::clipboard::{ClipboardTransaction, WasmClipboard, WasmClipboardBackend, WasmClipboardBackendMessage};
use crate::error::{IronRdpError, IronRdpErrorKind};
use crate::error::{RemoteDesktopError, RemoteDesktopErrorKind};
use crate::image::extract_partial_image;
use crate::input::InputTransaction;
use crate::network_client::WasmNetworkClient;
@@ -102,7 +103,7 @@ impl Default for SessionBuilderInner {
#[wasm_bindgen]
impl SessionBuilder {
pub fn new() -> SessionBuilder {
pub fn construct() -> SessionBuilder {
Self(Rc::new(RefCell::new(SessionBuilderInner::default())))
}
@@ -146,18 +147,6 @@ impl SessionBuilder {
self.clone()
}
/// Optional
pub fn pcb(&self, pcb: String) -> SessionBuilder {
self.0.borrow_mut().pcb = Some(pcb);
self.clone()
}
/// Optional
pub fn kdc_proxy_url(&self, kdc_proxy_url: Option<String>) -> SessionBuilder {
self.0.borrow_mut().kdc_proxy_url = kdc_proxy_url;
self.clone()
}
/// Optional
pub fn desktop_size(&self, desktop_size: DesktopSize) -> SessionBuilder {
self.0.borrow_mut().desktop_size = desktop_size;
@@ -216,13 +205,22 @@ impl SessionBuilder {
self.clone()
}
/// Optional
pub fn use_display_control(&self) -> SessionBuilder {
self.0.borrow_mut().use_display_control = true;
pub fn extension(&self, value: JsValue) -> SessionBuilder {
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),
Extension::Pcb(pcb) => self.0.borrow_mut().pcb = Some(pcb),
Extension::DisplayControl(use_display_control) => {
self.0.borrow_mut().use_display_control = use_display_control
}
},
Err(error) => error!(%error, "Unsupported extension value"),
}
self.clone()
}
pub async fn connect(&self) -> Result<Session, IronRdpError> {
pub async fn connect(&self) -> Result<Session, RemoteDesktopError> {
let (
username,
destination,
@@ -297,11 +295,11 @@ impl SessionBuilder {
loop {
match ws.state() {
websocket::State::Closing | websocket::State::Closed => {
return Err(IronRdpError::from(anyhow::anyhow!(
"Failed to connect to {proxy_address} (WebSocket is `{:?}`)",
return Err(RemoteDesktopError::from(anyhow::anyhow!(
"failed to connect to {proxy_address} (WebSocket is `{:?}`)",
ws.state()
))
.with_kind(IronRdpErrorKind::ProxyConnect));
.with_kind(RemoteDesktopErrorKind::ProxyConnect));
}
websocket::State::Connecting => {
trace!("WebSocket is connecting to proxy at {proxy_address}...");
@@ -354,6 +352,13 @@ impl SessionBuilder {
}
}
#[derive(Debug, Serialize, Deserialize)]
enum Extension {
KdcProxyUrl(String),
Pcb(String),
DisplayControl(bool),
}
pub(crate) type FastPathInputEvents = smallvec::SmallVec<[FastPathInputEvent; 2]>;
#[derive(Debug)]
@@ -412,7 +417,7 @@ pub struct Session {
#[wasm_bindgen]
impl Session {
pub async fn run(&self) -> Result<SessionTerminationInfo, IronRdpError> {
pub async fn run(&self) -> Result<SessionTerminationInfo, RemoteDesktopError> {
let rdp_reader = self
.rdp_reader
.borrow_mut()
@@ -707,17 +712,17 @@ impl Session {
}
}
pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), IronRdpError> {
pub fn apply_inputs(&self, transaction: InputTransaction) -> Result<(), RemoteDesktopError> {
let inputs = self.input_database.borrow_mut().apply(transaction);
self.h_send_inputs(inputs)
}
pub fn release_all_inputs(&self) -> Result<(), IronRdpError> {
pub fn release_all_inputs(&self) -> Result<(), RemoteDesktopError> {
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<(), IronRdpError> {
fn h_send_inputs(&self, inputs: smallvec::SmallVec<[FastPathInputEvent; 2]>) -> Result<(), RemoteDesktopError> {
if !inputs.is_empty() {
trace!("Inputs: {inputs:?}");
@@ -735,7 +740,7 @@ impl Session {
num_lock: bool,
caps_lock: bool,
kana_lock: bool,
) -> Result<(), IronRdpError> {
) -> Result<(), RemoteDesktopError> {
use ironrdp::pdu::input::fast_path::FastPathInput;
let event = ironrdp::input::synchronize_event(scroll_lock, num_lock, caps_lock, kana_lock);
@@ -750,7 +755,7 @@ impl Session {
Ok(())
}
pub fn shutdown(&self) -> Result<(), IronRdpError> {
pub fn shutdown(&self) -> Result<(), RemoteDesktopError> {
self.input_events_tx
.unbounded_send(RdpInputEvent::TerminateSession)
.context("failed to send terminate session event to writer task")?;
@@ -758,7 +763,7 @@ impl Session {
Ok(())
}
pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), IronRdpError> {
pub async fn on_clipboard_paste(&self, content: ClipboardTransaction) -> Result<(), RemoteDesktopError> {
self.input_events_tx
.unbounded_send(RdpInputEvent::ClipboardBackend(
WasmClipboardBackendMessage::LocalClipboardChanged(content),
@@ -768,7 +773,7 @@ impl Session {
Ok(())
}
fn set_cursor_style(&self, style: CursorStyle) -> Result<(), IronRdpError> {
fn set_cursor_style(&self, style: CursorStyle) -> Result<(), RemoteDesktopError> {
let (kind, data, hotspot_x, hotspot_y) = match style {
CursorStyle::Default => ("default", None, None, None),
CursorStyle::Hidden => ("hidden", None, None, None),
@@ -818,6 +823,10 @@ impl Session {
// plain scancode events are allowed to function correctly).
false
}
pub fn extension_call(_value: JsValue) -> Result<JsValue, RemoteDesktopError> {
Ok(JsValue::null())
}
}
fn build_config(
@@ -913,7 +922,7 @@ async fn connect(
clipboard_backend,
use_display_control,
}: ConnectParams,
) -> Result<(connector::ConnectionResult, WebSocket), IronRdpError> {
) -> Result<(connector::ConnectionResult, WebSocket), RemoteDesktopError> {
let mut framed = ironrdp_futures::LocalFuturesFramed::new(ws);
let mut connector = ClientConnector::new(config);
@@ -960,7 +969,7 @@ async fn connect_rdcleanpath<S>(
destination: String,
proxy_auth_token: String,
pcb: Option<String>,
) -> Result<(ironrdp_futures::Upgraded, Vec<u8>), IronRdpError>
) -> Result<(ironrdp_futures::Upgraded, Vec<u8>), RemoteDesktopError>
where
S: ironrdp_futures::FramedRead + FramedWrite,
{
@@ -1039,10 +1048,10 @@ where
server_addr,
} => (x224_connection_response, server_cert_chain, server_addr),
ironrdp_rdcleanpath::RDCleanPath::Err(error) => {
return Err(
IronRdpError::from(anyhow::anyhow!("received an RDCleanPath error: {error}"))
.with_kind(IronRdpErrorKind::RDCleanPath),
);
return Err(RemoteDesktopError::from(
anyhow::Error::new(error).context("received an RDCleanPath error"),
)
.with_kind(RemoteDesktopErrorKind::RDCleanPath));
}
};
+1 -1
View File
@@ -2,7 +2,7 @@
IronRDP also supports the web browser as a first class target.
See the [iron-remote-gui](./iron-remote-gui) for the reusable Web Component, and [iron-svelte-client](./iron-svelte-client) for a demonstration.
See the [iron-remote-desktop](./iron-remote-desktop) for the reusable Web Component, and [iron-svelte-client](./iron-svelte-client) for a demonstration.
Note that the demonstration client is not intended to be used in production as-is.
Devolutions is shipping well-integrated, production-ready IronRDP web clients as part of:
@@ -0,0 +1,15 @@
node_modules/
.DS_Store
.env
.env.*
!.env.example
/package
/build
/static/bearcss
/static/material-icons
/dist
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
@@ -0,0 +1,19 @@
# Prettier:
# - https://prettier.io/docs/en/options
---
useTabs: false
tabWidth: 4
singleQuote: true
semi: true
trailingComma: all
printWidth: 120
overrides:
- files:
- '*.yml'
- '*.yaml'
- '*.json'
- '*.html'
- '*.md'
options:
tabWidth: 2
@@ -0,0 +1,23 @@
# Iron Remote Desktop RDP
This is implementation of `RemoteDesktopModule` interface from [iron-remote-desktop](../iron-remote-desktop) for RDP connection.
## Development
Make your modification in the source code then use [iron-svelte-client](../iron-svelte-client) to test.
## Build
Run `npm run build`
## Usage
As member of the Devolutions organization, you can import the Web Component from JFrog Artifactory by running the following npm command:
```shell
$ npm install @devolutions/iron-remote-desktop-rdp
```
Otherwise, you can run `npm install` targeting the `dist/` folder directly.
Import the `iron-remote-desktop-rdp.umd.cjs` from `node_modules/` folder.
@@ -0,0 +1,80 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: [
'**/*.cjs',
'**/.DS_Store',
'**/node_modules',
'build',
'package',
'**/.env',
'**/.env.*',
'!**/.env.example',
'**/pnpm-lock.yaml',
'**/package-lock.json',
'**/yarn.lock',
],
},
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'),
{
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: tsParser,
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
project: './tsconfig.json',
},
},
rules: {
strict: 2,
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
},
],
'@typescript-eslint/strict-boolean-expressions': [
2,
{
allowString: false,
allowNumber: false,
},
],
'prettier/prettier': [
'error',
{
endOfLine: 'auto',
},
],
},
},
];
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,51 @@
{
"name": "@devolutions/iron-remote-desktop-rdp",
"author": "Nicolas Girot",
"email": "ngirot@devolutions.net",
"contributors": [
"Benoit Cortier",
"Irving Ou",
"Vladislav Nikonov",
"Zacharia Ellaham",
"Alexandr Yusuk"
],
"description": "Web Component providing agnostic implementation for Iron Wasm base client",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"dev": "npm run pre-build && vite",
"build": "npm run pre-build && vite build",
"build-alone": "vite build",
"pre-build": "node ./pre-build.js",
"preview": "vite preview",
"check": "tsc --noEmit",
"check:dist": "tsc ./dist/index.d.ts --noEmit",
"check:watch": "tsc --watch --noEmit",
"lint": "npm run lint:prettier && npm run lint:eslint",
"lint:prettier": "prettier --check .",
"lint:eslint": "eslint src/**",
"format": "prettier --write ."
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.21.0",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^8.25.0",
"eslint": "^9.21.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"globals": "^16.0.0",
"prettier": "^3.1.0",
"tslib": "^2.4.1",
"typescript": "~5.7.2",
"vite": "^6.2.0",
"vite-plugin-dts": "^4.5.0",
"vite-plugin-top-level-await": "^1.2.2",
"vite-plugin-wasm": "^3.1.0"
},
"dependencies": {
"rxjs": "^6.6.7",
"ua-parser-js": "^1.0.33"
}
}
@@ -0,0 +1,34 @@
import { spawn } from 'child_process';
const run = async (command, cwd) => {
try {
const buildCommand = spawn(command, {
stdio: 'pipe',
shell: true,
cwd: cwd,
});
buildCommand.stdout.on('data', (data) => {
console.log(`${data}`);
});
buildCommand.stderr.on('data', (data) => {
console.error(`${data}`);
});
const exitCode = await new Promise((resolve, reject) => {
buildCommand.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Process exited with non-zero code: ${code}`));
}
resolve(code);
});
});
console.log(`Child process exited with code: ${exitCode}`);
} catch (err) {
console.error(`Process run failed: ${err}`);
}
};
await run('cargo xtask web build', '../../');
@@ -0,0 +1,22 @@
{
"name": "@devolutions/iron-remote-desktop-rdp",
"author": "Nicolas Girot",
"email": "ngirot@devolutions.net",
"contributors": [
"Benoit Cortier",
"Irving Ou",
"Vladislav Nikonov",
"Zacharia Ellaham"
],
"description": "Web Component providing agnostic implementation for Iron Wasm base client.",
"version": "0.13.1",
"main": "iron-remote-desktop-rdp.js",
"types": "index.d.ts",
"files": [
"iron-remote-desktop-rdp.js",
"index.d.ts"
],
"dependencies": {
"rxjs": "^6.6.7"
}
}

Some files were not shown because too many files have changed in this diff Show More