screenshot/
screenshot.rs

1//! Example of utilizing IronRDP in a blocking, synchronous fashion.
2//!
3//! This example showcases the use of IronRDP in a blocking manner. It
4//! demonstrates how to create a basic RDP client with just a few hundred lines
5//! of code by leveraging the IronRDP crates suite.
6//!
7//! In this basic client implementation, the client establishes a connection
8//! with the destination server, decodes incoming graphics updates, and saves the
9//! resulting output as a PNG image file on the disk.
10//!
11//! # Usage example
12//!
13//! ```shell
14//! cargo run --example=screenshot -- --host <HOSTNAME> -u <USERNAME> -p <PASSWORD> -o out.png
15//! ```
16
17#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary
18#![allow(clippy::print_stdout)]
19
20use core::time::Duration;
21use std::io::Write as _;
22use std::net::TcpStream;
23use std::path::PathBuf;
24
25use anyhow::Context as _;
26use connector::Credentials;
27use ironrdp::connector;
28use ironrdp::connector::ConnectionResult;
29use ironrdp::pdu::gcc::KeyboardType;
30use ironrdp::pdu::rdp::capability_sets::MajorPlatformType;
31use ironrdp::session::image::DecodedImage;
32use ironrdp::session::{ActiveStage, ActiveStageOutput};
33use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo};
34use sspi::network_client::reqwest_network_client::ReqwestNetworkClient;
35use tokio_rustls::rustls;
36use tracing::{debug, info, trace};
37
38const HELP: &str = "\
39USAGE:
40  cargo run --example=screenshot -- --host <HOSTNAME> --port <PORT>
41                                    -u/--username <USERNAME> -p/--password <PASSWORD>
42                                    [-o/--output <OUTPUT_FILE>] [-d/--domain <DOMAIN>]
43";
44
45fn main() -> anyhow::Result<()> {
46    let action = match parse_args() {
47        Ok(action) => action,
48        Err(e) => {
49            println!("{HELP}");
50            return Err(e.context("invalid argument(s)"));
51        }
52    };
53
54    setup_logging()?;
55
56    match action {
57        Action::ShowHelp => {
58            println!("{HELP}");
59            Ok(())
60        }
61        Action::Run {
62            host,
63            port,
64            username,
65            password,
66            output,
67            domain,
68        } => {
69            info!(host, port, username, password, output = %output.display(), domain, "run");
70            run(host, port, username, password, output, domain)
71        }
72    }
73}
74
75#[derive(Debug)]
76enum Action {
77    ShowHelp,
78    Run {
79        host: String,
80        port: u16,
81        username: String,
82        password: String,
83        output: PathBuf,
84        domain: Option<String>,
85    },
86}
87
88fn parse_args() -> anyhow::Result<Action> {
89    let mut args = pico_args::Arguments::from_env();
90
91    let action = if args.contains(["-h", "--help"]) {
92        Action::ShowHelp
93    } else {
94        let host = args.value_from_str("--host")?;
95        let port = args.opt_value_from_str("--port")?.unwrap_or(3389);
96        let username = args.value_from_str(["-u", "--username"])?;
97        let password = args.value_from_str(["-p", "--password"])?;
98        let output = args
99            .opt_value_from_str(["-o", "--output"])?
100            .unwrap_or_else(|| PathBuf::from("out.png"));
101        let domain = args.opt_value_from_str(["-d", "--domain"])?;
102
103        Action::Run {
104            host,
105            port,
106            username,
107            password,
108            output,
109            domain,
110        }
111    };
112
113    Ok(action)
114}
115
116fn setup_logging() -> anyhow::Result<()> {
117    use tracing::metadata::LevelFilter;
118    use tracing_subscriber::prelude::*;
119    use tracing_subscriber::EnvFilter;
120
121    let fmt_layer = tracing_subscriber::fmt::layer().compact();
122
123    let env_filter = EnvFilter::builder()
124        .with_default_directive(LevelFilter::WARN.into())
125        .with_env_var("IRONRDP_LOG")
126        .from_env_lossy();
127
128    tracing_subscriber::registry()
129        .with(fmt_layer)
130        .with(env_filter)
131        .try_init()
132        .context("failed to set tracing global subscriber")?;
133
134    Ok(())
135}
136
137fn run(
138    server_name: String,
139    port: u16,
140    username: String,
141    password: String,
142    output: PathBuf,
143    domain: Option<String>,
144) -> anyhow::Result<()> {
145    let config = build_config(username, password, domain);
146
147    let (connection_result, framed) = connect(config, server_name, port).context("connect")?;
148
149    let mut image = DecodedImage::new(
150        ironrdp_graphics::image_processing::PixelFormat::RgbA32,
151        connection_result.desktop_size.width,
152        connection_result.desktop_size.height,
153    );
154
155    active_stage(connection_result, framed, &mut image).context("active stage")?;
156
157    let img: image::ImageBuffer<image::Rgba<u8>, _> =
158        image::ImageBuffer::from_raw(u32::from(image.width()), u32::from(image.height()), image.data())
159            .context("invalid image")?;
160
161    img.save(output).context("save image to disk")?;
162
163    Ok(())
164}
165
166fn build_config(username: String, password: String, domain: Option<String>) -> connector::Config {
167    connector::Config {
168        credentials: Credentials::UsernamePassword { username, password },
169        domain,
170        enable_tls: false, // This example does not expose any frontend.
171        enable_credssp: true,
172        keyboard_type: KeyboardType::IbmEnhanced,
173        keyboard_subtype: 0,
174        keyboard_layout: 0,
175        keyboard_functional_keys_count: 12,
176        ime_file_name: String::new(),
177        dig_product_id: String::new(),
178        desktop_size: connector::DesktopSize {
179            width: 1280,
180            height: 1024,
181        },
182        bitmap: None,
183        client_build: 0,
184        client_name: "ironrdp-screenshot-example".to_owned(),
185        client_dir: "C:\\Windows\\System32\\mstscax.dll".to_owned(),
186
187        #[cfg(windows)]
188        platform: MajorPlatformType::WINDOWS,
189        #[cfg(target_os = "macos")]
190        platform: MajorPlatformType::MACINTOSH,
191        #[cfg(target_os = "ios")]
192        platform: MajorPlatformType::IOS,
193        #[cfg(target_os = "linux")]
194        platform: MajorPlatformType::UNIX,
195        #[cfg(target_os = "android")]
196        platform: MajorPlatformType::ANDROID,
197        #[cfg(target_os = "freebsd")]
198        platform: MajorPlatformType::UNIX,
199        #[cfg(target_os = "dragonfly")]
200        platform: MajorPlatformType::UNIX,
201        #[cfg(target_os = "openbsd")]
202        platform: MajorPlatformType::UNIX,
203        #[cfg(target_os = "netbsd")]
204        platform: MajorPlatformType::UNIX,
205
206        enable_server_pointer: false, // Disable custom pointers (there is no user interaction anyway).
207        request_data: None,
208        autologon: false,
209        enable_audio_playback: false,
210        pointer_software_rendering: true,
211        performance_flags: PerformanceFlags::default(),
212        desktop_scale_factor: 0,
213        hardware_id: None,
214        license_cache: None,
215        timezone_info: TimezoneInfo::default(),
216    }
217}
218
219type UpgradedFramed = ironrdp_blocking::Framed<rustls::StreamOwned<rustls::ClientConnection, TcpStream>>;
220
221fn connect(
222    config: connector::Config,
223    server_name: String,
224    port: u16,
225) -> anyhow::Result<(ConnectionResult, UpgradedFramed)> {
226    let server_addr = lookup_addr(&server_name, port).context("lookup addr")?;
227
228    info!(%server_addr, "Looked up server address");
229
230    let tcp_stream = TcpStream::connect(server_addr).context("TCP connect")?;
231
232    // Sets the read timeout for the TCP stream so we can break out of the
233    // infinite loop during the active stage once there is no more activity.
234    tcp_stream
235        .set_read_timeout(Some(Duration::from_secs(3)))
236        .expect("set_read_timeout call failed");
237
238    let client_addr = tcp_stream.local_addr().context("get socket local address")?;
239
240    let mut framed = ironrdp_blocking::Framed::new(tcp_stream);
241
242    let mut connector = connector::ClientConnector::new(config, client_addr);
243
244    let should_upgrade = ironrdp_blocking::connect_begin(&mut framed, &mut connector).context("begin connection")?;
245
246    debug!("TLS upgrade");
247
248    // Ensure there is no leftover
249    let initial_stream = framed.into_inner_no_leftover();
250
251    let (upgraded_stream, server_public_key) =
252        tls_upgrade(initial_stream, server_name.clone()).context("TLS upgrade")?;
253
254    let upgraded = ironrdp_blocking::mark_as_upgraded(should_upgrade, &mut connector);
255
256    let mut upgraded_framed = ironrdp_blocking::Framed::new(upgraded_stream);
257
258    let mut network_client = ReqwestNetworkClient;
259    let connection_result = ironrdp_blocking::connect_finalize(
260        upgraded,
261        connector,
262        &mut upgraded_framed,
263        &mut network_client,
264        server_name.into(),
265        server_public_key,
266        None,
267    )
268    .context("finalize connection")?;
269
270    Ok((connection_result, upgraded_framed))
271}
272
273fn active_stage(
274    connection_result: ConnectionResult,
275    mut framed: UpgradedFramed,
276    image: &mut DecodedImage,
277) -> anyhow::Result<()> {
278    let mut active_stage = ActiveStage::new(connection_result);
279
280    'outer: loop {
281        let (action, payload) = match framed.read_pdu() {
282            Ok((action, payload)) => (action, payload),
283            Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break 'outer,
284            Err(e) => return Err(anyhow::Error::new(e).context("read frame")),
285        };
286
287        trace!(?action, frame_length = payload.len(), "Frame received");
288
289        let outputs = active_stage.process(image, action, &payload)?;
290
291        for out in outputs {
292            match out {
293                ActiveStageOutput::ResponseFrame(frame) => framed.write_all(&frame).context("write response")?,
294                ActiveStageOutput::Terminate(_) => break 'outer,
295                _ => {}
296            }
297        }
298    }
299
300    Ok(())
301}
302
303fn lookup_addr(hostname: &str, port: u16) -> anyhow::Result<core::net::SocketAddr> {
304    use std::net::ToSocketAddrs as _;
305    let addr = (hostname, port)
306        .to_socket_addrs()?
307        .next()
308        .context("socket address not found")?;
309    Ok(addr)
310}
311
312fn tls_upgrade(
313    stream: TcpStream,
314    server_name: String,
315) -> anyhow::Result<(rustls::StreamOwned<rustls::ClientConnection, TcpStream>, Vec<u8>)> {
316    let mut config = rustls::client::ClientConfig::builder()
317        .dangerous()
318        .with_custom_certificate_verifier(std::sync::Arc::new(danger::NoCertificateVerification))
319        .with_no_client_auth();
320
321    // This adds support for the SSLKEYLOGFILE env variable (https://wiki.wireshark.org/TLS#using-the-pre-master-secret)
322    config.key_log = std::sync::Arc::new(rustls::KeyLogFile::new());
323
324    // Disable TLS resumption because it’s not supported by some services such as CredSSP.
325    //
326    // > The CredSSP Protocol does not extend the TLS wire protocol. TLS session resumption is not supported.
327    //
328    // source: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/385a7489-d46b-464c-b224-f7340e308a5c
329    config.resumption = rustls::client::Resumption::disabled();
330
331    let config = std::sync::Arc::new(config);
332
333    let server_name = server_name.try_into()?;
334
335    let client = rustls::ClientConnection::new(config, server_name)?;
336
337    let mut tls_stream = rustls::StreamOwned::new(client, stream);
338
339    // We need to flush in order to ensure the TLS handshake is moving forward. Without flushing,
340    // it’s likely the peer certificate is not yet received a this point.
341    tls_stream.flush()?;
342
343    let cert = tls_stream
344        .conn
345        .peer_certificates()
346        .and_then(|certificates| certificates.first())
347        .context("peer certificate is missing")?;
348
349    let server_public_key = extract_tls_server_public_key(cert)?;
350
351    Ok((tls_stream, server_public_key))
352}
353
354fn extract_tls_server_public_key(cert: &[u8]) -> anyhow::Result<Vec<u8>> {
355    use x509_cert::der::Decode as _;
356
357    let cert = x509_cert::Certificate::from_der(cert)?;
358
359    debug!(%cert.tbs_certificate.subject);
360
361    let server_public_key = cert
362        .tbs_certificate
363        .subject_public_key_info
364        .subject_public_key
365        .as_bytes()
366        .context("subject public key BIT STRING is not aligned")?
367        .to_owned();
368
369    Ok(server_public_key)
370}
371
372mod danger {
373    use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
374    use tokio_rustls::rustls::{pki_types, DigitallySignedStruct, Error, SignatureScheme};
375
376    #[derive(Debug)]
377    pub(super) struct NoCertificateVerification;
378
379    impl ServerCertVerifier for NoCertificateVerification {
380        fn verify_server_cert(
381            &self,
382            _: &pki_types::CertificateDer<'_>,
383            _: &[pki_types::CertificateDer<'_>],
384            _: &pki_types::ServerName<'_>,
385            _: &[u8],
386            _: pki_types::UnixTime,
387        ) -> Result<ServerCertVerified, Error> {
388            Ok(ServerCertVerified::assertion())
389        }
390
391        fn verify_tls12_signature(
392            &self,
393            _: &[u8],
394            _: &pki_types::CertificateDer<'_>,
395            _: &DigitallySignedStruct,
396        ) -> Result<HandshakeSignatureValid, Error> {
397            Ok(HandshakeSignatureValid::assertion())
398        }
399
400        fn verify_tls13_signature(
401            &self,
402            _: &[u8],
403            _: &pki_types::CertificateDer<'_>,
404            _: &DigitallySignedStruct,
405        ) -> Result<HandshakeSignatureValid, Error> {
406            Ok(HandshakeSignatureValid::assertion())
407        }
408
409        fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
410            vec![
411                SignatureScheme::RSA_PKCS1_SHA1,
412                SignatureScheme::ECDSA_SHA1_Legacy,
413                SignatureScheme::RSA_PKCS1_SHA256,
414                SignatureScheme::ECDSA_NISTP256_SHA256,
415                SignatureScheme::RSA_PKCS1_SHA384,
416                SignatureScheme::ECDSA_NISTP384_SHA384,
417                SignatureScheme::RSA_PKCS1_SHA512,
418                SignatureScheme::ECDSA_NISTP521_SHA512,
419                SignatureScheme::RSA_PSS_SHA256,
420                SignatureScheme::RSA_PSS_SHA384,
421                SignatureScheme::RSA_PSS_SHA512,
422                SignatureScheme::ED25519,
423                SignatureScheme::ED448,
424            ]
425        }
426    }
427}