mirror of
https://github.com/netbirdio/IronRDP.git
synced 2026-05-22 18:43:12 -07:00
Generated
+159
-45
File diff suppressed because it is too large
Load Diff
@@ -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"] }
|
||||
|
||||
@@ -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<RDCleanPathConfig>,
|
||||
}
|
||||
|
||||
#[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<std::net::SocketAddr> {
|
||||
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<String>,
|
||||
|
||||
/// Proxy URL to connect to for the RDCleanPath
|
||||
#[clap(long, requires("rdcleanpath_token"))]
|
||||
rdcleanpath_url: Option<String>,
|
||||
|
||||
/// Authentication token to insert in the RDCleanPath packet
|
||||
#[clap(long, requires("rdcleanpath_url"))]
|
||||
rdcleanpath_token: Option<String>,
|
||||
|
||||
/// 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +16,5 @@ pub mod app;
|
||||
pub mod clipboard;
|
||||
pub mod config;
|
||||
pub mod rdp;
|
||||
|
||||
mod ws;
|
||||
|
||||
@@ -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<ironrdp_tls::TlsStream<TcpStream>>;
|
||||
trait AsyncReadWrite: AsyncRead + AsyncWrite {}
|
||||
|
||||
impl<T> AsyncReadWrite for T where T: AsyncRead + AsyncWrite {}
|
||||
|
||||
type UpgradedFramed = ironrdp_tokio::TokioFramed<Box<dyn AsyncReadWrite + Unpin + Send + Sync>>;
|
||||
|
||||
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<dyn AsyncReadWrite + Unpin + Send + Sync>;
|
||||
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<dyn AsyncReadWrite + Unpin + Send + Sync>;
|
||||
let upgraded_framed = ironrdp_tokio::TokioFramed::new(erased_stream);
|
||||
|
||||
Ok((connection_result, upgraded_framed))
|
||||
}
|
||||
|
||||
async fn connect_rdcleanpath<S>(
|
||||
framed: &mut ironrdp_tokio::Framed<S>,
|
||||
connector: &mut connector::ClientConnector,
|
||||
destination: String,
|
||||
proxy_auth_token: String,
|
||||
pcb: Option<String>,
|
||||
) -> ConnectorResult<(ironrdp_tokio::Upgraded, Vec<u8>)>
|
||||
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<Option<(bool, usize)>> {
|
||||
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,
|
||||
|
||||
@@ -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<S>(stream: S) -> impl AsyncRead + AsyncWrite + Unpin + Send + 'static
|
||||
where
|
||||
S: Stream<Item = Result<tungstenite::Message, tungstenite::Error>>
|
||||
+ Sink<tungstenite::Message, Error = tungstenite::Error>
|
||||
+ 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)
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user