From cc3dbf124fa72fb7ce9f275bab31e9eda2940c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Cortier?= <3809077+CBenoit@users.noreply.github.com> Date: Mon, 14 Apr 2025 15:08:44 +0200 Subject: [PATCH] feat(client): add support for RDCleanPath (#745) Issue: ARC-309 --- Cargo.lock | 204 ++++++++++++++++++------ crates/ironrdp-client/Cargo.toml | 17 +- crates/ironrdp-client/src/config.rs | 30 ++-- crates/ironrdp-client/src/lib.rs | 2 + crates/ironrdp-client/src/rdp.rs | 230 ++++++++++++++++++++++++++-- crates/ironrdp-client/src/ws.rs | 34 ++++ crates/ironrdp-web/src/session.rs | 2 +- 7 files changed, 446 insertions(+), 73 deletions(-) create mode 100644 crates/ironrdp-client/src/ws.rs diff --git a/Cargo.lock b/Cargo.lock index 085515ef..8b499033 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,7 +139,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -270,7 +270,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -593,7 +593,7 @@ dependencies = [ "polling", "rustix 0.38.44", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1056,7 +1056,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -1068,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -1384,7 +1384,7 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2", "subtle", @@ -1412,7 +1412,7 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -1500,7 +1500,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1515,7 +1515,7 @@ dependencies = [ "ironrdp-cliprdr-native", "ironrdp-core", "sspi", - "thiserror", + "thiserror 1.0.69", "tracing", "tracing-subscriber", ] @@ -1806,7 +1806,7 @@ dependencies = [ "http", "js-sys", "pin-project", - "thiserror", + "thiserror 1.0.69", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1842,7 +1842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1921,8 +1921,8 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand", - "thiserror", + "rand 0.8.5", + "thiserror 1.0.69", "tinyvec", "tokio", "tracing", @@ -1942,10 +1942,10 @@ dependencies = [ "lru-cache", "once_cell", "parking_lot", - "rand", + "rand 0.8.5", "resolv-conf", "smallvec", - "thiserror", + "thiserror 1.0.69", "tokio", "tracing", ] @@ -2354,7 +2354,7 @@ dependencies = [ "ironrdp-svc", "opus", "pico-args", - "rand", + "rand 0.8.5", "sspi", "tokio-rustls", "tracing", @@ -2423,10 +2423,12 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "futures-util", "inquire", "ironrdp", "ironrdp-cliprdr-native", "ironrdp-core", + "ironrdp-rdcleanpath", "ironrdp-rdpsnd-native", "ironrdp-tls", "ironrdp-tokio", @@ -2437,12 +2439,15 @@ dependencies = [ "softbuffer", "tap", "tokio", + "tokio-tungstenite", "tracing", "tracing-subscriber", + "transport", "uuid", "whoami", "windows 0.61.1", "winit", + "x509-cert", ] [[package]] @@ -2453,7 +2458,7 @@ dependencies = [ "ironrdp-core", "ironrdp-pdu", "ironrdp-svc", - "thiserror", + "thiserror 1.0.69", "tracing", ] @@ -2463,7 +2468,7 @@ version = "0.1.3" dependencies = [ "ironrdp-core", "png", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2472,7 +2477,7 @@ version = "0.2.0" dependencies = [ "ironrdp-cliprdr", "ironrdp-core", - "thiserror", + "thiserror 1.0.69", "tracing", "windows 0.61.1", ] @@ -2489,7 +2494,7 @@ dependencies = [ "picky", "picky-asn1-der", "picky-asn1-x509", - "rand_core", + "rand_core 0.6.4", "sspi", "tracing", "url", @@ -2568,7 +2573,7 @@ dependencies = [ "lazy_static", "num-derive", "num-traits", - "thiserror", + "thiserror 1.0.69", "yuvutils-rs", ] @@ -2601,7 +2606,7 @@ dependencies = [ "pkcs1", "sha1", "tap", - "thiserror", + "thiserror 1.0.69", "x509-cert", ] @@ -2886,7 +2891,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -3159,7 +3164,7 @@ dependencies = [ "log", "ndk-sys 0.5.0+25.2.9519653", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3174,7 +3179,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3264,7 +3269,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "serde", "smallvec", "zeroize", @@ -3712,7 +3717,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "rand_core", + "rand_core 0.6.4", "sha2", ] @@ -3797,8 +3802,8 @@ dependencies = [ "picky-asn1", "picky-asn1-der", "picky-asn1-x509", - "rand", - "rand_core", + "rand 0.8.5", + "rand_core 0.6.4", "rc2", "rsa", "serde", @@ -3806,7 +3811,7 @@ dependencies = [ "sha1", "sha2", "sha3", - "thiserror", + "thiserror 1.0.69", "x25519-dalek", "zeroize", ] @@ -3869,10 +3874,10 @@ dependencies = [ "picky-asn1", "picky-asn1-der", "picky-asn1-x509", - "rand", + "rand 0.8.5", "serde", "sha1", - "thiserror", + "thiserror 1.0.69", "uuid", ] @@ -4015,7 +4020,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -4097,8 +4102,8 @@ dependencies = [ "bitflags 2.9.0", "lazy_static", "num-traits", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax 0.8.5", "rusty-fork", @@ -4143,8 +4148,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.23", ] [[package]] @@ -4154,7 +4170,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -4166,13 +4192,22 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -4391,7 +4426,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sha1", "signature", "spki", @@ -4818,7 +4853,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -4856,7 +4891,7 @@ dependencies = [ "log", "memmap2", "rustix 0.38.44", - "thiserror", + "thiserror 1.0.69", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -4962,7 +4997,7 @@ dependencies = [ "picky-asn1-x509", "picky-krb", "portpicker", - "rand", + "rand 0.8.5", "reqwest", "rsa", "rustls", @@ -5100,7 +5135,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -5114,6 +5158,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -5305,6 +5360,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.13" @@ -5455,6 +5528,21 @@ dependencies = [ "web-sys", ] +[[package]] +name = "transport" +version = "0.0.0" +source = "git+https://github.com/Devolutions/devolutions-gateway?rev=06e91dfe82751a6502eaf74b6a99663f06f0236d#06e91dfe82751a6502eaf74b6a99663f06f0236d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "parking_lot", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -5467,6 +5555,26 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.0", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + [[package]] name = "typenum" version = "1.18.0" @@ -5524,6 +5632,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -6498,7 +6612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "serde", "zeroize", ] diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index 3c808b00..2ce92a7a 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -25,8 +25,8 @@ test = false [features] default = ["rustls"] -rustls = ["ironrdp-tls/rustls"] -native-tls = ["ironrdp-tls/native-tls"] +rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"] +native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"] [dependencies] # Protocols @@ -42,15 +42,12 @@ ironrdp = { path = "../ironrdp", version = "0.9", features = [ "displaycontrol", "connector", ] } -ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = [ - "alloc", -] } +ironrdp-core = { path = "../ironrdp-core", version = "0.1", features = ["alloc"] } ironrdp-cliprdr-native = { path = "../ironrdp-cliprdr-native", version = "0.2" } ironrdp-rdpsnd-native = { path = "../ironrdp-rdpsnd-native", version = "0.2" } ironrdp-tls = { path = "../ironrdp-tls", version = "0.1" } -ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.3", features = [ - "reqwest", -] } +ironrdp-tokio = { path = "../ironrdp-tokio", version = "0.3", features = ["reqwest"] } +ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" # Windowing and rendering winit = { version = "0.30", features = ["rwh_06"] } @@ -67,6 +64,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Async, futures tokio = { version = "1", features = ["full"] } +tokio-tungstenite = "0.26" +transport = { git = "https://github.com/Devolutions/devolutions-gateway", rev = "06e91dfe82751a6502eaf74b6a99663f06f0236d" } +futures-util = { version = "0.3", features = ["sink"] } # Utils whoami = "1.6" @@ -76,6 +76,7 @@ tap = "1" semver = "1" raw-window-handle = "0.6" uuid = { version = "1.16" } +x509-cert = { version = "0.2", default-features = false, features = ["std"] } [target.'cfg(windows)'.dependencies] windows = { version = "0.61", features = ["Win32_Foundation"] } diff --git a/crates/ironrdp-client/src/config.rs b/crates/ironrdp-client/src/config.rs index 21a10d9f..9a398a5e 100644 --- a/crates/ironrdp-client/src/config.rs +++ b/crates/ironrdp-client/src/config.rs @@ -1,6 +1,5 @@ use core::num::ParseIntError; use core::str::FromStr; -use std::io; use anyhow::Context as _; use clap::clap_derive::ValueEnum; @@ -19,6 +18,7 @@ pub struct Config { pub destination: Destination, pub connector: connector::Config, pub clipboard_type: ClipboardType, + pub rdcleanpath: Option, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] @@ -107,14 +107,6 @@ impl Destination { pub fn port(&self) -> u16 { self.port } - - pub fn lookup_addr(&self) -> io::Result { - use std::net::ToSocketAddrs as _; - - let sockaddr = (self.name.as_str(), self.port).to_socket_addrs()?.next().unwrap(); - - Ok(sockaddr) - } } impl FromStr for Destination { @@ -137,6 +129,12 @@ impl From<&Destination> for connector::ServerName { } } +#[derive(Clone, Debug)] +pub struct RDCleanPathConfig { + pub url: String, + pub auth_token: String, +} + /// Devolutions IronRDP client #[derive(Parser, Debug)] #[clap(author = "Devolutions", about = "Devolutions-IronRDP client")] @@ -161,6 +159,14 @@ struct Args { #[clap(short, long, value_parser)] password: Option, + /// Proxy URL to connect to for the RDCleanPath + #[clap(long, requires("rdcleanpath_token"))] + rdcleanpath_url: Option, + + /// Authentication token to insert in the RDCleanPath packet + #[clap(long, requires("rdcleanpath_url"))] + rdcleanpath_token: Option, + /// The keyboard type #[clap(long, value_enum, value_parser, default_value_t = KeyboardType::IbmEnhanced)] keyboard_type: KeyboardType, @@ -326,11 +332,17 @@ impl Config { performance_flags: PerformanceFlags::default(), }; + let rdcleanpath = args + .rdcleanpath_url + .zip(args.rdcleanpath_token) + .map(|(url, auth_token)| RDCleanPathConfig { url, auth_token }); + Ok(Self { log_file: args.log_file, destination, connector, clipboard_type, + rdcleanpath, }) } } diff --git a/crates/ironrdp-client/src/lib.rs b/crates/ironrdp-client/src/lib.rs index 0ae2115a..1973f4b1 100644 --- a/crates/ironrdp-client/src/lib.rs +++ b/crates/ironrdp-client/src/lib.rs @@ -16,3 +16,5 @@ pub mod app; pub mod clipboard; pub mod config; pub mod rdp; + +mod ws; diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index 5469e2aa..cfcf366c 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -14,11 +14,12 @@ use ironrdp_tokio::reqwest::ReqwestNetworkClient; use ironrdp_tokio::{single_sequence_step_read, split_tokio_framed, FramedWrite}; use rdpdr::NoopRdpdrBackend; use smallvec::SmallVec; +use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; use tokio::sync::mpsc; use winit::event_loop::EventLoopProxy; -use crate::config::Config; +use crate::config::{Config, RDCleanPathConfig}; #[derive(Debug)] pub enum RdpOutputEvent { @@ -60,11 +61,21 @@ pub struct RdpClient { impl RdpClient { pub async fn run(mut self) { loop { - let (connection_result, framed) = match connect(&self.config, self.cliprdr_factory.as_deref()).await { - Ok(result) => result, - Err(e) => { - let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e)); - break; + let (connection_result, framed) = if let Some(rdcleanpath) = self.config.rdcleanpath.as_ref() { + match connect_ws(&self.config, rdcleanpath, self.cliprdr_factory.as_deref()).await { + Ok(result) => result, + Err(e) => { + let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e)); + break; + } + } + } else { + match connect(&self.config, self.cliprdr_factory.as_deref()).await { + Ok(result) => result, + Err(e) => { + let _ = self.event_loop_proxy.send_event(RdpOutputEvent::ConnectionFailure(e)); + break; + } } }; @@ -98,7 +109,11 @@ enum RdpControlFlow { TerminatedGracefully(GracefulDisconnectReason), } -type UpgradedFramed = ironrdp_tokio::TokioFramed>; +trait AsyncReadWrite: AsyncRead + AsyncWrite {} + +impl AsyncReadWrite for T where T: AsyncRead + AsyncWrite {} + +type UpgradedFramed = ironrdp_tokio::TokioFramed>; async fn connect( config: &Config, @@ -145,16 +160,16 @@ async fn connect( let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, &mut connector); - let mut upgraded_framed = ironrdp_tokio::TokioFramed::new(upgraded_stream); + let erased_stream = Box::new(upgraded_stream) as Box; + let mut upgraded_framed = ironrdp_tokio::TokioFramed::new(erased_stream); - let mut network_client = ReqwestNetworkClient::new(); let connection_result = ironrdp_tokio::connect_finalize( upgraded, &mut upgraded_framed, connector, (&config.destination).into(), server_public_key, - Some(&mut network_client), + Some(&mut ReqwestNetworkClient::new()), None, ) .await?; @@ -164,6 +179,201 @@ async fn connect( Ok((connection_result, upgraded_framed)) } +async fn connect_ws( + config: &Config, + rdcleanpath: &RDCleanPathConfig, + cliprdr_factory: Option<&(dyn CliprdrBackendFactory + Send)>, +) -> ConnectorResult<(ConnectionResult, UpgradedFramed)> { + let (ws, _) = tokio_tungstenite::connect_async(&rdcleanpath.url) + .await + .map_err(|e| connector::custom_err!("WS connect", e))?; + + let ws = crate::ws::websocket_compat(ws); + + let mut framed = ironrdp_tokio::TokioFramed::new(ws); + + let mut connector = connector::ClientConnector::new(config.connector.clone()) + .with_static_channel( + ironrdp::dvc::DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))), + ) + .with_static_channel(rdpsnd::client::Rdpsnd::new(Box::new(cpal::RdpsndBackend::new()))) + .with_static_channel(rdpdr::Rdpdr::new(Box::new(NoopRdpdrBackend {}), "IronRDP".to_owned()).with_smartcard(0)); + + if let Some(builder) = cliprdr_factory { + let backend = builder.build_cliprdr_backend(); + + let cliprdr = cliprdr::Cliprdr::new(backend); + + connector.attach_static_channel(cliprdr); + } + + let destination = format!("{}:{}", config.destination.name(), config.destination.port()); + + let (upgraded, server_public_key) = connect_rdcleanpath( + &mut framed, + &mut connector, + destination, + rdcleanpath.auth_token.clone(), + None, + ) + .await?; + + let connection_result = ironrdp_tokio::connect_finalize( + upgraded, + &mut framed, + connector, + (&config.destination).into(), + server_public_key, + Some(&mut ReqwestNetworkClient::new()), + None, + ) + .await?; + + let ws = framed.into_inner_no_leftover(); + let erased_stream = Box::new(ws) as Box; + let upgraded_framed = ironrdp_tokio::TokioFramed::new(erased_stream); + + Ok((connection_result, upgraded_framed)) +} + +async fn connect_rdcleanpath( + framed: &mut ironrdp_tokio::Framed, + connector: &mut connector::ClientConnector, + destination: String, + proxy_auth_token: String, + pcb: Option, +) -> ConnectorResult<(ironrdp_tokio::Upgraded, Vec)> +where + S: ironrdp_tokio::FramedRead + FramedWrite, +{ + use ironrdp::connector::Sequence as _; + use x509_cert::der::Decode as _; + + #[derive(Clone, Copy, Debug)] + struct RDCleanPathHint; + + const RDCLEANPATH_HINT: RDCleanPathHint = RDCleanPathHint; + + impl ironrdp::pdu::PduHint for RDCleanPathHint { + fn find_size(&self, bytes: &[u8]) -> ironrdp::core::DecodeResult> { + match ironrdp_rdcleanpath::RDCleanPathPdu::detect(bytes) { + ironrdp_rdcleanpath::DetectionResult::Detected { total_length, .. } => Ok(Some((true, total_length))), + ironrdp_rdcleanpath::DetectionResult::NotEnoughBytes => Ok(None), + ironrdp_rdcleanpath::DetectionResult::Failed => Err(ironrdp::core::other_err!( + "RDCleanPathHint", + "detection failed (invalid PDU)" + )), + } + } + } + + let mut buf = WriteBuf::new(); + + info!("Begin connection procedure"); + + { + // RDCleanPath request + + let connector::ClientConnectorState::ConnectionInitiationSendRequest = connector.state else { + return Err(connector::general_err!("invalid connector state (send request)")); + }; + + debug_assert!(connector.next_pdu_hint().is_none()); + + let written = connector.step_no_input(&mut buf)?; + let x224_pdu_len = written.size().expect("written size"); + debug_assert_eq!(x224_pdu_len, buf.filled_len()); + let x224_pdu = buf.filled().to_vec(); + + let rdcleanpath_req = + ironrdp_rdcleanpath::RDCleanPathPdu::new_request(x224_pdu, destination, proxy_auth_token, pcb) + .map_err(|e| connector::custom_err!("new RDCleanPath request", e))?; + debug!(message = ?rdcleanpath_req, "Send RDCleanPath request"); + let rdcleanpath_req = rdcleanpath_req + .to_der() + .map_err(|e| connector::custom_err!("RDCleanPath request encode", e))?; + + framed + .write_all(&rdcleanpath_req) + .await + .map_err(|e| connector::custom_err!("couldn’t write RDCleanPath request", e))?; + } + + { + // RDCleanPath response + + let rdcleanpath_res = framed + .read_by_hint(&RDCLEANPATH_HINT) + .await + .map_err(|e| connector::custom_err!("read RDCleanPath request", e))?; + + let rdcleanpath_res = ironrdp_rdcleanpath::RDCleanPathPdu::from_der(&rdcleanpath_res) + .map_err(|e| connector::custom_err!("RDCleanPath response decode", e))?; + + debug!(message = ?rdcleanpath_res, "Received RDCleanPath PDU"); + + let (x224_connection_response, server_cert_chain, server_addr) = match rdcleanpath_res + .into_enum() + .map_err(|e| connector::custom_err!("invalid RDCleanPath PDU", e))? + { + ironrdp_rdcleanpath::RDCleanPath::Request { .. } => { + return Err(connector::general_err!( + "received an unexpected RDCleanPath type (request)", + )); + } + ironrdp_rdcleanpath::RDCleanPath::Response { + x224_connection_response, + server_cert_chain, + server_addr, + } => (x224_connection_response, server_cert_chain, server_addr), + ironrdp_rdcleanpath::RDCleanPath::Err(error) => { + return Err(connector::custom_err!("received an RDCleanPath error", error)); + } + }; + + let server_addr = server_addr + .parse() + .map_err(|e| connector::custom_err!("failed to parse server address sent by proxy", e))?; + + connector.attach_server_addr(server_addr); + + let connector::ClientConnectorState::ConnectionInitiationWaitConfirm { .. } = connector.state else { + return Err(connector::general_err!("invalid connector state (wait confirm)")); + }; + + debug_assert!(connector.next_pdu_hint().is_some()); + + buf.clear(); + let written = connector.step(x224_connection_response.as_bytes(), &mut buf)?; + + debug_assert!(written.is_nothing()); + + let server_cert = server_cert_chain + .into_iter() + .next() + .ok_or_else(|| connector::general_err!("server cert chain missing from rdcleanpath response"))?; + + let cert = x509_cert::Certificate::from_der(server_cert.as_bytes()) + .map_err(|e| connector::custom_err!("server cert chain missing from rdcleanpath response", e))?; + + let server_public_key = cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() + .ok_or_else(|| connector::general_err!("subject public key BIT STRING is not aligned"))? + .to_owned(); + + let should_upgrade = ironrdp_tokio::skip_connect_begin(connector); + + // At this point, proxy established the TLS session. + + let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, connector); + + Ok((upgraded, server_public_key)) + } +} + async fn active_session( framed: UpgradedFramed, connection_result: ConnectionResult, diff --git a/crates/ironrdp-client/src/ws.rs b/crates/ironrdp-client/src/ws.rs new file mode 100644 index 00000000..675553b9 --- /dev/null +++ b/crates/ironrdp-client/src/ws.rs @@ -0,0 +1,34 @@ +use futures_util::{Sink, SinkExt as _, Stream, StreamExt as _}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_tungstenite::tungstenite; + +pub(crate) fn websocket_compat(stream: S) -> impl AsyncRead + AsyncWrite + Unpin + Send + 'static +where + S: Stream> + + Sink + + Unpin + + Send + + 'static, +{ + let compat = stream + .filter_map(|item| { + let mapped = item + .map(|msg| match msg { + tungstenite::Message::Text(s) => Some(transport::WsReadMsg::Payload(tungstenite::Bytes::from(s))), + tungstenite::Message::Binary(data) => Some(transport::WsReadMsg::Payload(data)), + tungstenite::Message::Ping(_) | tungstenite::Message::Pong(_) => None, + tungstenite::Message::Close(_) => Some(transport::WsReadMsg::Close), + tungstenite::Message::Frame(_) => unreachable!("raw frames are never returned when reading"), + }) + .transpose(); + + core::future::ready(mapped) + }) + .with(|item| { + core::future::ready(Ok::<_, tungstenite::Error>(tungstenite::Message::Binary( + tungstenite::Bytes::from(item), + ))) + }); + + transport::WsStream::new(compat) +} diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 9ce6e73b..2b912a50 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -1090,7 +1090,7 @@ where let should_upgrade = ironrdp_futures::skip_connect_begin(connector); - // At this point, proxy established the TLS session + // At this point, proxy established the TLS session. let upgraded = ironrdp_futures::mark_as_upgraded(should_upgrade, connector);