mirror of
https://github.com/Devolutions/IronRDP.git
synced 2025-08-04 07:08:17 +00:00

Adds support for license caching by storing the license obtained from SERVER_UPGRADE_LICENSE message and sending CLIENT_LICENSE_INFO if a license requested by the server is already stored in the cache. Co-authored-by: Benoît Cortier <3809077+CBenoit@users.noreply.github.com>
422 lines
14 KiB
Rust
422 lines
14 KiB
Rust
//! 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 PNG image file on the disk.
|
||
//!
|
||
//! # Usage example
|
||
//!
|
||
//! ```shell
|
||
//! cargo run --example=screenshot -- --host <HOSTNAME> -u <USERNAME> -p <PASSWORD> -o out.png
|
||
//! ```
|
||
|
||
#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary
|
||
#![allow(clippy::print_stdout)]
|
||
|
||
#[macro_use]
|
||
extern crate tracing;
|
||
|
||
use anyhow::Context as _;
|
||
use connector::Credentials;
|
||
use core::time::Duration;
|
||
use ironrdp::connector;
|
||
use ironrdp::connector::ConnectionResult;
|
||
use ironrdp::pdu::gcc::KeyboardType;
|
||
use ironrdp::pdu::rdp::capability_sets::MajorPlatformType;
|
||
use ironrdp::session::image::DecodedImage;
|
||
use ironrdp::session::{ActiveStage, ActiveStageOutput};
|
||
use ironrdp_pdu::rdp::client_info::PerformanceFlags;
|
||
use sspi::network_client::reqwest_network_client::ReqwestNetworkClient;
|
||
use std::io::Write as _;
|
||
use std::net::TcpStream;
|
||
use std::path::PathBuf;
|
||
use tokio_rustls::rustls;
|
||
|
||
const HELP: &str = "\
|
||
USAGE:
|
||
cargo run --example=screenshot -- --host <HOSTNAME> --port <PORT>
|
||
-u/--username <USERNAME> -p/--password <PASSWORD>
|
||
[-o/--output <OUTPUT_FILE>] [-d/--domain <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<String>,
|
||
},
|
||
}
|
||
|
||
fn parse_args() -> anyhow::Result<Action> {
|
||
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.png"));
|
||
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<String>,
|
||
) -> 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 img: image::ImageBuffer<image::Rgba<u8>, _> =
|
||
image::ImageBuffer::from_raw(u32::from(image.width()), u32::from(image.height()), image.data())
|
||
.context("invalid image")?;
|
||
|
||
img.save(output).context("save image to disk")?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn build_config(username: String, password: String, domain: Option<String>) -> connector::Config {
|
||
connector::Config {
|
||
credentials: Credentials::UsernamePassword { username, password },
|
||
domain,
|
||
enable_tls: false, // This example does not expose any frontend.
|
||
enable_credssp: true,
|
||
keyboard_type: KeyboardType::IbmEnhanced,
|
||
keyboard_subtype: 0,
|
||
keyboard_layout: 0,
|
||
keyboard_functional_keys_count: 12,
|
||
ime_file_name: String::new(),
|
||
dig_product_id: String::new(),
|
||
desktop_size: connector::DesktopSize {
|
||
width: 1280,
|
||
height: 1024,
|
||
},
|
||
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,
|
||
|
||
// Disable custom pointers (there is no user interaction anyway)
|
||
no_server_pointer: true,
|
||
request_data: None,
|
||
autologon: false,
|
||
pointer_software_rendering: true,
|
||
performance_flags: PerformanceFlags::default(),
|
||
desktop_scale_factor: 0,
|
||
hardware_id: None,
|
||
license_cache: None,
|
||
}
|
||
}
|
||
|
||
type UpgradedFramed = ironrdp_blocking::Framed<rustls::StreamOwned<rustls::ClientConnection, TcpStream>>;
|
||
|
||
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);
|
||
|
||
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.clone()).context("TLS upgrade")?;
|
||
|
||
let upgraded = ironrdp_blocking::mark_as_upgraded(should_upgrade, &mut connector);
|
||
|
||
let mut upgraded_framed = ironrdp_blocking::Framed::new(upgraded_stream);
|
||
|
||
let mut network_client = ReqwestNetworkClient;
|
||
let connection_result = ironrdp_blocking::connect_finalize(
|
||
upgraded,
|
||
&mut upgraded_framed,
|
||
connector,
|
||
server_name.into(),
|
||
server_public_key,
|
||
&mut network_client,
|
||
None,
|
||
)
|
||
.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);
|
||
|
||
'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<std::net::SocketAddr> {
|
||
use std::net::ToSocketAddrs as _;
|
||
let addr = (hostname, port).to_socket_addrs()?.next().unwrap();
|
||
Ok(addr)
|
||
}
|
||
|
||
fn tls_upgrade(
|
||
stream: TcpStream,
|
||
server_name: String,
|
||
) -> anyhow::Result<(rustls::StreamOwned<rustls::ClientConnection, TcpStream>, Vec<u8>)> {
|
||
let mut config = rustls::client::ClientConfig::builder()
|
||
.dangerous()
|
||
.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)?;
|
||
|
||
Ok((tls_stream, server_public_key))
|
||
}
|
||
|
||
fn extract_tls_server_public_key(cert: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||
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 tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||
use tokio_rustls::rustls::{pki_types, DigitallySignedStruct, Error, SignatureScheme};
|
||
|
||
#[derive(Debug)]
|
||
pub(super) struct NoCertificateVerification;
|
||
|
||
impl ServerCertVerifier for NoCertificateVerification {
|
||
fn verify_server_cert(
|
||
&self,
|
||
_: &pki_types::CertificateDer<'_>,
|
||
_: &[pki_types::CertificateDer<'_>],
|
||
_: &pki_types::ServerName<'_>,
|
||
_: &[u8],
|
||
_: pki_types::UnixTime,
|
||
) -> Result<ServerCertVerified, Error> {
|
||
Ok(ServerCertVerified::assertion())
|
||
}
|
||
|
||
fn verify_tls12_signature(
|
||
&self,
|
||
_: &[u8],
|
||
_: &pki_types::CertificateDer<'_>,
|
||
_: &DigitallySignedStruct,
|
||
) -> Result<HandshakeSignatureValid, Error> {
|
||
Ok(HandshakeSignatureValid::assertion())
|
||
}
|
||
|
||
fn verify_tls13_signature(
|
||
&self,
|
||
_: &[u8],
|
||
_: &pki_types::CertificateDer<'_>,
|
||
_: &DigitallySignedStruct,
|
||
) -> Result<HandshakeSignatureValid, Error> {
|
||
Ok(HandshakeSignatureValid::assertion())
|
||
}
|
||
|
||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||
vec![
|
||
SignatureScheme::RSA_PKCS1_SHA1,
|
||
SignatureScheme::ECDSA_SHA1_Legacy,
|
||
SignatureScheme::RSA_PKCS1_SHA256,
|
||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||
SignatureScheme::RSA_PKCS1_SHA384,
|
||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||
SignatureScheme::RSA_PKCS1_SHA512,
|
||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||
SignatureScheme::RSA_PSS_SHA256,
|
||
SignatureScheme::RSA_PSS_SHA384,
|
||
SignatureScheme::RSA_PSS_SHA512,
|
||
SignatureScheme::ED25519,
|
||
SignatureScheme::ED448,
|
||
]
|
||
}
|
||
}
|
||
}
|