diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f8e493634..63f6b86d2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -46,6 +46,7 @@ a feature flag called `alloc` must exist to enable its use. Higher level libraries and binaries built on top of the core tier. Guidelines and constraints are relaxed to some extent. +- `crates/ironrdp-blocking`: blocking I/O abstraction wrapping the state machines conveniently. - `crates/ironrdp-async`: provides `Future`s wrapping the state machines conveniently. - `crates/ironrdp-tokio`: `Framed*` traits implementation above `tokio`’s traits. - `crates/ironrdp-futures`: `Framed*` traits implementation above `futures`’s traits. diff --git a/Cargo.lock b/Cargo.lock index 559bc5e78..64f7b105b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -993,7 +993,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.0", ] [[package]] @@ -1797,7 +1797,10 @@ checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" name = "ironrdp" version = "0.5.0" dependencies = [ + "anyhow", + "bmp", "ironrdp-acceptor", + "ironrdp-blocking", "ironrdp-cliprdr", "ironrdp-connector", "ironrdp-dvc", @@ -1809,6 +1812,11 @@ dependencies = [ "ironrdp-server", "ironrdp-session", "ironrdp-svc", + "pico-args", + "rustls", + "tracing", + "tracing-subscriber", + "x509-cert", ] [[package]] @@ -1832,6 +1840,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "ironrdp-blocking" +version = "0.1.0" +dependencies = [ + "bytes", + "ironrdp-connector", + "ironrdp-pdu", + "tap", + "tracing", +] + [[package]] name = "ironrdp-client" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2d95bc9fe..2c973fb25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ categories = ["network-programming"] [workspace.dependencies] ironrdp-acceptor = { version = "0.1", path = "crates/ironrdp-acceptor" } ironrdp-async = { version = "0.1", path = "crates/ironrdp-async" } +ironrdp-blocking = { version = "0.1", path = "crates/ironrdp-blocking" } ironrdp-cliprdr = { version = "0.1", path = "crates/ironrdp-cliprdr" } ironrdp-connector = { version = "0.1", path = "crates/ironrdp-connector" } ironrdp-dvc = { version = "0.1", path = "crates/ironrdp-dvc" } diff --git a/README.md b/README.md index 228777b0e..3de70b990 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,32 @@ Supported codecs: - RDP 6.0 Bitmap Compression - Microsoft RemoteFX (RFX) +## Examples + +### [`ironrdp-client`](./crates/ironrdp-client) + +A full-fledged RDP client based on IronRDP crates suite, and implemented using non-blocking, asynchronous I/O. + +```bash +cargo run --bin ironrdp-client -- --username --password +``` + +### [`screenshot`](./crates/ironrdp/examples/screenshot.rs) + +Example of utilizing IronRDP in a blocking, synchronous fashion. + +This example showcases the use of IronRDP in a blocking manner. It +demonstrates how to create a basic RDP client with just a few hundred lines +of code by leveraging the IronRDP crates suite. + +In this basic client implementation, the client establishes a connection +with the destination server, decodes incoming graphics updates, and saves the +resulting output as a BMP image file on the local disk. + +```bash +cargo run --example=screenshot -- --host --username --password --output out.bmp +``` + ### How to enable RemoteFX on server Run the following PowerShell commands, and reboot. diff --git a/crates/ironrdp-async/src/framed.rs b/crates/ironrdp-async/src/framed.rs index 7aedb5387..b1478ffbe 100644 --- a/crates/ironrdp-async/src/framed.rs +++ b/crates/ironrdp-async/src/framed.rs @@ -12,6 +12,12 @@ pub trait FramedRead { Self: 'read; /// Reads from stream and fills internal buffer + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is guaranteed that no data was read. fn read<'a>(&'a mut self, buf: &'a mut BytesMut) -> Self::ReadFut<'a>; } @@ -21,6 +27,14 @@ pub trait FramedWrite { Self: 'write; /// Writes an entire buffer into this stream. + /// + /// # Cancel safety + /// + /// This method is not cancellation safe. If it is used as the event + /// in a `tokio::select!` statement and some other + /// branch completes first, then the provided buffer may have been + /// partially written, but future calls to `write_all` will start over + /// from the beginning of the buffer. fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> Self::WriteAllFut<'a>; } @@ -81,11 +95,14 @@ impl Framed where S: FramedRead, { - /// Reads from stream and fills internal buffer - pub async fn read(&mut self) -> io::Result { - self.stream.read(&mut self.buf).await - } - + /// Accumulates at least `length` bytes and returns exactly `length` bytes, keeping the leftover in the internal buffer. + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is safe to drop the future and re-create it later. + /// Data may have been read, but it will be stored in the internal buffer. pub async fn read_exact(&mut self, length: usize) -> io::Result { loop { if self.buf.len() >= length { @@ -103,6 +120,14 @@ where } } + /// Reads a standard RDP PDU frame. + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is safe to drop the future and re-create it later. + /// Data may have been read, but it will be stored in the internal buffer. pub async fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, BytesMut)> { loop { // Try decoding and see if a frame has been received already @@ -125,6 +150,14 @@ where } } + /// Reads a frame using the provided PduHint. + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is safe to drop the future and re-create it later. + /// Data may have been read, but it will be stored in the internal buffer. pub async fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result { loop { match hint @@ -145,13 +178,32 @@ where }; } } + + /// Reads from stream and fills internal buffer, returning how many bytes were read. + /// + /// # Cancel safety + /// + /// This method is cancel safe. If you use it as the event in a + /// `tokio::select!` statement and some other branch + /// completes first, then it is guaranteed that no data was read. + async fn read(&mut self) -> io::Result { + self.stream.read(&mut self.buf).await + } } impl Framed where S: FramedWrite, { - /// Writes an entire buffer into this stream. + /// Attempts to write an entire buffer into this `Framed`’s stream. + /// + /// # Cancel safety + /// + /// This method is not cancellation safe. If it is used as the event + /// in a `tokio::select!` statement and some other + /// branch completes first, then the provided buffer may have been + /// partially written, but future calls to `write_all` will start over + /// from the beginning of the buffer. pub async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { self.stream.write_all(buf).await } diff --git a/crates/ironrdp-blocking/Cargo.toml b/crates/ironrdp-blocking/Cargo.toml new file mode 100644 index 000000000..f7fbddbba --- /dev/null +++ b/crates/ironrdp-blocking/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ironrdp-blocking" +version = "0.1.0" +readme = "README.md" +description = "Blocking I/O abstraction wrapping the IronRDP state machines conveniently" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[dependencies] +bytes = "1" +ironrdp-connector.workspace = true +ironrdp-pdu.workspace = true +# ironrdp-session.workspace = true +tap = "1" +tracing.workspace = true diff --git a/crates/ironrdp-blocking/README.md b/crates/ironrdp-blocking/README.md new file mode 100644 index 000000000..521f6d71e --- /dev/null +++ b/crates/ironrdp-blocking/README.md @@ -0,0 +1,7 @@ +# IronRDP Blocking + +Blocking I/O abstraction wrapping the IronRDP state machines conveniently. + +This crate is a higher level abstraction for IronRDP state machines using blocking I/O instead of +asynchronous I/O. This results in a simpler API with fewer dependencies that should be used +instead of `ironrdp-async` when concurrency is not a requirement. \ No newline at end of file diff --git a/crates/ironrdp-blocking/src/connector.rs b/crates/ironrdp-blocking/src/connector.rs new file mode 100644 index 000000000..b38361b9b --- /dev/null +++ b/crates/ironrdp-blocking/src/connector.rs @@ -0,0 +1,116 @@ +use std::io::{Read, Write}; + +use ironrdp_connector::{ + ClientConnector, ClientConnectorState, ConnectionResult, ConnectorResult, Sequence as _, State as _, +}; +use ironrdp_pdu::write_buf::WriteBuf; + +use crate::framed::Framed; + +pub struct ShouldUpgrade { + _priv: (), +} + +#[instrument(skip_all)] +pub fn connect_begin(framed: &mut Framed, connector: &mut ClientConnector) -> ConnectorResult +where + S: Sync + Read + Write, +{ + let mut buf = WriteBuf::new(); + + info!("Begin connection procedure"); + + while !connector.should_perform_security_upgrade() { + single_connect_step(framed, connector, &mut buf)?; + } + + Ok(ShouldUpgrade { _priv: () }) +} + +pub fn skip_connect_begin(connector: &mut ClientConnector) -> ShouldUpgrade { + assert!(connector.should_perform_security_upgrade()); + ShouldUpgrade { _priv: () } +} + +pub struct Upgraded { + _priv: (), +} + +#[instrument(skip_all)] +pub fn mark_as_upgraded(_: ShouldUpgrade, connector: &mut ClientConnector, server_public_key: Vec) -> Upgraded { + trace!("marked as upgraded"); + connector.attach_server_public_key(server_public_key); + connector.mark_security_upgrade_as_done(); + Upgraded { _priv: () } +} + +#[instrument(skip_all)] +pub fn connect_finalize( + _: Upgraded, + framed: &mut Framed, + mut connector: ClientConnector, +) -> ConnectorResult +where + S: Read + Write, +{ + let mut buf = WriteBuf::new(); + + debug!("CredSSP procedure"); + + while connector.is_credssp_step() { + single_connect_step(framed, &mut connector, &mut buf)?; + } + + debug!("Remaining of connection sequence"); + + let result = loop { + single_connect_step(framed, &mut connector, &mut buf)?; + + if let ClientConnectorState::Connected { result } = connector.state { + break result; + } + }; + + info!("Connected with success"); + + Ok(result) +} + +pub fn single_connect_step( + framed: &mut Framed, + connector: &mut ClientConnector, + buf: &mut WriteBuf, +) -> ConnectorResult +where + S: Read + Write, +{ + buf.clear(); + + let written = if let Some(next_pdu_hint) = connector.next_pdu_hint() { + debug!( + connector.state = connector.state.name(), + hint = ?next_pdu_hint, + "Wait for PDU" + ); + + let pdu = framed + .read_by_hint(next_pdu_hint) + .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; + + trace!(length = pdu.len(), "PDU received"); + + connector.step(&pdu, buf)? + } else { + connector.step_no_input(buf)? + }; + + if let Some(response_len) = written.size() { + let response = &buf[..response_len]; + trace!(response_len, "Send response"); + framed + .write_all(response) + .map_err(|e| ironrdp_connector::custom_err!("write all", e))?; + } + + Ok(written) +} diff --git a/crates/ironrdp-blocking/src/framed.rs b/crates/ironrdp-blocking/src/framed.rs new file mode 100644 index 000000000..3aa6cfd36 --- /dev/null +++ b/crates/ironrdp-blocking/src/framed.rs @@ -0,0 +1,130 @@ +use std::io::{self, Read, Write}; + +use bytes::{Bytes, BytesMut}; +use ironrdp_pdu::PduHint; + +pub struct Framed { + stream: S, + buf: BytesMut, +} + +impl Framed { + pub fn new(stream: S) -> Self { + Self { + stream, + buf: BytesMut::new(), + } + } + + pub fn into_inner(self) -> (S, BytesMut) { + (self.stream, self.buf) + } + + pub fn into_inner_no_leftover(self) -> S { + let (stream, leftover) = self.into_inner(); + debug_assert_eq!(leftover.len(), 0, "unexpected leftover"); + stream + } + + pub fn get_inner(&self) -> (&S, &BytesMut) { + (&self.stream, &self.buf) + } + + pub fn get_inner_mut(&mut self) -> (&mut S, &mut BytesMut) { + (&mut self.stream, &mut self.buf) + } + + pub fn peek(&self) -> &[u8] { + &self.buf + } +} + +impl Framed +where + S: Read, +{ + /// Accumulates at least `length` bytes and returns exactly `length` bytes, keeping the leftover in the internal buffer. + pub fn read_exact(&mut self, length: usize) -> io::Result { + loop { + if self.buf.len() >= length { + return Ok(self.buf.split_to(length)); + } else { + self.buf.reserve(length - self.buf.len()); + } + + let len = self.read()?; + + // Handle EOF + if len == 0 { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); + } + } + } + + /// Reads a standard RDP PDU frame. + pub fn read_pdu(&mut self) -> io::Result<(ironrdp_pdu::Action, BytesMut)> { + loop { + // Try decoding and see if a frame has been received already + match ironrdp_pdu::find_size(self.peek()) { + Ok(Some(pdu_info)) => { + let frame = self.read_exact(pdu_info.length)?; + + return Ok((pdu_info.action, frame)); + } + Ok(None) => { + let len = self.read()?; + + // Handle EOF + if len == 0 { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); + } + } + Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e)), + }; + } + } + + /// Reads a frame using the provided PduHint. + pub fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result { + loop { + match hint + .find_size(self.peek()) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))? + { + Some(length) => { + return Ok(self.read_exact(length)?.freeze()); + } + None => { + let len = self.read()?; + + // Handle EOF + if len == 0 { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "not enough bytes")); + } + } + }; + } + } + + /// Reads from stream and fills internal buffer, returning how many bytes were read. + fn read(&mut self) -> io::Result { + // FIXME(perf): use read_buf (https://doc.rust-lang.org/std/io/trait.Read.html#method.read_buf) + // once its stabilized. See tracking issue for RFC 2930: https://github.com/rust-lang/rust/issues/78485 + + let mut read_bytes = [0u8; 1024]; + let len = self.stream.read(&mut read_bytes)?; + self.buf.extend_from_slice(&read_bytes[..len]); + + Ok(len) + } +} + +impl Framed +where + S: Write, +{ + /// Attempts to write an entire buffer into this `Framed`’s stream. + pub fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { + self.stream.write_all(buf) + } +} diff --git a/crates/ironrdp-blocking/src/lib.rs b/crates/ironrdp-blocking/src/lib.rs new file mode 100644 index 000000000..d7dfd19a2 --- /dev/null +++ b/crates/ironrdp-blocking/src/lib.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate tracing; + +mod connector; +mod framed; +mod session; + +pub use connector::*; +pub use framed::*; +pub use session::*; diff --git a/crates/ironrdp-blocking/src/session.rs b/crates/ironrdp-blocking/src/session.rs new file mode 100644 index 000000000..5c31b31db --- /dev/null +++ b/crates/ironrdp-blocking/src/session.rs @@ -0,0 +1 @@ +// TODO: active session I/O helpers? I’m not yet sure we need that diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index 90b55d3d1..edb5e7f3e 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -52,7 +52,7 @@ tokio = { version = "1", features = ["full"]} # Utils chrono = "0.4" whoami = "1.4" -anyhow = "1.0" +anyhow = "1" smallvec = "1.10" tap = "1" semver = "1" diff --git a/crates/ironrdp-client/README.md b/crates/ironrdp-client/README.md index 1aa558980..a89d52b09 100644 --- a/crates/ironrdp-client/README.md +++ b/crates/ironrdp-client/README.md @@ -1,66 +1,41 @@ # IronRDP client -A command-line RDP client, which performs connection to an RDP server and decodes RFX graphical updates. -If IronRDP client encounters an error, then will return `error` exit code and print what caused -an error. +A full-fledged RDP client based on IronRDP crates suite, and implemented using non-blocking, asynchronous I/O. -## Prerequisites +## Sample usage -You need to enable RemoteFX on the target machine (see top-level [README](../README.md)). +```bash +ironrdp-client --username --password +``` + +## Configuring log filter directives -## Command-line Interface +The `IRONRDP_LOG` environment variable is used to set the log filter directives. +```bash +IRONRDP_LOG="info,ironrdp_connector=trace" ironrdp-client --username --password ``` -USAGE: - ironrdp_client [OPTIONS] --password --security-protocol ... --username -FLAGS: - -h, --help Prints help information - -v, --version Prints version information +See [`tracing-subscriber`’s documentation][tracing-doc] for more details. -OPTIONS: - --dig-product-id - Contains a value that uniquely identifies the client [default: ] +[tracing-doc]: https://docs.rs/tracing-subscriber/0.3.17/tracing_subscriber/filter/struct.EnvFilter.html#directives - -d, --domain An optional target RDP server domain name - --ime-file-name - The input method editor (IME) file name associated with the active input locale [default: ] +## Support for `SSLKEYLOGFILE` - --keyboard-functional-keys-count - The number of function keys on the keyboard [default: 12] +This client supports reading the `SSLKEYLOGFILE` environment variable. +When set, the TLS encryption secrets for the session will be dumped to the file specified +by the environment variable. +This file can be read by Wireshark so that in can decrypt the packets. - --keyboard-subtype - The keyboard subtype (an original equipment manufacturer-dependent value) [default: 0] +### Example - --keyboard-type - The keyboard type [default: ibm_enhanced] [possible values: ibm_pc_xt, olivetti_ico, ibm_pc_at, - ibm_enhanced, nokia1050, nokia9140, japanese] - --log-file - A file with IronRDP client logs [default: ironrdp_client.log] +```bash +SSLKEYLOGFILE=/tmp/tls-secrets ironrdp-client --username --password +``` - -p, --password A target RDP server user password - --security-protocol ... - Specify the security protocols to use [default: hybrid_ex] [possible values: ssl, hybrid, hybrid_ex] +### Usage in Wireshark - -u, --username A target RDP server user name +See this [awakecoding's repository][awakecoding-repository] explaining how to use the file in wireshark. -ARGS: - An address on which the client will connect. Format: : -``` +[awakecoding-repository]: https://github.com/awakecoding/wireshark-rdp#sslkeylogfile -It worth to notice that the client takes mandatory arguments as - - `` as first argument; - - `--username` or `-u`; - - `--password` or `-p`. - -## Sample Usage - -1. Run the RDP server (Windows RDP server, FreeRDP server, etc.); -2. Run the IronRDP client and specify the RDP server address, username and password: - ``` - cargo run 192.168.1.100:3389 -u SimpleUsername -p SimplePassword! - ``` -3. After the RDP Connection Sequence the client will start receive RFX updates -and save to the internal buffer. -In case of error, the client will print (for example) `RDP failed because of negotiation error: ...`. -Additional logs are available in `` (`ironrdp_client.log` by default). diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index d4cafb923..02e21788d 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -1,3 +1,4 @@ +use ironrdp::connector::sspi::network_client::reqwest_network_client::RequestClientFactory; use ironrdp::connector::{ConnectionResult, ConnectorResult}; use ironrdp::graphics::image_processing::PixelFormat; use ironrdp::pdu::input::fast_path::FastPathInputEvent; @@ -5,7 +6,6 @@ use ironrdp::session::image::DecodedImage; use ironrdp::session::{ActiveStage, ActiveStageOutput, SessionResult}; use ironrdp::{connector, session}; use smallvec::SmallVec; -use sspi::network_client::reqwest_network_client::RequestClientFactory; use tokio::net::TcpStream; use tokio::sync::mpsc; use winit::event_loop::EventLoopProxy; diff --git a/crates/ironrdp-connector/src/connection.rs b/crates/ironrdp-connector/src/connection.rs index 3a8eb84eb..20ee8f6e4 100644 --- a/crates/ironrdp-connector/src/connection.rs +++ b/crates/ironrdp-connector/src/connection.rs @@ -431,6 +431,8 @@ impl Sequence for ClientConnector { let ts_request_from_server = credssp::TsRequest::from_buffer(input) .map_err(|e| reason_err!("CredSSP", "TsRequest decode: {e}"))?; + debug!(message = ?ts_request_from_server, "Received"); + let result = credssp_client .process(ts_request_from_server) .map_err(|e| ConnectorError::new("CredSSP", ConnectorErrorKind::Credssp(e)))?; @@ -463,6 +465,8 @@ impl Sequence for ClientConnector { let early_user_auth_result = credssp::EarlyUserAuthResult::from_buffer(input) .map_err(|e| custom_err!("credssp::EarlyUserAuthResult", e))?; + debug!(message = ?early_user_auth_result, "Received"); + let credssp::EarlyUserAuthResult::Success = early_user_auth_result else { return Err(ConnectorError::new("CredSSP", ConnectorErrorKind::AccessDenied)); }; diff --git a/crates/ironrdp-futures/src/lib.rs b/crates/ironrdp-futures/src/lib.rs index 7a257d3c2..f00929c98 100644 --- a/crates/ironrdp-futures/src/lib.rs +++ b/crates/ironrdp-futures/src/lib.rs @@ -47,7 +47,7 @@ where Box::pin(async { // NOTE(perf): tokio implementation is more efficient let mut read_bytes = [0u8; 1024]; - let len = self.inner.read(&mut read_bytes[..]).await?; + let len = self.inner.read(&mut read_bytes).await?; buf.extend_from_slice(&read_bytes[..len]); Ok(len) diff --git a/crates/ironrdp-svc/src/lib.rs b/crates/ironrdp-svc/src/lib.rs index dd6df3035..54e092be1 100644 --- a/crates/ironrdp-svc/src/lib.rs +++ b/crates/ironrdp-svc/src/lib.rs @@ -65,7 +65,7 @@ pub trait StaticVirtualChannel: AsAny + fmt::Debug + Send + Sync { /// Returns the name of the `StaticVirtualChannel` fn channel_name(&self) -> ChannelName; - /// Defines which compression flag should be sent along the [`Channel`] Definition Structure (`CHANNEL_DEF`) + /// Defines which compression flag should be sent along the [`ChannelDef`] Definition Structure (`CHANNEL_DEF`) fn compression_condition(&self) -> CompressionCondition { CompressionCondition::Never } @@ -83,7 +83,7 @@ pub trait StaticVirtualChannel: AsAny + fmt::Debug + Send + Sync { assert_obj_safe!(StaticVirtualChannel); -/// Takes a vector of PDUs and breaks them into chunks prefixed with a [`ChannelPduHeader`]. +/// Takes a vector of PDUs and breaks them into chunks prefixed with a Channel PDU Header (`CHANNEL_PDU_HEADER`). /// /// Each chunk is at most `max_chunk_len` bytes long (not including the Channel PDU Header). pub fn chunkify(messages: Vec, max_chunk_len: usize) -> PduResult> { diff --git a/crates/ironrdp-tls/src/lib.rs b/crates/ironrdp-tls/src/lib.rs index a46e83153..dca5d31d6 100644 --- a/crates/ironrdp-tls/src/lib.rs +++ b/crates/ironrdp-tls/src/lib.rs @@ -14,10 +14,6 @@ where { #[cfg(feature = "rustls")] let mut tls_stream = { - // FIXME: disable TLS session resume just to be safe (not unsupported by CredSSP server) - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/385a7489-d46b-464c-b224-f7340e308a5c - // Option is available starting rustls 0.21 - let mut config = tokio_rustls::rustls::client::ClientConfig::builder() .with_safe_defaults() .with_custom_certificate_verifier(std::sync::Arc::new(danger::NoCertificateVerification)) @@ -26,6 +22,13 @@ where // This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret) config.key_log = std::sync::Arc::new(tokio_rustls::rustls::KeyLogFile::new()); + // Disable TLS resumption because it’s not supported by some services such as CredSSP. + // + // > The CredSSP Protocol does not extend the TLS wire protocol. TLS session resumption is not supported. + // + // source: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/385a7489-d46b-464c-b224-f7340e308a5c + config.resumption = tokio_rustls::rustls::client::Resumption::disabled(); + let config = std::sync::Arc::new(config); let server_name = server_name.try_into().unwrap(); diff --git a/crates/ironrdp-web/src/image.rs b/crates/ironrdp-web/src/image.rs index 155e2c997..876ee8cca 100644 --- a/crates/ironrdp-web/src/image.rs +++ b/crates/ironrdp-web/src/image.rs @@ -1,17 +1,5 @@ use ironrdp::pdu::geometry::{InclusiveRectangle, Rectangle as _}; use ironrdp::session::image::DecodedImage; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -pub struct RectInfo { - pub frame_id: usize, - pub top: u16, - pub left: u16, - pub right: u16, - pub bottom: u16, - pub width: u16, - pub height: u16, -} pub fn extract_partial_image(image: &DecodedImage, region: InclusiveRectangle) -> (InclusiveRectangle, Vec) { // PERF: needs actual benchmark to find a better heuristic diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml index e1e72c69e..6caef384a 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -11,6 +11,10 @@ authors.workspace = true keywords.workspace = true categories.workspace = true +[package.metadata.docs.rs] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] +all-features = true + [lib] doctest = false test = false @@ -44,5 +48,12 @@ ironrdp-dvc = { workspace = true, optional = true } ironrdp-rdpdr = { workspace = true, optional = true } ironrdp-rdpsnd = { workspace = true, optional = true } -[package.metadata.docs.rs] -all-features = true +[dev-dependencies] +ironrdp-blocking.workspace = true +anyhow = "1" +rustls = "0.21" +bmp = "0.5" +pico-args = "0.5" +x509-cert = { version = "0.2.1", default-features = false, features = ["std"] } +tracing.workspace = true +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/crates/ironrdp/examples/screenshot.rs b/crates/ironrdp/examples/screenshot.rs new file mode 100644 index 000000000..632e1933f --- /dev/null +++ b/crates/ironrdp/examples/screenshot.rs @@ -0,0 +1,379 @@ +//! Example of utilizing IronRDP in a blocking, synchronous fashion. +//! +//! This example showcases the use of IronRDP in a blocking manner. It +//! demonstrates how to create a basic RDP client with just a few hundred lines +//! of code by leveraging the IronRDP crates suite. +//! +//! In this basic client implementation, the client establishes a connection +//! with the destination server, decodes incoming graphics updates, and saves the +//! resulting output as a BMP image file on the local disk. +//! +//! ## Usage example +//! +//! cargo run --example=screenshot -- --host -u -p -o out.bmp + +#[macro_use] +extern crate tracing; + +use std::io::Write as _; +use std::net::TcpStream; +use std::path::PathBuf; +use std::time::Duration; + +use anyhow::Context as _; +use ironrdp::connector; +use ironrdp::connector::sspi::network_client::reqwest_network_client::RequestClientFactory; +use ironrdp::connector::ConnectionResult; +use ironrdp::pdu::gcc::KeyboardType; +use ironrdp::pdu::nego::SecurityProtocol; +use ironrdp::pdu::rdp::capability_sets::MajorPlatformType; +use ironrdp::session::image::DecodedImage; +use ironrdp::session::{ActiveStage, ActiveStageOutput}; + +const HELP: &str = "\ +USAGE: + cargo run --example=screenshot -- --host --port + -u/--username -p/--password + [-o/--output ] [-d/--domain ] +"; + +fn main() -> anyhow::Result<()> { + let action = match parse_args() { + Ok(action) => action, + Err(e) => { + println!("{HELP}"); + return Err(e.context("invalid argument(s)")); + } + }; + + setup_logging()?; + + match action { + Action::ShowHelp => { + println!("{HELP}"); + Ok(()) + } + Action::Run { + host, + port, + username, + password, + output, + domain, + } => { + info!(host, port, username, password, output = %output.display(), domain, "run"); + run(host, port, username, password, output, domain) + } + } +} + +#[derive(Debug)] +enum Action { + ShowHelp, + Run { + host: String, + port: u16, + username: String, + password: String, + output: PathBuf, + domain: Option, + }, +} + +fn parse_args() -> anyhow::Result { + let mut args = pico_args::Arguments::from_env(); + + let action = if args.contains(["-h", "--help"]) { + Action::ShowHelp + } else { + let host = args.value_from_str("--host")?; + let port = args.opt_value_from_str("--port")?.unwrap_or(3389); + let username = args.value_from_str(["-u", "--username"])?; + let password = args.value_from_str(["-p", "--password"])?; + let output = args + .opt_value_from_str(["-o", "--output"])? + .unwrap_or_else(|| PathBuf::from("out.bmp")); + let domain = args.opt_value_from_str(["-d", "--domain"])?; + + Action::Run { + host, + port, + username, + password, + output, + domain, + } + }; + + Ok(action) +} + +fn setup_logging() -> anyhow::Result<()> { + use tracing::metadata::LevelFilter; + use tracing_subscriber::prelude::*; + use tracing_subscriber::EnvFilter; + + let fmt_layer = tracing_subscriber::fmt::layer().compact(); + + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::WARN.into()) + .with_env_var("IRONRDP_LOG") + .from_env_lossy(); + + tracing_subscriber::registry() + .with(fmt_layer) + .with(env_filter) + .try_init() + .context("failed to set tracing global subscriber")?; + + Ok(()) +} + +fn run( + server_name: String, + port: u16, + username: String, + password: String, + output: PathBuf, + domain: Option, +) -> anyhow::Result<()> { + let config = build_config(username, password, domain); + + let (connection_result, framed) = connect(config, server_name, port).context("connect")?; + + let mut image = DecodedImage::new( + ironrdp_graphics::image_processing::PixelFormat::RgbA32, + connection_result.desktop_size.width, + connection_result.desktop_size.height, + ); + + active_stage(connection_result, framed, &mut image).context("active stage")?; + + let mut bmp = bmp::Image::new(u32::from(image.width()), u32::from(image.height())); + + image + .data() + .chunks_exact(usize::from(image.width() * 4)) + .enumerate() + .for_each(|(y, row)| { + row.chunks_exact(4).enumerate().for_each(|(x, pixel)| { + let r = pixel[0]; + let g = pixel[1]; + let b = pixel[2]; + let _a = pixel[3]; + bmp.set_pixel(x as u32, y as u32, bmp::Pixel::new(r, g, b)); + }) + }); + + bmp.save(output).context("save BMP image to disk")?; + + Ok(()) +} + +fn build_config(username: String, password: String, domain: Option) -> connector::Config { + connector::Config { + username, + password, + domain, + security_protocol: SecurityProtocol::HYBRID, + keyboard_type: KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_functional_keys_count: 12, + ime_file_name: String::new(), + dig_product_id: String::new(), + desktop_size: connector::DesktopSize { + width: 1280, + height: 1024, + }, + graphics: None, + bitmap: None, + client_build: 0, + client_name: "ironrdp-screenshot-example".to_owned(), + client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(), + + #[cfg(windows)] + platform: MajorPlatformType::WINDOWS, + #[cfg(target_os = "macos")] + platform: MajorPlatformType::MACINTOSH, + #[cfg(target_os = "ios")] + platform: MajorPlatformType::IOS, + #[cfg(target_os = "linux")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "android")] + platform: MajorPlatformType::ANDROID, + #[cfg(target_os = "freebsd")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "dragonfly")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "openbsd")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "netbsd")] + platform: MajorPlatformType::UNIX, + + no_server_pointer: false, + } +} + +type UpgradedFramed = ironrdp_blocking::Framed>; + +fn connect( + config: connector::Config, + server_name: String, + port: u16, +) -> anyhow::Result<(ConnectionResult, UpgradedFramed)> { + let server_addr = lookup_addr(&server_name, port).context("lookup addr")?; + + info!(%server_addr, "Looked up server address"); + + let tcp_stream = TcpStream::connect(server_addr).context("TCP connect")?; + + // Sets the read timeout for the TCP stream so we can break out of the + // infinite loop during the active stage once there is no more activity. + tcp_stream + .set_read_timeout(Some(Duration::from_secs(3))) + .expect("set_read_timeout call failed"); + + let mut framed = ironrdp_blocking::Framed::new(tcp_stream); + + let mut connector = connector::ClientConnector::new(config) + .with_server_addr(server_addr) + .with_server_name(&server_name) + .with_credssp_network_client(RequestClientFactory); + + let should_upgrade = ironrdp_blocking::connect_begin(&mut framed, &mut connector).context("begin connection")?; + + debug!("TLS upgrade"); + + // Ensure there is no leftover + let initial_stream = framed.into_inner_no_leftover(); + + let (upgraded_stream, server_public_key) = tls_upgrade(initial_stream, &server_name).context("TLS upgrade")?; + + let upgraded = ironrdp_blocking::mark_as_upgraded(should_upgrade, &mut connector, server_public_key); + + let mut upgraded_framed = ironrdp_blocking::Framed::new(upgraded_stream); + + let connection_result = + ironrdp_blocking::connect_finalize(upgraded, &mut upgraded_framed, connector).context("finalize connection")?; + + Ok((connection_result, upgraded_framed)) +} + +fn active_stage( + connection_result: ConnectionResult, + mut framed: UpgradedFramed, + image: &mut DecodedImage, +) -> anyhow::Result<()> { + let mut active_stage = ActiveStage::new(connection_result, None); + + 'outer: loop { + let (action, payload) = match framed.read_pdu() { + Ok((action, payload)) => (action, payload), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break 'outer, + Err(e) => return Err(anyhow::Error::new(e).context("read frame")), + }; + + trace!(?action, frame_length = payload.len(), "Frame received"); + + let outputs = active_stage.process(image, action, &payload)?; + + for out in outputs { + match out { + ActiveStageOutput::ResponseFrame(frame) => framed.write_all(&frame).context("write response")?, + ActiveStageOutput::Terminate => break 'outer, + _ => {} + } + } + } + + Ok(()) +} + +fn lookup_addr(hostname: &str, port: u16) -> anyhow::Result { + use std::net::ToSocketAddrs as _; + let addr = (hostname, port).to_socket_addrs()?.next().unwrap(); + Ok(addr) +} + +fn tls_upgrade( + stream: TcpStream, + server_name: &str, +) -> anyhow::Result<(rustls::StreamOwned, Vec)> { + let mut config = rustls::client::ClientConfig::builder() + .with_safe_defaults() + .with_custom_certificate_verifier(std::sync::Arc::new(danger::NoCertificateVerification)) + .with_no_client_auth(); + + // This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret) + config.key_log = std::sync::Arc::new(rustls::KeyLogFile::new()); + + // Disable TLS resumption because it’s not supported by some services such as CredSSP. + // + // > The CredSSP Protocol does not extend the TLS wire protocol. TLS session resumption is not supported. + // + // source: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/385a7489-d46b-464c-b224-f7340e308a5c + config.resumption = rustls::client::Resumption::disabled(); + + let config = std::sync::Arc::new(config); + + let server_name = server_name.try_into().unwrap(); + + let client = rustls::ClientConnection::new(config, server_name)?; + + let mut tls_stream = rustls::StreamOwned::new(client, stream); + + // We need to flush in order to ensure the TLS handshake is moving forward. Without flushing, + // it’s likely the peer certificate is not yet received a this point. + tls_stream.flush()?; + + let cert = tls_stream + .conn + .peer_certificates() + .and_then(|certificates| certificates.first()) + .context("peer certificate is missing")?; + + let server_public_key = extract_tls_server_public_key(&cert.0)?; + + Ok((tls_stream, server_public_key)) +} + +fn extract_tls_server_public_key(cert: &[u8]) -> anyhow::Result> { + use x509_cert::der::Decode as _; + + let cert = x509_cert::Certificate::from_der(cert)?; + + debug!(%cert.tbs_certificate.subject); + + let server_public_key = cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() + .context("subject public key BIT STRING is not aligned")? + .to_owned(); + + Ok(server_public_key) +} + +mod danger { + use std::time::SystemTime; + + use rustls::client::{ServerCertVerified, ServerCertVerifier}; + use rustls::{Certificate, Error, ServerName}; + + pub(super) struct NoCertificateVerification; + + impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + } +}