use core::fmt;
use core::str::FromStr;
use core::time::Duration;
#[cfg(all(windows, feature = "dvc-com-plugin"))]
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Context as _;
use ironrdp_cfg::PropertySetExt as _;
use ironrdp_propertyset::PropertySet;
use url::Url;
type StaticChannelFn = Arc<dyn Fn(&mut ironrdp_connector::ClientConnector, &PropertySet) + Send + Sync>;
type DvcChannelFn = Arc<dyn Fn(&mut ironrdp_dvc::DrdynvcClient, &PropertySet) + Send + Sync>;
#[derive(Default)]
pub(crate) struct ExtensionRegistry {
pub(crate) static_channels: Vec<StaticChannelFn>,
pub(crate) dvc_channels: Vec<DvcChannelFn>,
}
impl Clone for ExtensionRegistry {
fn clone(&self) -> Self {
Self {
static_channels: self.static_channels.clone(),
dvc_channels: self.dvc_channels.clone(),
}
}
}
impl fmt::Debug for ExtensionRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ExtensionRegistry")
.field("static_channels", &self.static_channels.len())
.field("dvc_channels", &self.dvc_channels.len())
.finish()
}
}
#[derive(Clone)]
pub struct Config {
pub(crate) connector: ironrdp_connector::Config,
pub(crate) destination: Destination,
pub(crate) transport: Transport,
pub(crate) kerberos_config: Option<ironrdp_connector::credssp::KerberosConfig>,
pub(crate) fake_events_interval: Option<Duration>,
pub(crate) channels: ChannelConfig,
#[cfg(feature = "dvc-pipe-proxy")]
pub(crate) dvc_pipe_proxies: Vec<DvcProxyInfo>,
#[cfg(all(windows, feature = "dvc-com-plugin"))]
pub(crate) dvc_plugins: Vec<PathBuf>,
pub(crate) properties: PropertySet,
pub(crate) extensions: ExtensionRegistry,
}
impl Config {
pub fn connector(&self) -> &ironrdp_connector::Config {
&self.connector
}
pub fn destination(&self) -> &Destination {
&self.destination
}
pub fn transport(&self) -> &Transport {
&self.transport
}
pub fn kerberos_config(&self) -> Option<&ironrdp_connector::credssp::KerberosConfig> {
self.kerberos_config.as_ref()
}
pub fn fake_events_interval(&self) -> Option<Duration> {
self.fake_events_interval
}
pub fn channels(&self) -> &ChannelConfig {
&self.channels
}
#[cfg(feature = "dvc-pipe-proxy")]
pub fn dvc_pipe_proxies(&self) -> &[DvcProxyInfo] {
&self.dvc_pipe_proxies
}
#[cfg(all(windows, feature = "dvc-com-plugin"))]
pub fn dvc_plugins(&self) -> &[PathBuf] {
&self.dvc_plugins
}
pub fn properties(&self) -> &PropertySet {
&self.properties
}
}
impl fmt::Debug for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Config");
s.field("connector", &self.connector);
s.field("destination", &self.destination);
s.field("transport", &self.transport);
s.field("kerberos_config", &self.kerberos_config);
s.field("fake_events_interval", &self.fake_events_interval);
s.field("channels", &self.channels);
#[cfg(feature = "dvc-pipe-proxy")]
s.field("dvc_pipe_proxies", &self.dvc_pipe_proxies);
#[cfg(all(windows, feature = "dvc-com-plugin"))]
s.field("dvc_plugins", &self.dvc_plugins);
s.field("extensions", &self.extensions);
s.finish()
}
}
#[cfg(feature = "clipboard")]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ClipboardType {
Enable,
Disable,
Stub,
}
#[derive(Clone, Debug)]
pub struct ChannelConfig {
#[cfg(feature = "sound")]
pub sound: bool,
#[cfg(feature = "clipboard")]
pub clipboard: ClipboardType,
#[cfg(feature = "rdpdr")]
pub rdpdr: RdpdrConfig,
#[cfg(feature = "qoi")]
pub qoi: bool,
#[cfg(feature = "qoiz")]
pub qoiz: bool,
}
#[cfg_attr(
not(any(feature = "sound", feature = "clipboard", feature = "qoi", feature = "qoiz")),
expect(
clippy::derivable_impls,
reason = "fields setting non-default values are feature-gated; the impl is only trivially derivable in some feature combinations"
)
)]
impl Default for ChannelConfig {
fn default() -> Self {
Self {
#[cfg(feature = "sound")]
sound: true,
#[cfg(feature = "clipboard")]
clipboard: ClipboardType::Enable,
#[cfg(feature = "rdpdr")]
rdpdr: RdpdrConfig::default(),
#[cfg(feature = "qoi")]
qoi: true,
#[cfg(feature = "qoiz")]
qoiz: true,
}
}
}
#[cfg(feature = "rdpdr")]
#[derive(Clone, Debug)]
pub struct RdpdrConfig {
pub enabled: bool,
#[cfg(feature = "smartcard")]
pub smartcard: bool,
}
#[cfg(feature = "rdpdr")]
impl Default for RdpdrConfig {
fn default() -> Self {
Self {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: true,
}
}
}
#[derive(Clone, Debug, Default)]
pub enum Transport {
#[default]
Direct,
#[cfg(feature = "gateway")]
Gateway(GatewayConfig),
RDCleanPath(RDCleanPathConfig),
}
#[derive(Clone, Debug, Default)]
pub enum TransportKind {
#[default]
Direct,
#[cfg(feature = "gateway")]
Gateway {
endpoint: String,
},
RDCleanPath {
url: Url,
},
}
#[cfg(feature = "gateway")]
#[derive(Clone, Debug)]
pub struct GatewayConfig {
pub endpoint: String,
pub username: String,
pub password: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Destination {
name: String,
port: u16,
}
impl Destination {
pub fn new(addr: impl Into<String>) -> anyhow::Result<Self> {
const RDP_DEFAULT_PORT: u16 = 3389;
let addr = addr.into();
if let Some(addr_split) = addr.rsplit_once(':') {
if let Ok(sock_addr) = addr.parse::<core::net::SocketAddr>() {
Ok(Self {
name: sock_addr.ip().to_string(),
port: sock_addr.port(),
})
} else if addr.parse::<core::net::Ipv6Addr>().is_ok() {
Ok(Self {
name: addr,
port: RDP_DEFAULT_PORT,
})
} else {
Ok(Self {
name: addr_split.0.to_owned(),
port: addr_split.1.parse().context("invalid port")?,
})
}
} else {
Ok(Self {
name: addr,
port: RDP_DEFAULT_PORT,
})
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn port(&self) -> u16 {
self.port
}
pub fn from_parts(name: impl Into<String>, port: u16) -> Self {
Self {
name: name.into(),
port,
}
}
}
impl fmt::Display for Destination {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.name.parse::<core::net::Ipv6Addr>().is_ok() {
write!(f, "[{}]:{}", self.name, self.port)
} else {
write!(f, "{}:{}", self.name, self.port)
}
}
}
impl FromStr for Destination {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl From<Destination> for ironrdp_connector::ServerName {
fn from(value: Destination) -> Self {
Self::new(value.name)
}
}
impl From<&Destination> for ironrdp_connector::ServerName {
fn from(value: &Destination) -> Self {
Self::new(&value.name)
}
}
#[derive(Clone, Debug)]
pub struct RDCleanPathConfig {
pub url: Url,
pub auth_token: String,
}
#[derive(Clone, Debug)]
pub struct DvcProxyInfo {
pub channel_name: String,
pub pipe_name: String,
}
impl FromStr for DvcProxyInfo {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (channel_name, pipe_name) = s
.split_once('=')
.context("missing '=' delimiter in DVC proxy specification")?;
Ok(Self {
channel_name: channel_name.to_owned(),
pipe_name: pipe_name.to_owned(),
})
}
}
const RDP_DEFAULT_PORT: u16 = 3389;
const DEFAULT_WIDTH: u16 = 1280;
const DEFAULT_HEIGHT: u16 = 720;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MissingField {
ServerAddress,
Username,
Password,
GatewayUsername,
GatewayPassword,
RDCleanPathToken,
ClientBuild,
ClientDir,
Platform,
ClientName,
}
impl fmt::Display for MissingField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::ServerAddress => "server address",
Self::Username => "username",
Self::Password => "password",
Self::GatewayUsername => "gateway username",
Self::GatewayPassword => "gateway password",
Self::RDCleanPathToken => "RDCleanPath token",
Self::ClientBuild => "client build",
Self::ClientDir => "client dir",
Self::Platform => "platform",
Self::ClientName => "client name",
};
f.write_str(s)
}
}
#[derive(Default)]
pub struct ConfigBuilder {
destination: Option<Destination>,
username: Option<String>,
password: Option<String>,
client_build: Option<u32>,
client_dir: Option<String>,
client_name: Option<String>,
platform: Option<ironrdp_pdu::rdp::capability_sets::MajorPlatformType>,
gateway_username: Option<String>,
gateway_password: Option<String>,
domain: Option<String>,
enable_tls: Option<bool>,
enable_credssp: Option<bool>,
keyboard_type: Option<ironrdp_pdu::gcc::KeyboardType>,
keyboard_subtype: Option<u32>,
keyboard_functional_keys_count: Option<u32>,
ime_file_name: Option<String>,
dig_product_id: Option<String>,
desktop_width: Option<u16>,
desktop_height: Option<u16>,
desktop_scale_factor: Option<u32>,
color_depth: Option<u32>,
codecs: Vec<String>,
autologon: Option<bool>,
enable_server_pointer: Option<bool>,
pointer_software_rendering: Option<bool>,
enable_audio_playback: Option<bool>,
compression_type: Option<ironrdp_pdu::rdp::client_info::CompressionType>,
compression_enabled: Option<bool>,
alternate_shell: Option<String>,
work_dir: Option<String>,
transport: TransportKind,
rdcleanpath_token: Option<String>,
kerberos_config: Option<ironrdp_connector::credssp::KerberosConfig>,
fake_events_interval: Option<Duration>,
channels: ChannelConfig,
#[cfg(feature = "dvc-pipe-proxy")]
dvc_pipe_proxies: Vec<DvcProxyInfo>,
#[cfg(all(windows, feature = "dvc-com-plugin"))]
dvc_plugins: Vec<PathBuf>,
properties: PropertySet,
extensions: ExtensionRegistry,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_destination(mut self, destination: Destination) -> Self {
let host = match destination.name.parse::<core::net::IpAddr>() {
Ok(ip) => ironrdp_cfg::TargetHost::Ip(ip),
Err(_) => ironrdp_cfg::TargetHost::Domain(destination.name.clone()),
};
self.properties
.set_full_address(&ironrdp_cfg::TargetAddr { host, port: None });
self.properties.set_server_port(destination.port);
self.properties.clear_alternate_full_address();
self.destination = Some(destination);
self
}
#[must_use]
pub fn with_username(mut self, username: impl Into<String>) -> Self {
let username = username.into();
self.username = Some(username.clone());
self.properties.set_username(username);
self
}
#[must_use]
pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
let domain = domain.into();
self.properties.set_domain(domain.clone());
self.domain = Some(domain);
self
}
#[must_use]
pub fn with_password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
#[must_use]
pub fn with_gateway_username(mut self, username: impl Into<String>) -> Self {
let username = username.into();
self.gateway_username = Some(username.clone());
self.properties.set_gateway_username(username);
self
}
#[must_use]
pub fn with_gateway_password(mut self, password: impl Into<String>) -> Self {
self.gateway_password = Some(password.into());
self
}
#[must_use]
pub fn with_client_build(mut self, build: u32) -> Self {
self.client_build = Some(build);
self
}
#[must_use]
pub fn with_client_dir(mut self, dir: impl Into<String>) -> Self {
self.client_dir = Some(dir.into());
self
}
#[must_use]
pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
self.client_name = Some(name.into());
self
}
#[must_use]
pub fn with_platform(mut self, platform: ironrdp_pdu::rdp::capability_sets::MajorPlatformType) -> Self {
self.platform = Some(platform);
self
}
#[must_use]
pub fn with_keyboard_type(mut self, ty: ironrdp_pdu::gcc::KeyboardType) -> Self {
self.keyboard_type = Some(ty);
self
}
#[must_use]
pub fn with_keyboard_subtype(mut self, subtype: u32) -> Self {
self.keyboard_subtype = Some(subtype);
self
}
#[must_use]
pub fn with_keyboard_functional_keys_count(mut self, count: u32) -> Self {
self.keyboard_functional_keys_count = Some(count);
self
}
#[must_use]
pub fn with_ime_file_name(mut self, name: impl Into<String>) -> Self {
self.ime_file_name = Some(name.into());
self
}
#[must_use]
pub fn with_dig_product_id(mut self, id: impl Into<String>) -> Self {
self.dig_product_id = Some(id.into());
self
}
#[must_use]
pub fn with_color_depth(mut self, depth: u32) -> Self {
self.color_depth = Some(depth);
self.properties.set_color_depth(depth);
self
}
#[must_use]
pub fn with_desktop_width(mut self, width: u16) -> Self {
self.desktop_width = Some(width);
self.properties.set_desktop_width(width);
self
}
#[must_use]
pub fn with_desktop_height(mut self, height: u16) -> Self {
self.desktop_height = Some(height);
self.properties.set_desktop_height(height);
self
}
#[must_use]
pub fn with_desktop_scale_factor(mut self, scale: u32) -> Self {
self.desktop_scale_factor = Some(scale);
self.properties.set_desktop_scale_factor(scale);
self
}
#[doc(alias("with_nla", "with_enable_credssp"))]
#[must_use]
pub fn with_credssp(mut self, enabled: bool) -> Self {
self.enable_credssp = Some(enabled);
self.properties.set_enable_credssp_support(enabled);
self
}
#[must_use]
pub fn with_codecs(mut self, codecs: Vec<String>) -> Self {
self.codecs = codecs;
self
}
#[must_use]
pub fn with_autologon(mut self, enabled: bool) -> Self {
self.autologon = Some(enabled);
self.properties.set_autologon(enabled);
self
}
#[doc(alias("with_enable_tls"))]
#[must_use]
pub fn with_tls(mut self, enabled: bool) -> Self {
self.enable_tls = Some(enabled);
self.properties.set_enable_tls(enabled);
self
}
#[must_use]
pub fn with_server_pointer(mut self, enabled: bool) -> Self {
self.enable_server_pointer = Some(enabled);
self.properties.set_server_pointer(enabled);
self
}
#[must_use]
pub fn with_pointer_software_rendering(mut self, enabled: bool) -> Self {
self.pointer_software_rendering = Some(enabled);
self
}
#[must_use]
pub fn with_compression(mut self, enabled: bool) -> Self {
self.compression_enabled = Some(enabled);
self.properties.set_compression(enabled);
self
}
#[must_use]
pub fn with_compression_type(mut self, ty: Option<ironrdp_pdu::rdp::client_info::CompressionType>) -> Self {
self.compression_type = ty;
if let Some(ty) = ty {
self.properties.set_compression_level(level_from_compression_type(ty));
} else {
self.properties.clear_compression_level();
}
self
}
#[must_use]
pub fn with_compression_level(self, level: u32) -> Self {
match compression_type_from_level(level) {
Ok(ty) => self.with_compression_type(Some(ty)),
Err(_) => self,
}
}
#[must_use]
pub fn with_transport(mut self, transport: TransportKind) -> Self {
match &transport {
TransportKind::Direct => {
self.properties.clear_rdcleanpath();
self.properties.clear_gateway();
}
TransportKind::RDCleanPath { url } => {
self.properties.clear_gateway();
self.properties.set_rdcleanpath_url(url.to_string());
}
#[cfg(feature = "gateway")]
TransportKind::Gateway { endpoint } => {
self.properties.clear_rdcleanpath();
self.properties.set_gateway_hostname(endpoint.clone());
self.properties
.set_gateway_usage_method(ironrdp_cfg::GatewayUsageMethod::UseAlways);
}
}
self.transport = transport;
self
}
#[must_use]
pub fn with_rdcleanpath_token(mut self, token: impl Into<String>) -> Self {
self.rdcleanpath_token = Some(token.into());
self
}
#[must_use]
pub fn with_kerberos_config(mut self, cfg: ironrdp_connector::credssp::KerberosConfig) -> Self {
if let Some(url) = &cfg.kdc_proxy_url {
self.properties.set_kdc_proxy_url(url.to_string());
} else {
self.properties.clear_kdc_proxy_url();
}
self.kerberos_config = Some(cfg);
self
}
#[must_use]
pub fn with_fake_events_interval(mut self, interval: Duration) -> Self {
self.fake_events_interval = Some(interval);
self.properties
.set_fake_events_interval(u32::try_from(interval.as_secs() / 60).unwrap_or(u32::MAX));
self
}
#[cfg(feature = "sound")]
#[must_use]
pub fn with_sound(mut self, enabled: bool) -> Self {
self.channels.sound = enabled;
self.properties.set_audio_mode(if enabled {
ironrdp_cfg::AudioMode::RedirectToClient
} else {
ironrdp_cfg::AudioMode::Disabled
});
self
}
#[cfg(feature = "clipboard")]
#[must_use]
pub fn with_clipboard(mut self, mode: ClipboardType) -> Self {
self.channels.clipboard = mode;
self.properties
.set_redirect_clipboard(matches!(mode, ClipboardType::Enable));
self
}
#[cfg(feature = "rdpdr")]
#[must_use]
pub fn with_rdpdr(mut self, enabled: bool) -> Self {
self.channels.rdpdr.enabled = enabled;
self.properties.set_enable_rdpdr(enabled);
self
}
#[cfg(feature = "smartcard")]
#[must_use]
pub fn with_smartcard(mut self, enabled: bool) -> Self {
self.channels.rdpdr.smartcard = enabled;
self.properties.set_enable_smartcard(enabled);
self
}
#[cfg(feature = "qoi")]
#[must_use]
pub fn with_qoi(mut self, enabled: bool) -> Self {
self.channels.qoi = enabled;
self.properties.set_enable_qoi(enabled);
self
}
#[cfg(feature = "qoiz")]
#[must_use]
pub fn with_qoiz(mut self, enabled: bool) -> Self {
self.channels.qoiz = enabled;
self.properties.set_enable_qoiz(enabled);
self
}
#[cfg(feature = "dvc-pipe-proxy")]
#[must_use]
pub fn with_dvc_pipe_proxy(mut self, info: DvcProxyInfo) -> Self {
self.dvc_pipe_proxies.push(info);
self.properties
.set_dvc_pipe_proxies(self.dvc_pipe_proxies.iter().map(|p| ironrdp_cfg::DvcPipeProxy {
channel_name: p.channel_name.clone(),
pipe_name: p.pipe_name.clone(),
}));
self
}
#[cfg(all(windows, feature = "dvc-com-plugin"))]
#[must_use]
pub fn with_dvc_plugin(mut self, path: impl Into<PathBuf>) -> Self {
self.dvc_plugins.push(path.into());
self.properties
.set_dvc_plugins(self.dvc_plugins.iter().map(PathBuf::as_path));
self
}
#[must_use]
pub fn with_static_channel<P, F>(mut self, factory: F) -> Self
where
F: Fn(&PropertySet) -> Option<P> + Send + Sync + 'static,
P: ironrdp_svc::SvcClientProcessor + 'static,
{
let cb: StaticChannelFn = Arc::new(move |connector: &mut ironrdp_connector::ClientConnector, ps| {
if let Some(processor) = factory(ps) {
connector.attach_static_channel(processor);
}
});
self.extensions.static_channels.push(cb);
self
}
#[must_use]
pub fn with_dvc<P, F>(mut self, factory: F) -> Self
where
F: Fn(&PropertySet) -> Option<P> + Send + Sync + 'static,
P: ironrdp_dvc::DvcProcessor + 'static,
{
let cb: DvcChannelFn = Arc::new(move |drdynvc, ps| {
if let Some(processor) = factory(ps) {
drdynvc.attach_dynamic_channel(processor);
}
});
self.extensions.dvc_channels.push(cb);
self
}
pub fn missing(&self) -> Vec<MissingField> {
let mut missing = Vec::new();
if self.destination.is_none() {
missing.push(MissingField::ServerAddress);
}
if self.username.is_none() {
missing.push(MissingField::Username);
}
if self.password.is_none() {
missing.push(MissingField::Password);
}
#[cfg(feature = "gateway")]
if matches!(self.transport, TransportKind::Gateway { .. }) {
if self.gateway_username.is_none() {
missing.push(MissingField::GatewayUsername);
}
if self.gateway_password.is_none() {
missing.push(MissingField::GatewayPassword);
}
}
if matches!(self.transport, TransportKind::RDCleanPath { .. }) && self.rdcleanpath_token.is_none() {
missing.push(MissingField::RDCleanPathToken);
}
if self.client_build.is_none() {
missing.push(MissingField::ClientBuild);
}
if self.client_dir.is_none() {
missing.push(MissingField::ClientDir);
}
if self.platform.is_none() {
missing.push(MissingField::Platform);
}
if self.client_name.is_none() {
missing.push(MissingField::ClientName);
}
missing
}
#[expect(
clippy::missing_panics_doc,
reason = "a panic here would be a bug (secrets are guaranteed present by missing()), not documented behavior"
)]
pub fn build(self) -> anyhow::Result<Config> {
use ironrdp_pdu::rdp::capability_sets::client_codecs_capabilities;
use ironrdp_pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo};
let missing = self.missing();
if !missing.is_empty() {
anyhow::bail!(
"missing required configuration: {}",
missing
.iter()
.map(MissingField::to_string)
.collect::<Vec<_>>()
.join(", ")
);
}
let codecs: Vec<&str> = self.codecs.iter().map(String::as_str).collect();
let codecs = client_codecs_capabilities(&codecs).map_err(|help| anyhow::anyhow!("{help}"))?;
let color_depth = self.color_depth.unwrap_or(32);
if color_depth != 16 && color_depth != 32 {
anyhow::bail!("invalid color depth: only 16 and 32 bit color depths are supported");
}
let bitmap = ironrdp_connector::BitmapConfig {
color_depth,
lossy_compression: true,
codecs,
};
#[expect(
clippy::unwrap_used,
reason = "the transport secrets are guaranteed present by the missing() check above"
)]
let transport = match self.transport {
TransportKind::Direct => Transport::Direct,
#[cfg(feature = "gateway")]
TransportKind::Gateway { endpoint } => Transport::Gateway(GatewayConfig {
endpoint,
username: self.gateway_username.unwrap(),
password: self.gateway_password.unwrap(),
}),
TransportKind::RDCleanPath { url } => Transport::RDCleanPath(RDCleanPathConfig {
url,
auth_token: self.rdcleanpath_token.unwrap(),
}),
};
let client_name = self.client_name.unwrap_or_default();
let kerberos_config = self
.kerberos_config
.or_else(|| kerberos_config_from_properties(&self.properties, &client_name));
let compression_type = if self.compression_enabled.unwrap_or(true) {
Some(
self.compression_type
.unwrap_or(ironrdp_pdu::rdp::client_info::CompressionType::K64),
)
} else {
None
};
let connector = ironrdp_connector::Config {
credentials: ironrdp_connector::Credentials::UsernamePassword {
username: self.username.unwrap_or_default(),
password: self.password.unwrap_or_default(),
},
domain: self.domain,
enable_tls: self.enable_tls.unwrap_or(true),
enable_credssp: self.enable_credssp.unwrap_or(true),
keyboard_type: self
.keyboard_type
.unwrap_or(ironrdp_pdu::gcc::KeyboardType::IbmEnhanced),
keyboard_subtype: self.keyboard_subtype.unwrap_or(0),
keyboard_layout: 0,
keyboard_functional_keys_count: self.keyboard_functional_keys_count.unwrap_or(12),
ime_file_name: self.ime_file_name.unwrap_or_default(),
dig_product_id: self.dig_product_id.unwrap_or_default(),
desktop_size: ironrdp_connector::DesktopSize {
width: self.desktop_width.unwrap_or(DEFAULT_WIDTH),
height: self.desktop_height.unwrap_or(DEFAULT_HEIGHT),
},
desktop_scale_factor: self.desktop_scale_factor.unwrap_or(0),
bitmap: Some(bitmap),
client_build: self.client_build.unwrap_or_default(),
client_name,
client_dir: self.client_dir.unwrap_or_default(),
platform: self
.platform
.unwrap_or(ironrdp_pdu::rdp::capability_sets::MajorPlatformType::UNSPECIFIED),
hardware_id: None,
license_cache: None,
enable_server_pointer: self.enable_server_pointer.unwrap_or(true),
autologon: self.autologon.unwrap_or(false),
enable_audio_playback: self.enable_audio_playback.unwrap_or(true),
request_data: None,
pointer_software_rendering: self.pointer_software_rendering.unwrap_or(false),
multitransport_flags: None,
compression_type,
performance_flags: PerformanceFlags::default(),
timezone_info: TimezoneInfo::default(),
alternate_shell: self.alternate_shell.unwrap_or_default(),
work_dir: self.work_dir.unwrap_or_default(),
};
let mut properties = self.properties;
let detected_secrets = properties
.iter()
.filter(|(key, _)| ironrdp_cfg::is_secret_key(key))
.map(|(key, _)| key.clone().into_owned())
.collect::<Vec<_>>();
detected_secrets.into_iter().for_each(|key| {
properties.remove(&key);
});
Ok(Config {
connector,
destination: self.destination.context("server address is required")?,
transport,
kerberos_config,
fake_events_interval: self.fake_events_interval,
channels: self.channels,
#[cfg(feature = "dvc-pipe-proxy")]
dvc_pipe_proxies: self.dvc_pipe_proxies,
#[cfg(all(windows, feature = "dvc-com-plugin"))]
dvc_plugins: self.dvc_plugins,
properties,
extensions: self.extensions,
})
}
pub fn from_property_set(ps: &PropertySet) -> anyhow::Result<Self> {
ConfigBuilder::new().with_property_set(ps)
}
pub fn with_property_set(mut self, ps: &PropertySet) -> anyhow::Result<Self> {
#[cfg(feature = "gateway")]
use ironrdp_cfg::GatewayUsageMethod;
use ironrdp_cfg::{AudioMode, TargetHost};
self.properties.merge(ps);
let target = ps.full_address().context("invalid 'full address'")?.or(ps
.alternate_full_address()
.context("invalid 'alternate full address'")?);
if let Some(target) = target {
let port = target
.port
.or(ps.server_port().context("invalid 'server port'")?)
.unwrap_or(RDP_DEFAULT_PORT);
let name = match target.host {
TargetHost::Ip(ip) => ip.to_string(),
TargetHost::Domain(host) => host,
};
self.destination = Some(Destination::from_parts(name, port));
}
if let Some(username) = ps.username() {
self.username = Some(username.to_owned());
}
if let Some(password) = ps.clear_text_password() {
self.password = Some(password.to_owned());
}
if let Some(domain) = ps.domain() {
self.domain = Some(domain.to_owned());
}
if let Some(enable_credssp) = ps.enable_credssp_support() {
self.enable_credssp = Some(enable_credssp);
}
if let Some(enable_tls) = ps.enable_tls() {
self.enable_tls = Some(enable_tls);
}
if let Some(server_pointer) = ps.server_pointer() {
self.enable_server_pointer = Some(server_pointer);
}
if let Some(autologon) = ps.autologon() {
self.autologon = Some(autologon);
}
if let Some(scale) = ps.desktop_scale_factor().ok().flatten() {
self.desktop_scale_factor = Some(scale);
}
if let Some(width) = ps.desktop_width().ok().flatten() {
self.desktop_width = Some(width);
}
if let Some(height) = ps.desktop_height().ok().flatten() {
self.desktop_height = Some(height);
}
if let Some(shell) = ps.alternate_shell() {
self.alternate_shell = Some(shell.to_owned());
}
if let Some(dir) = ps.shell_working_directory() {
self.work_dir = Some(dir.to_owned());
}
if let Some(minutes) = ps.fake_events_interval() {
self.fake_events_interval = Some(Duration::from_secs(u64::from(minutes) * 60));
}
if let Some(level) = ps.compression_level() {
self.compression_type = Some(compression_type_from_level(level)?);
}
if let Some(enabled) = ps.compression() {
self.compression_enabled = Some(enabled);
}
if let Some(depth) = ps.color_depth() {
self.color_depth = Some(depth);
}
match ps.audio_mode() {
Ok(Some(AudioMode::PlayOnServer | AudioMode::Disabled)) => self.enable_audio_playback = Some(false),
Ok(Some(AudioMode::RedirectToClient)) => self.enable_audio_playback = Some(true),
_ => {}
}
if let Some((url, token)) = ps.rdcleanpath_url().zip(ps.rdcleanpath_token()) {
let url = Url::parse(url).context("invalid 'ironrdp_rdcleanpathurl'")?;
self.transport = TransportKind::RDCleanPath { url };
self.rdcleanpath_token = Some(token.to_owned());
} else {
#[cfg(feature = "gateway")]
{
let gateway_usage = ps
.gateway_usage_method()
.context("invalid Gateway usage method")?
.unwrap_or_default();
let gateway_hostname = ps.gateway_hostname();
let select_gateway_transport = match gateway_usage {
GatewayUsageMethod::UseAlways => true,
GatewayUsageMethod::Detect => gateway_hostname.is_some(),
GatewayUsageMethod::UseDefaultSettings => false,
GatewayUsageMethod::Direct | GatewayUsageMethod::DirectBypassLocal => false,
};
if select_gateway_transport {
let endpoint = gateway_hostname.context("missing Gateway hostname")?;
self.transport = TransportKind::Gateway {
endpoint: endpoint.to_owned(),
};
if let Some(user) = ps.gateway_username() {
self.gateway_username = Some(user.to_owned());
}
if let Some(pass) = ps.gateway_password() {
self.gateway_password = Some(pass.to_owned());
}
}
}
}
if let Some(redirect) = ps.redirect_clipboard() {
#[cfg(feature = "clipboard")]
{
self.channels.clipboard = if redirect {
ClipboardType::Enable
} else {
ClipboardType::Disable
};
}
let _ = redirect;
}
#[cfg(feature = "sound")]
if matches!(ps.audio_mode(), Ok(Some(AudioMode::Disabled))) {
self.channels.sound = false;
}
#[cfg(feature = "rdpdr")]
if let Some(enabled) = ps.enable_rdpdr() {
self.channels.rdpdr.enabled = enabled;
}
#[cfg(feature = "smartcard")]
if let Some(enabled) = ps.enable_smartcard() {
self.channels.rdpdr.smartcard = enabled;
}
#[cfg(feature = "qoi")]
if let Some(enabled) = ps.enable_qoi() {
self.channels.qoi = enabled;
}
#[cfg(feature = "qoiz")]
if let Some(enabled) = ps.enable_qoiz() {
self.channels.qoiz = enabled;
}
#[cfg(feature = "dvc-pipe-proxy")]
for (idx, proxy) in ps.dvc_pipe_proxies().enumerate() {
let proxy = proxy.with_context(|| format!("invalid DVC pipe proxy spec at idx {idx}"))?;
self.dvc_pipe_proxies.push(DvcProxyInfo {
channel_name: proxy.channel_name,
pipe_name: proxy.pipe_name,
});
}
#[cfg(all(windows, feature = "dvc-com-plugin"))]
self.dvc_plugins.extend(ps.dvc_plugins());
Ok(self)
}
}
fn compression_type_from_level(level: u32) -> anyhow::Result<ironrdp_pdu::rdp::client_info::CompressionType> {
use ironrdp_pdu::rdp::client_info::CompressionType;
match level {
0 => Ok(CompressionType::K8),
1 => Ok(CompressionType::K64),
2 => Ok(CompressionType::Rdp6),
3 => Ok(CompressionType::Rdp61),
_ => anyhow::bail!("invalid compression level: valid values are 0, 1, 2, 3"),
}
}
fn level_from_compression_type(ty: ironrdp_pdu::rdp::client_info::CompressionType) -> u32 {
use ironrdp_pdu::rdp::client_info::CompressionType;
match ty {
CompressionType::K8 => 0,
CompressionType::K64 => 1,
CompressionType::Rdp6 => 2,
CompressionType::Rdp61 => 3,
}
}
fn kerberos_config_from_properties(
ps: &PropertySet,
client_name: &str,
) -> Option<ironrdp_connector::credssp::KerberosConfig> {
use ironrdp_cfg::PropertySetExt as _;
let kdc_proxy_url = ps.kdc_proxy_url().map(str::to_owned).or_else(|| {
ps.kdc_proxy_name().map(|name| {
if name.starts_with("http://") || name.starts_with("https://") {
name.to_owned()
} else {
format!("https://{name}/KdcProxy")
}
})
})?;
Url::parse(&kdc_proxy_url)
.ok()
.map(|url| ironrdp_connector::credssp::KerberosConfig {
kdc_proxy_url: Some(url),
hostname: client_name.to_owned(),
})
}