#![allow(clippy::print_stdout, clippy::print_stderr)]
use core::time::Duration;
use std::path::PathBuf;
use anyhow::Context as _;
use clap::Parser;
use clap::clap_derive::ValueEnum;
use ironrdp::client::config::{
ClipboardType as ResolvedClipboardType, Config, ConfigBuilder, Destination, DvcProxyInfo, MissingField,
TransportKind,
};
use ironrdp::pdu::rdp::capability_sets::{MajorPlatformType, client_codecs_capabilities};
use ironrdp_cfg::PropertySetExt as _;
use tap::prelude::*;
use url::Url;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum ClipboardType {
Enable,
Disable,
Stub,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum KeyboardType {
IbmPcXt,
OlivettiIco,
IbmPcAt,
IbmEnhanced,
Nokia1050,
Nokia9140,
Japanese,
}
impl KeyboardType {
fn into_pdu(self) -> ironrdp::pdu::gcc::KeyboardType {
match self {
KeyboardType::IbmEnhanced => ironrdp::pdu::gcc::KeyboardType::IbmEnhanced,
KeyboardType::IbmPcAt => ironrdp::pdu::gcc::KeyboardType::IbmPcAt,
KeyboardType::IbmPcXt => ironrdp::pdu::gcc::KeyboardType::IbmPcXt,
KeyboardType::OlivettiIco => ironrdp::pdu::gcc::KeyboardType::OlivettiIco,
KeyboardType::Nokia1050 => ironrdp::pdu::gcc::KeyboardType::Nokia1050,
KeyboardType::Nokia9140 => ironrdp::pdu::gcc::KeyboardType::Nokia9140,
KeyboardType::Japanese => ironrdp::pdu::gcc::KeyboardType::Japanese,
}
}
}
#[derive(Parser, Debug)]
#[clap(author = "Devolutions", about = "Devolutions-IronRDP viewer")]
#[clap(version, long_about = None)]
struct Args {
#[clap(short, long, value_parser)]
log_file: Option<String>,
#[clap(long, value_parser)]
gw_endpoint: Option<String>,
#[clap(long, value_parser)]
gw_user: Option<String>,
#[clap(long, value_parser)]
gw_pass: Option<String>,
destination: Option<Destination>,
#[clap(long)]
rdp_file: Option<PathBuf>,
#[clap(short, long)]
username: Option<String>,
#[clap(short, long)]
domain: Option<String>,
#[clap(short, long)]
password: Option<String>,
#[clap(long)]
rdcleanpath_url: Option<Url>,
#[clap(long, requires("rdcleanpath_url"))]
rdcleanpath_token: Option<String>,
#[clap(long, value_enum, default_value_t = KeyboardType::IbmEnhanced)]
keyboard_type: KeyboardType,
#[clap(long, default_value_t = 0)]
keyboard_subtype: u32,
#[clap(long, default_value_t = 12)]
keyboard_functional_keys_count: u32,
#[clap(long, default_value_t = String::from(""))]
ime_file_name: String,
#[clap(long, default_value_t = String::from(""))]
dig_product_id: String,
#[clap(long, value_parser = clap::value_parser!(u32).range(100..=500))]
scale_desktop: Option<u32>,
#[clap(long, value_parser = clap::value_parser!(u16).range(1..=8192))]
desktop_width: Option<u16>,
#[clap(long, value_parser = clap::value_parser!(u16).range(1..=8192))]
desktop_height: Option<u16>,
#[clap(long)]
color_depth: Option<u32>,
#[clap(long)]
no_server_pointer: bool,
#[clap(long)]
autologon: bool,
#[clap(long)]
no_tls: bool,
#[clap(long, alias = "no-nla")]
no_credssp: bool,
#[clap(long, value_enum, default_value_t = ClipboardType::Enable)]
clipboard_type: ClipboardType,
#[clap(long, num_args = 1.., value_delimiter = ',')]
codecs: Vec<String>,
#[clap(long)]
no_compression: bool,
#[clap(long, value_parser = clap::value_parser!(u32).range(0..=3))]
compression_level: Option<u32>,
#[clap(long)]
prevent_session_lock: Option<u32>,
#[clap(long)]
dvc_proxy: Vec<DvcProxyInfo>,
#[cfg(windows)]
#[clap(long)]
dvc_plugin: Vec<PathBuf>,
#[clap(long)]
dump_rdp: Option<PathBuf>,
}
pub struct ViewerConfig {
builder: ConfigBuilder,
log_file: Option<String>,
dump_rdp: Option<PathBuf>,
}
impl ViewerConfig {
pub fn parse_args() -> anyhow::Result<Self> {
Self::parse_from(std::env::args_os())
}
pub fn parse_from<I, T>(args: I) -> anyhow::Result<Self>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let args = Args::parse_from(args);
let mut properties = ironrdp_propertyset::PropertySet::new();
if let Some(rdp_file) = &args.rdp_file {
let input =
std::fs::read_to_string(rdp_file).with_context(|| format!("failed to read {}", rdp_file.display()))?;
if let Err(errors) = ironrdp_rdpfile::load(&mut properties, &input) {
for error in &errors {
eprintln!("Warning: skipped entry in {}: {error}", rdp_file.display());
}
}
}
let log_file = args.log_file.clone();
let dump_rdp = args.dump_rdp.clone();
let builder = ConfigBuilder::from_property_set(&properties)?;
let redirect_clipboard = properties.redirect_clipboard().unwrap_or(true);
let builder = apply_cli_to_builder(builder, args, redirect_clipboard);
Ok(Self {
builder,
log_file,
dump_rdp,
})
}
pub fn into_config(self) -> anyhow::Result<Config> {
let dump = self.dump_rdp.is_some();
prompt_missing(self.builder, dump)
}
pub fn log_file(&self) -> Option<&str> {
self.log_file.as_deref()
}
pub fn dump_rdp(&self) -> Option<&std::path::Path> {
self.dump_rdp.as_deref()
}
}
fn apply_cli_to_builder(mut builder: ConfigBuilder, args: Args, redirect_clipboard: bool) -> ConfigBuilder {
{
let codecs: Vec<_> = args.codecs.iter().map(String::as_str).collect();
if let Err(help) = client_codecs_capabilities(&codecs) {
print!("{help}");
std::process::exit(0);
}
}
if let Some(destination) = args.destination {
builder = builder.with_destination(destination);
}
if let Some(username) = args.username {
builder = builder.with_username(username);
}
if let Some(password) = args.password {
builder = builder.with_password(password);
}
if let Some(domain) = args.domain {
builder = builder.with_domain(domain);
}
if let Some(scale) = args.scale_desktop {
builder = builder.with_desktop_scale_factor(scale);
}
if let Some(width) = args.desktop_width {
builder = builder.with_desktop_width(width);
}
if let Some(height) = args.desktop_height {
builder = builder.with_desktop_height(height);
}
if let Some(color_depth) = args.color_depth {
builder = builder.with_color_depth(color_depth);
}
if args.no_credssp {
builder = builder.with_credssp(false);
}
if args.no_tls {
builder = builder.with_tls(false);
}
if args.no_server_pointer {
builder = builder.with_server_pointer(false);
}
if args.autologon {
builder = builder.with_autologon(true);
}
if args.no_compression {
builder = builder.with_compression(false);
}
if let Some(level) = args.compression_level {
builder = builder.with_compression_level(level);
}
if let Some(minutes) = args.prevent_session_lock {
builder = builder.with_fake_events_interval(Duration::from_secs(u64::from(minutes) * 60));
}
if let Some(url) = args.rdcleanpath_url {
builder = builder.with_transport(TransportKind::RDCleanPath { url });
if let Some(token) = args.rdcleanpath_token {
builder = builder.with_rdcleanpath_token(token);
}
} else if let Some(endpoint) = args.gw_endpoint {
builder = builder.with_transport(TransportKind::Gateway { endpoint });
if let Some(username) = args.gw_user {
builder = builder.with_gateway_username(username);
}
if let Some(password) = args.gw_pass {
builder = builder.with_gateway_password(password);
}
}
builder = builder.with_clipboard(resolve_clipboard_type(args.clipboard_type, redirect_clipboard));
builder = builder
.with_keyboard_type(args.keyboard_type.into_pdu())
.with_keyboard_subtype(args.keyboard_subtype)
.with_keyboard_functional_keys_count(args.keyboard_functional_keys_count)
.with_ime_file_name(args.ime_file_name)
.with_dig_product_id(args.dig_product_id)
.with_codecs(args.codecs);
for proxy in args.dvc_proxy {
builder = builder.with_dvc_pipe_proxy(proxy);
}
#[cfg(windows)]
for plugin in args.dvc_plugin {
builder = builder.with_dvc_plugin(plugin);
}
builder
}
fn prompt_missing(mut builder: ConfigBuilder, dump: bool) -> anyhow::Result<Config> {
const DUMP_SECRET_PLACEHOLDER: &str = "<stripped>";
for field in builder.missing() {
builder = match field {
MissingField::ServerAddress => {
let dest = inquire::Text::new("Server address:")
.prompt()
.context("Address prompt")?
.pipe(Destination::new)?;
builder.with_destination(dest)
}
MissingField::Username => {
let username = inquire::Text::new("Username:").prompt().context("Username prompt")?;
builder.with_username(username)
}
MissingField::Password if dump => builder.with_password(DUMP_SECRET_PLACEHOLDER),
MissingField::Password => {
let password = inquire::Password::new("Password:")
.without_confirmation()
.prompt()
.context("Password prompt")?;
builder.with_password(password)
}
MissingField::GatewayUsername => {
let username = inquire::Text::new("Gateway username:")
.prompt()
.context("Gateway username prompt")?;
builder.with_gateway_username(username)
}
MissingField::GatewayPassword if dump => builder.with_gateway_password(DUMP_SECRET_PLACEHOLDER),
MissingField::GatewayPassword => {
let password = inquire::Password::new("Gateway password:")
.without_confirmation()
.prompt()
.context("Gateway password prompt")?;
builder.with_gateway_password(password)
}
MissingField::RDCleanPathToken if dump => builder.with_rdcleanpath_token(DUMP_SECRET_PLACEHOLDER),
MissingField::RDCleanPathToken => {
let token = inquire::Text::new("RDCleanPath token:")
.prompt()
.context("RDCleanPath token prompt")?;
builder.with_rdcleanpath_token(token)
}
MissingField::ClientBuild => builder.with_client_build(client_build()),
MissingField::ClientDir => {
builder.with_client_dir("C:\\Windows\\System32\\mstscax.dll")
}
MissingField::Platform => builder.with_platform(current_platform()),
MissingField::ClientName => builder.with_client_name(client_name()),
};
}
builder.build()
}
fn client_build() -> u32 {
semver::Version::parse(env!("CARGO_PKG_VERSION"))
.map_or(0, |v| v.major * 100 + v.minor * 10 + v.patch)
.try_into()
.unwrap_or(0)
}
fn client_name() -> String {
whoami::hostname().unwrap_or_else(|_| "ironrdp".to_owned())
}
fn current_platform() -> MajorPlatformType {
match whoami::platform() {
whoami::Platform::Windows => MajorPlatformType::WINDOWS,
whoami::Platform::Linux => MajorPlatformType::UNIX,
whoami::Platform::Mac => MajorPlatformType::MACINTOSH,
whoami::Platform::Ios => MajorPlatformType::IOS,
whoami::Platform::Android => MajorPlatformType::ANDROID,
_ => MajorPlatformType::UNSPECIFIED,
}
}
pub fn parse_config() -> anyhow::Result<Config> {
ViewerConfig::parse_args()?.into_config()
}
pub fn parse_config_from<I, T>(args: I) -> anyhow::Result<Config>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
ViewerConfig::parse_from(args)?.into_config()
}
fn resolve_clipboard_type(cli: ClipboardType, redirect_clipboard: bool) -> ResolvedClipboardType {
if !redirect_clipboard {
return ResolvedClipboardType::Disable;
}
match cli {
ClipboardType::Enable => ResolvedClipboardType::Enable,
ClipboardType::Disable => ResolvedClipboardType::Disable,
ClipboardType::Stub => ResolvedClipboardType::Stub,
}
}