diff --git a/crates/ironrdp-cfg/src/lib.rs b/crates/ironrdp-cfg/src/lib.rs index e9a00061e..acc226236 100644 --- a/crates/ironrdp-cfg/src/lib.rs +++ b/crates/ironrdp-cfg/src/lib.rs @@ -10,14 +10,44 @@ pub trait PropertySetExt { fn alternate_full_address(&self) -> Option<&str>; + fn domain(&self) -> Option<&str>; + + fn enable_credssp_support(&self) -> Option; + + fn compression(&self) -> Option; + fn gateway_hostname(&self) -> Option<&str>; + fn gateway_usage_method(&self) -> Option; + + fn gateway_credentials_source(&self) -> Option; + + fn gateway_username(&self) -> Option<&str>; + + fn gateway_password(&self) -> Option<&str>; + + fn desktop_width(&self) -> Option; + + fn desktop_height(&self) -> Option; + + fn desktop_scale_factor(&self) -> Option; + + fn alternate_shell(&self) -> Option<&str>; + + fn shell_working_directory(&self) -> Option<&str>; + + fn redirect_clipboard(&self) -> Option; + + fn audio_mode(&self) -> Option; + fn remote_application_name(&self) -> Option<&str>; fn remote_application_program(&self) -> Option<&str>; fn kdc_proxy_url(&self) -> Option<&str>; + fn kdc_proxy_name(&self) -> Option<&str>; + fn username(&self) -> Option<&str>; /// Target RDP server password - use for testing only @@ -37,10 +67,67 @@ impl PropertySetExt for PropertySet { self.get::<&str>("alternate full address") } + fn domain(&self) -> Option<&str> { + self.get::<&str>("domain") + } + + fn enable_credssp_support(&self) -> Option { + self.get::("enablecredsspsupport") + } + + fn compression(&self) -> Option { + self.get::("compression") + } + fn gateway_hostname(&self) -> Option<&str> { self.get::<&str>("gatewayhostname") } + fn gateway_usage_method(&self) -> Option { + self.get::("gatewayusagemethod") + } + + fn gateway_credentials_source(&self) -> Option { + self.get::("gatewaycredentialssource") + } + + fn gateway_username(&self) -> Option<&str> { + self.get::<&str>("gatewayusername") + } + + fn gateway_password(&self) -> Option<&str> { + self.get::<&str>("GatewayPassword") + .or_else(|| self.get::<&str>("gatewaypassword")) + } + + fn desktop_width(&self) -> Option { + self.get::("desktopwidth") + } + + fn desktop_height(&self) -> Option { + self.get::("desktopheight") + } + + fn desktop_scale_factor(&self) -> Option { + self.get::("desktopscalefactor") + } + + fn alternate_shell(&self) -> Option<&str> { + self.get::<&str>("alternate shell") + } + + fn shell_working_directory(&self) -> Option<&str> { + self.get::<&str>("shell working directory") + } + + fn redirect_clipboard(&self) -> Option { + self.get::("redirectclipboard") + } + + fn audio_mode(&self) -> Option { + self.get::("audiomode") + } + fn remote_application_name(&self) -> Option<&str> { self.get::<&str>("remoteapplicationname") } @@ -51,6 +138,11 @@ impl PropertySetExt for PropertySet { fn kdc_proxy_url(&self) -> Option<&str> { self.get::<&str>("kdcproxyurl") + .or_else(|| self.get::<&str>("KDCProxyURL")) + } + + fn kdc_proxy_name(&self) -> Option<&str> { + self.get::<&str>("kdcproxyname") } fn username(&self) -> Option<&str> { diff --git a/crates/ironrdp-client/README.md b/crates/ironrdp-client/README.md index 95a9f90f6..edd9a38aa 100644 --- a/crates/ironrdp-client/README.md +++ b/crates/ironrdp-client/README.md @@ -12,6 +12,43 @@ and winit for windowing. ironrdp-client --username --password ``` +## `.rdp` file support + +You can load a `.rdp` file with `--rdp-file `. + +Currently supported properties: + +- `full address:s:` +- `alternate full address:s:` +- `server port:i:` +- `username:s:` +- `domain:s:` +- `enablecredsspsupport:i:<0|1>` +- `gatewayhostname:s:` +- `gatewayusagemethod:i:` +- `gatewaycredentialssource:i:` +- `gatewayusername:s:` +- `GatewayPassword:s:` +- `kdcproxyname:s:` +- `KDCProxyURL:s:` +- `alternate shell:s:` +- `shell working directory:s:` +- `redirectclipboard:i:<0|1>` +- `audiomode:i:<0|1|2>` +- `desktopwidth:i:` +- `desktopheight:i:` +- `desktopscalefactor:i:` +- `compression:i:<0|1>` +- `ClearTextPassword:s:` + +Property precedence is: + +1. CLI options +2. `.rdp` file values +3. Defaults and interactive prompts + +Unknown or unsupported `.rdp` properties are ignored and do not fail parsing. Parse issues and skipped properties are logged at debug level. + ## Configuring log filter directives The `IRONRDP_LOG` environment variable is used to set the log filter directives. diff --git a/crates/ironrdp-client/src/app.rs b/crates/ironrdp-client/src/app.rs index bcce8d4fd..5fbb51506 100644 --- a/crates/ironrdp-client/src/app.rs +++ b/crates/ironrdp-client/src/app.rs @@ -23,6 +23,7 @@ type WindowSurface = (Arc, softbuffer::Surface, A pub struct App { input_event_sender: mpsc::UnboundedSender, context: softbuffer::Context>, + initial_window_size: PhysicalSize, window: Option, buffer: Vec, buffer_size: (u16, u16), @@ -35,6 +36,7 @@ impl App { pub fn new( event_loop: &EventLoop, input_event_sender: &mpsc::UnboundedSender, + initial_window_size: PhysicalSize, ) -> anyhow::Result { // SAFETY: We drop the softbuffer context right before the event loop is stopped, thus making this safe. // FIXME: This is not a sufficient proof and the API is actually unsound as-is. @@ -50,6 +52,7 @@ impl App { Ok(Self { input_event_sender: input_event_sender.clone(), context, + initial_window_size, window: None, buffer: Vec::new(), buffer_size: (0, 0), @@ -111,7 +114,9 @@ impl ApplicationHandler for App { } fn resumed(&mut self, event_loop: &ActiveEventLoop) { - let window_attributes = WindowAttributes::default().with_title("IronRDP"); + let window_attributes = WindowAttributes::default() + .with_title("IronRDP") + .with_inner_size(self.initial_window_size); match event_loop.create_window(window_attributes) { Ok(window) => { let window = Arc::new(window); diff --git a/crates/ironrdp-client/src/config.rs b/crates/ironrdp-client/src/config.rs index 1d0bc4f58..85f269e94 100644 --- a/crates/ironrdp-client/src/config.rs +++ b/crates/ironrdp-client/src/config.rs @@ -12,15 +12,108 @@ use ironrdp::pdu::rdp::capability_sets::{client_codecs_capabilities, MajorPlatfo use ironrdp::pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; use ironrdp_mstsgu::GwConnectTarget; use tap::prelude::*; +use tracing::debug; use url::Url; const DEFAULT_WIDTH: u16 = 1920; const DEFAULT_HEIGHT: u16 = 1080; +const REDACTED_RDP_VALUE: &str = "*omitted*"; +const SUPPORTED_RDP_PROPERTIES: &[&str] = &[ + "full address", + "alternate full address", + "server port", + "username", + "domain", + "enablecredsspsupport", + "gatewayhostname", + "gatewayusagemethod", + "gatewaycredentialssource", + "gatewayusername", + "gatewaypassword", + "kdcproxyname", + "kdcproxyurl", + "alternate shell", + "shell working directory", + "redirectclipboard", + "audiomode", + "desktopwidth", + "desktopheight", + "desktopscalefactor", + "compression", + "ClearTextPassword", +]; + +const SENSITIVE_RDP_PROPERTIES: &[&str] = &["ClearTextPassword", "GatewayPassword", "gatewaypassword"]; + +fn is_supported_rdp_property(key: &str) -> bool { + SUPPORTED_RDP_PROPERTIES + .iter() + .any(|supported| supported.eq_ignore_ascii_case(key)) +} + +fn is_sensitive_rdp_property(key: &str) -> bool { + SENSITIVE_RDP_PROPERTIES + .iter() + .any(|sensitive| sensitive.eq_ignore_ascii_case(key)) +} + +fn redacted_rdp_line(line: &str) -> String { + let mut parts = line.splitn(3, ':'); + let property = parts.next().unwrap_or(""); + + if is_sensitive_rdp_property(property) { + format!("{property}:*:{REDACTED_RDP_VALUE}") + } else { + line.to_owned() + } +} + +fn rdp_u16_property(value: Option, property: &'static str) -> Option { + value.and_then(|value| { + u16::try_from(value).ok().or_else(|| { + debug!(%property, value, "Ignored .RDP property with out-of-range value"); + None + }) + }) +} + +fn rdp_u32_property(value: Option, property: &'static str) -> Option { + value.and_then(|value| { + u32::try_from(value).ok().or_else(|| { + debug!(%property, value, "Ignored .RDP property with out-of-range value"); + None + }) + }) +} + +fn normalize_kdc_proxy_url_from_name(name: &str) -> String { + if name.starts_with("http://") || name.starts_with("https://") { + name.to_owned() + } else { + format!("https://{name}/KdcProxy") + } +} + +fn should_use_gateway_from_rdp(gateway_usage_method: Option, has_gateway_host: bool) -> bool { + match gateway_usage_method { + Some(0 | 4) => false, + Some(1..=3) => true, + Some(value) => { + debug!( + property = "gatewayusagemethod", + value, "Ignored .RDP property with unsupported value" + ); + has_gateway_host + } + None => has_gateway_host, + } +} #[derive(Clone, Debug)] pub struct Config { pub log_file: Option, pub gw: Option, + pub kerberos_config: Option, pub destination: Destination, pub connector: connector::Config, pub clipboard_type: ClipboardType, @@ -343,9 +436,17 @@ struct Args { impl Config { pub fn parse_args() -> anyhow::Result { + Self::parse_from(std::env::args_os()) + } + + pub fn parse_from(args: I) -> anyhow::Result + where + I: IntoIterator, + T: Into + Clone, + { use ironrdp_cfg::PropertySetExt as _; - let args = Args::parse(); + let args = Args::parse_from(args); let mut properties = ironrdp_propertyset::PropertySet::new(); @@ -354,27 +455,92 @@ impl Config { 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 e in errors { - #[expect(clippy::print_stderr)] - { - eprintln!("Error when reading {}: {e}", rdp_file.display()) + for error in errors { + match &error.kind { + ironrdp_rdpfile::ErrorKind::UnknownType { ty } => { + debug!( + rdp_file = %rdp_file.display(), + line = error.line, + %ty, + "Skipped .RDP property with unsupported type" + ); + } + ironrdp_rdpfile::ErrorKind::InvalidValue { ty, value } => { + debug!( + rdp_file = %rdp_file.display(), + line = error.line, + %ty, + value = %value, + "Skipped .RDP property with invalid value" + ); + } + ironrdp_rdpfile::ErrorKind::MalformedLine { line } => { + let redacted_line = redacted_rdp_line(line); + debug!( + rdp_file = %rdp_file.display(), + line = error.line, + malformed_line = %redacted_line, + "Skipped malformed .RDP property line" + ); + } } } } + + for (key, _) in properties.iter() { + let property = key.as_ref(); + + if !is_supported_rdp_property(property) { + debug!(rdp_file = %rdp_file.display(), %property, "Ignored unsupported .RDP property"); + } + } } + let has_gateway_host = properties.gateway_hostname().is_some(); + let use_gateway_from_rdp = should_use_gateway_from_rdp(properties.gateway_usage_method(), has_gateway_host); + let mut gw: Option = None; - if let Some(gw_addr) = args.gw_endpoint { + if let Some(gw_addr) = args.gw_endpoint.or_else(|| { + if use_gateway_from_rdp { + properties.gateway_hostname().map(str::to_owned) + } else { + None + } + }) { gw = Some(GwConnectTarget { gw_endpoint: gw_addr, gw_user: String::new(), gw_pass: String::new(), server: String::new(), // TODO: non-standard port? also dont use here? }); + } else if has_gateway_host && !use_gateway_from_rdp { + debug!("Ignored gatewayhostname due to gatewayusagemethod policy"); } if let Some(ref mut gw) = gw { - gw.gw_user = if let Some(gw_user) = args.gw_user { + if let Some(gateway_credentials_source) = properties.gateway_credentials_source() { + match gateway_credentials_source { + 0 | 3 | 4 => {} + 1 | 2 | 5 => { + debug!( + property = "gatewaycredentialssource", + value = gateway_credentials_source, + "Unsupported gateway credential source; falling back to username/password" + ); + } + value => { + debug!( + property = "gatewaycredentialssource", + value, "Ignored .RDP property with unsupported value" + ); + } + } + } + + gw.gw_user = if let Some(gw_user) = args + .gw_user + .or_else(|| properties.gateway_username().map(str::to_owned)) + { gw_user } else { inquire::Text::new("Gateway username:") @@ -382,7 +548,10 @@ impl Config { .context("Username prompt")? }; - gw.gw_pass = if let Some(gw_pass) = args.gw_pass { + gw.gw_pass = if let Some(gw_pass) = args + .gw_pass + .or_else(|| properties.gateway_password().map(str::to_owned)) + { gw_pass } else { inquire::Password::new("Gateway password:") @@ -394,7 +563,10 @@ impl Config { let destination = if let Some(destination) = args.destination { destination - } else if let Some(destination) = properties.full_address() { + } else if let Some(destination) = properties + .full_address() + .or_else(|| properties.alternate_full_address()) + { if let Some(port) = properties.server_port() { format!("{destination}:{port}").parse() } else { @@ -431,6 +603,12 @@ impl Config { .context("Password prompt")? }; + let enable_credssp = if args.no_credssp { + false + } else { + properties.enable_credssp_support().unwrap_or(true) + }; + let codecs: Vec<_> = args.codecs.iter().map(|s| s.as_str()).collect(); let codecs = match client_codecs_capabilities(&codecs) { Ok(codecs) => codecs, @@ -453,29 +631,77 @@ impl Config { }; let clipboard_type = if args.clipboard_type == ClipboardType::Default { - #[cfg(windows)] - { - ClipboardType::Windows - } - #[cfg(not(windows))] - { + let redirect_clipboard = properties.redirect_clipboard().unwrap_or(true); + + if !redirect_clipboard { ClipboardType::None + } else { + #[cfg(windows)] + { + ClipboardType::Windows + } + #[cfg(not(windows))] + { + ClipboardType::None + } } } else { args.clipboard_type }; - let compression_type = if args.compression_enabled { + let enable_audio_playback = match properties.audio_mode() { + Some(0) => true, + Some(1 | 2) => false, + Some(value) => { + debug!( + property = "audiomode", + value, "Ignored .RDP property with unsupported value" + ); + true + } + None => true, + }; + + let compression_enabled = if !args.compression_enabled { + false + } else { + properties.compression().unwrap_or(args.compression_enabled) + }; + + let compression_type = if compression_enabled { Some(compression_type_from_level(args.compression_level)?) } else { None }; + let desktop_width = rdp_u16_property(properties.desktop_width(), "desktopwidth").unwrap_or(DEFAULT_WIDTH); + let desktop_height = rdp_u16_property(properties.desktop_height(), "desktopheight").unwrap_or(DEFAULT_HEIGHT); + let desktop_scale_factor = + rdp_u32_property(properties.desktop_scale_factor(), "desktopscalefactor").unwrap_or(0); + + let kdc_proxy_url = properties + .kdc_proxy_url() + .map(str::to_owned) + .or_else(|| properties.kdc_proxy_name().map(normalize_kdc_proxy_url_from_name)); + + let kerberos_config = kdc_proxy_url.and_then(|kdc_proxy_url| { + Url::parse(&kdc_proxy_url) + .ok() + .map(|url| connector::credssp::KerberosConfig { + kdc_proxy_url: Some(url), + hostname: None, + }) + .or_else(|| { + debug!(%kdc_proxy_url, "Ignored invalid KDC proxy URL from .RDP property"); + None + }) + }); + let connector = connector::Config { credentials: Credentials::UsernamePassword { username, password }, - domain: args.domain, + domain: args.domain.or_else(|| properties.domain().map(str::to_owned)), enable_tls: !args.no_tls, - enable_credssp: !args.no_credssp, + enable_credssp, keyboard_type: KeyboardType::parse(args.keyboard_type), keyboard_subtype: args.keyboard_subtype, keyboard_layout: 0, // the server SHOULD use the default active input locale identifier @@ -483,10 +709,10 @@ impl Config { ime_file_name: args.ime_file_name, dig_product_id: args.dig_product_id, desktop_size: connector::DesktopSize { - width: DEFAULT_WIDTH, - height: DEFAULT_HEIGHT, + width: desktop_width, + height: desktop_height, }, - desktop_scale_factor: 0, // Default to 0 per FreeRDP + desktop_scale_factor, bitmap: Some(bitmap), client_build: semver::Version::parse(env!("CARGO_PKG_VERSION")) .map_or(0, |version| version.major * 100 + version.minor * 10 + version.patch) @@ -508,15 +734,15 @@ impl Config { license_cache: None, enable_server_pointer: !args.no_server_pointer, autologon: args.autologon, - enable_audio_playback: true, + enable_audio_playback, request_data: None, pointer_software_rendering: false, multitransport_flags: None, compression_type, performance_flags: PerformanceFlags::default(), timezone_info: TimezoneInfo::default(), - alternate_shell: String::new(), - work_dir: String::new(), + alternate_shell: properties.alternate_shell().unwrap_or_default().to_owned(), + work_dir: properties.shell_working_directory().unwrap_or_default().to_owned(), }; let rdcleanpath = args @@ -527,6 +753,7 @@ impl Config { Ok(Self { log_file: args.log_file, gw, + kerberos_config, destination, connector, clipboard_type, diff --git a/crates/ironrdp-client/src/main.rs b/crates/ironrdp-client/src/main.rs index 18d41f347..82fe3fc2d 100644 --- a/crates/ironrdp-client/src/main.rs +++ b/crates/ironrdp-client/src/main.rs @@ -6,10 +6,11 @@ use ironrdp_client::config::{ClipboardType, Config}; use ironrdp_client::rdp::{DvcPipeProxyFactory, RdpClient, RdpInputEvent, RdpOutputEvent}; use tokio::runtime; use tracing::debug; +use winit::dpi::PhysicalSize; use winit::event_loop::EventLoop; fn main() -> anyhow::Result<()> { - let mut config = Config::parse_args().context("CLI arguments parsing")?; + let config = Config::parse_args().context("CLI arguments parsing")?; setup_logging(config.log_file.as_deref()).context("unable to initialize logging")?; @@ -17,13 +18,12 @@ fn main() -> anyhow::Result<()> { let event_loop = EventLoop::::with_user_event().build()?; let event_loop_proxy = event_loop.create_proxy(); let (input_event_sender, input_event_receiver) = RdpInputEvent::create_channel(); - let mut app = App::new(&event_loop, &input_event_sender).context("unable to initialize App")?; - - // TODO: get window size & scale factor from GUI/App - let window_size = (1024, 768); - config.connector.desktop_scale_factor = 0; - config.connector.desktop_size.width = window_size.0; - config.connector.desktop_size.height = window_size.1; + let initial_window_size = PhysicalSize::new( + u32::from(config.connector.desktop_size.width), + u32::from(config.connector.desktop_size.height), + ); + let mut app = + App::new(&event_loop, &input_event_sender, initial_window_size).context("unable to initialize App")?; let rt = runtime::Builder::new_multi_thread() .enable_all() diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index 2a7e5e248..45bb9bdba 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -288,11 +288,11 @@ async fn connect( &mut ReqwestNetworkClient::new(), (&config.destination).into(), server_public_key.to_owned(), - None, + config.kerberos_config.clone(), ) .await?; - debug!(?connection_result); + debug!("Connection finalized"); Ok((connection_result, upgraded_framed)) } @@ -405,7 +405,7 @@ async fn connect_ws( &mut ReqwestNetworkClient::new(), (&config.destination).into(), server_public_key, - None, + config.kerberos_config.clone(), ) .await?; diff --git a/crates/ironrdp-client/tests/config_rdp_merge.rs b/crates/ironrdp-client/tests/config_rdp_merge.rs new file mode 100644 index 000000000..785734dd8 --- /dev/null +++ b/crates/ironrdp-client/tests/config_rdp_merge.rs @@ -0,0 +1,128 @@ +#![allow(unused_crate_dependencies)] + +use std::fs; +use std::path::PathBuf; + +use ironrdp_client::config::{ClipboardType, Config}; +use uuid::Uuid; + +fn write_rdp_file(content: &str) -> PathBuf { + let path = std::env::temp_dir().join(format!("ironrdp-client-rdp-{}.rdp", Uuid::new_v4())); + fs::write(&path, content).expect("failed to write temporary .rdp file"); + path +} + +fn parse_config_from_rdp(content: &str, extra_args: &[&str]) -> Config { + let rdp_file = write_rdp_file(content); + + let mut args = vec![ + "ironrdp-client".to_owned(), + "--rdp-file".to_owned(), + rdp_file.display().to_string(), + ]; + + args.extend(extra_args.iter().map(|arg| (*arg).to_owned())); + + let result = Config::parse_from(args); + let _ = fs::remove_file(rdp_file); + + result.expect("failed to parse client config") +} + +#[test] +fn gateway_is_disabled_when_gateway_usage_method_is_zero() { + let config = parse_config_from_rdp( + "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\ngatewayhostname:s:gw.example.com:443\ngatewayusagemethod:i:0\n", + &[], + ); + + assert!(config.gw.is_none()); +} + +#[test] +fn gateway_is_disabled_when_gateway_usage_method_is_four() { + let config = parse_config_from_rdp( + "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\ngatewayhostname:s:gw.example.com:443\ngatewayusagemethod:i:4\n", + &[], + ); + + assert!(config.gw.is_none()); +} + +#[test] +fn gateway_is_enabled_with_usage_method_one_and_file_credentials() { + let config = parse_config_from_rdp( + "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\ngatewayhostname:s:gw.example.com:443\ngatewayusagemethod:i:1\ngatewayusername:s:gw-user\nGatewayPassword:s:gw-pass\n", + &[], + ); + + let gw = config.gw.expect("gateway should be configured"); + assert_eq!(gw.gw_endpoint, "gw.example.com:443"); + assert_eq!(gw.gw_user, "gw-user"); + assert_eq!(gw.gw_pass, "gw-pass"); +} + +#[test] +fn unsupported_gateway_credential_source_falls_back_to_username_password() { + let config = parse_config_from_rdp( + "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\ngatewayhostname:s:gw.example.com:443\ngatewayusagemethod:i:1\ngatewaycredentialssource:i:2\ngatewayusername:s:gw-user\nGatewayPassword:s:gw-pass\n", + &[], + ); + + let gw = config.gw.expect("gateway should be configured"); + assert_eq!(gw.gw_user, "gw-user"); + assert_eq!(gw.gw_pass, "gw-pass"); +} + +#[test] +fn no_credssp_cli_flag_overrides_rdp_enable_credssp_property() { + let config = parse_config_from_rdp( + "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\nenablecredsspsupport:i:1\n", + &["--no-credssp"], + ); + + assert!(!config.connector.enable_credssp); +} + +#[test] +fn kdc_proxy_name_is_normalized_to_https_url() { + let config = parse_config_from_rdp( + "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\nkdcproxyname:s:kdc.example.com\n", + &[], + ); + + let kerberos = config.kerberos_config.expect("kerberos config should be present"); + let kdc_proxy_url = kerberos.kdc_proxy_url.expect("kdc proxy url should be present"); + + assert_eq!(kdc_proxy_url.as_str(), "https://kdc.example.com/KdcProxy"); +} + +#[test] +fn redirectclipboard_zero_disables_clipboard_for_default_mode() { + let config = parse_config_from_rdp( + "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\nredirectclipboard:i:0\n", + &[], + ); + + assert!(matches!(config.clipboard_type, ClipboardType::None)); +} + +#[test] +fn audiomode_two_disables_audio_playback() { + let config = parse_config_from_rdp( + "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\naudiomode:i:2\n", + &[], + ); + + assert!(!config.connector.enable_audio_playback); +} + +#[test] +fn invalid_audiomode_falls_back_to_audio_playback_enabled() { + let config = parse_config_from_rdp( + "full address:s:rdp.example.com\nusername:s:test-user\nClearTextPassword:s:test-pass\naudiomode:i:99\n", + &[], + ); + + assert!(config.connector.enable_audio_playback); +} diff --git a/crates/ironrdp-rdpfile/README.md b/crates/ironrdp-rdpfile/README.md index 3d9d8483c..159f70340 100644 --- a/crates/ironrdp-rdpfile/README.md +++ b/crates/ironrdp-rdpfile/README.md @@ -2,6 +2,11 @@ Loader and writer for the .RDP file format. +This crate provides generic parsing and writing of `.rdp` key/value pairs. +Runtime support for specific properties depends on each consumer. + +For the native client supported-property list, see `crates/ironrdp-client/README.md`. + This crate is part of the [IronRDP] project. [IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/xtask/src/cov.rs b/xtask/src/cov.rs index 50e073aba..af78e74f6 100644 --- a/xtask/src/cov.rs +++ b/xtask/src/cov.rs @@ -53,6 +53,7 @@ pub fn report(sh: &Shell, html_report: bool) -> anyhow::Result<()> { if html_report { cmd!(sh, "{CARGO} llvm-cov --html") + .arg("--no-cfg-coverage") .arg("--ignore-filename-regex") .arg(COV_IGNORE_REGEX) .run()?; @@ -283,6 +284,7 @@ impl CoverageReport { let output = cmd!( sh, "{CARGO} llvm-cov + --no-cfg-coverage --ignore-filename-regex {COV_IGNORE_REGEX} --json" )