1#![allow(unused_crate_dependencies)] #![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, 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, 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 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 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 config.key_log = std::sync::Arc::new(rustls::KeyLogFile::new());
323
324 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 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}