Skip to content

Commit d0e18e3

Browse files
authored
feat: improved RDP file support (#1220)
ironrdp-rdpfile: - ErrorKind::InvalidValue and UnknownType now carry the property key - ErrorKind::MalformedLine carries no data (no risk of leaking secrets) - ErrorKind::InvalidValue carries no value (no risk of leaking secrets) - Line numbers are 1-based in all errors ironrdp-cfg: - Add GatewayUsageMethod, GatewayCredentialsSource, AudioMode proper enums with TryFrom<i64> and corresponding Unknown* error types - Add TargetAddr / TargetHost types with full IPv6 bracketing support; ParseTargetAddrError is a Copy enum (no allocation, no sensitive data) - Add 15+ new PropertySetExt methods: full_address, alternate_full_address, server_port, domain, enable_credssp_support, compression, gateway_usage_method, gateway_credentials_source, gateway_username, gateway_password, desktop_width/height/scale_factor, alternate_shell, shell_working_directory, redirect_clipboard, audio_mode, kdc_proxy_name, kdc_proxy_url ironrdp-client: - Two-phase config parsing: PartialConfig (phase 1, no prompts) and into_config() (phase 2, interactive prompts + strong typing) - Logger is set up right after PartialConfig::parse_from(), before phase 2 - New --dump-rdp <path> flag: writes merged PropertySet as .rdp file and exits immediately (between phases, no session started) - Map .rdp file properties to Config fields: gateway host/method/creds, desktop dimensions, clipboard redirect, audio mode, alternate shell, shell working dir, KDC proxy, compression, CredSSP toggle, domain - Config.kerberos_config wired through both connect() call sites - initial_window_size derived from config desktop dimensions - Update README with supported .rdp properties and precedence rules
1 parent 53d814e commit d0e18e3

16 files changed

Lines changed: 1073 additions & 121 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironrdp-cfg/src/lib.rs

Lines changed: 287 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,319 @@
1-
// QUESTION: consider auto-generating this file based on a reference file?
2-
// https://gist.github.com/awakecoding/838c7fe2ed3a6208e3ca5d8af25363f6
1+
mod target_addr;
2+
pub use target_addr::{ParseTargetAddrError, TargetAddr, TargetHost};
33

44
use ironrdp_propertyset::PropertySet;
55

6+
/// Error returned when the `server port` property value is outside the valid port range (1–65535).
7+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8+
pub struct InvalidServerPort;
9+
10+
impl core::fmt::Display for InvalidServerPort {
11+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
12+
f.write_str("server port value is out of the valid port range (1-65535)")
13+
}
14+
}
15+
16+
impl core::error::Error for InvalidServerPort {}
17+
18+
/// Error returned when a desktop dimension or scale factor property value is out of range.
19+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20+
pub struct InvalidDesktopSize;
21+
22+
impl core::fmt::Display for InvalidDesktopSize {
23+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
24+
f.write_str("desktop size property value is out of range")
25+
}
26+
}
27+
28+
impl core::error::Error for InvalidDesktopSize {}
29+
30+
/// Controls whether and how an RD Gateway server is used.
31+
///
32+
/// Corresponds to the `gatewayusagemethod` `.rdp` property.
33+
/// See also: <https://learn.microsoft.com/en-us/windows/win32/termserv/rdp-file-settings>
34+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35+
pub enum GatewayUsageMethod {
36+
/// 0: Do not use an RD Gateway server.
37+
Direct,
38+
/// 1: Always use an RD Gateway server.
39+
UseAlways,
40+
/// 2: Use an RD Gateway server, bypass for local addresses.
41+
UseBypassLocal,
42+
/// 3: Use an RD Gateway server, never bypass.
43+
UseNeverBypass,
44+
/// 4: Automatically detect RD Gateway settings (client-side heuristic; no explicit gateway configured).
45+
Automatic,
46+
}
47+
48+
impl GatewayUsageMethod {
49+
/// Returns `true` when the file explicitly requires routing through a gateway server.
50+
pub fn is_gateway_required(self) -> bool {
51+
matches!(self, Self::UseAlways | Self::UseBypassLocal | Self::UseNeverBypass)
52+
}
53+
54+
/// Returns the raw integer value for writing to a `.rdp` property set.
55+
pub fn as_i64(self) -> i64 {
56+
match self {
57+
Self::Direct => 0,
58+
Self::UseAlways => 1,
59+
Self::UseBypassLocal => 2,
60+
Self::UseNeverBypass => 3,
61+
Self::Automatic => 4,
62+
}
63+
}
64+
}
65+
66+
impl TryFrom<i64> for GatewayUsageMethod {
67+
type Error = UnknownGatewayUsageMethod;
68+
69+
fn try_from(value: i64) -> Result<Self, Self::Error> {
70+
match value {
71+
0 => Ok(Self::Direct),
72+
1 => Ok(Self::UseAlways),
73+
2 => Ok(Self::UseBypassLocal),
74+
3 => Ok(Self::UseNeverBypass),
75+
4 => Ok(Self::Automatic),
76+
_ => Err(UnknownGatewayUsageMethod(value)),
77+
}
78+
}
79+
}
80+
81+
/// Error returned when a `gatewayusagemethod` value is not a recognized variant.
82+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83+
pub struct UnknownGatewayUsageMethod(pub i64);
84+
85+
impl core::fmt::Display for UnknownGatewayUsageMethod {
86+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87+
write!(f, "unknown gatewayusagemethod value: {}", self.0)
88+
}
89+
}
90+
91+
impl core::error::Error for UnknownGatewayUsageMethod {}
92+
93+
/// Controls which credentials are used to authenticate to the RD Gateway.
94+
///
95+
/// Corresponds to the `gatewaycredentialssource` `.rdp` property.
96+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97+
pub enum GatewayCredentialsSource {
98+
/// 0: Use the same credentials as the RDP server (pass-through / NTLM).
99+
UseServerCredentials,
100+
/// 1: Use the gateway-specific user credentials.
101+
UseUserCredentials,
102+
/// 2: Use credentials stored in a profile.
103+
UseProfile,
104+
/// 3: Prompt the user for gateway credentials.
105+
Prompt,
106+
/// 4: Use a smart card.
107+
SmartCard,
108+
/// 5: Use the logged-on user's credentials.
109+
UseLogonCredentials,
110+
}
111+
112+
impl TryFrom<i64> for GatewayCredentialsSource {
113+
type Error = UnknownGatewayCredentialsSource;
114+
115+
fn try_from(value: i64) -> Result<Self, Self::Error> {
116+
match value {
117+
0 => Ok(Self::UseServerCredentials),
118+
1 => Ok(Self::UseUserCredentials),
119+
2 => Ok(Self::UseProfile),
120+
3 => Ok(Self::Prompt),
121+
4 => Ok(Self::SmartCard),
122+
5 => Ok(Self::UseLogonCredentials),
123+
_ => Err(UnknownGatewayCredentialsSource(value)),
124+
}
125+
}
126+
}
127+
128+
/// Error returned when a `gatewaycredentialssource` value is not a recognized variant.
129+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130+
pub struct UnknownGatewayCredentialsSource(pub i64);
131+
132+
impl core::fmt::Display for UnknownGatewayCredentialsSource {
133+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
134+
write!(f, "unknown gatewaycredentialssource value: {}", self.0)
135+
}
136+
}
137+
138+
impl core::error::Error for UnknownGatewayCredentialsSource {}
139+
140+
/// Controls where audio is played during a remote session.
141+
///
142+
/// Corresponds to the `audiomode` `.rdp` property.
143+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144+
pub enum AudioMode {
145+
/// 0: Redirect audio to the local (client) machine.
146+
RedirectToClient,
147+
/// 1: Play audio on the remote computer.
148+
PlayOnServer,
149+
/// 2: Do not play audio.
150+
Disabled,
151+
}
152+
153+
impl TryFrom<i64> for AudioMode {
154+
type Error = UnknownAudioMode;
155+
156+
fn try_from(value: i64) -> Result<Self, Self::Error> {
157+
match value {
158+
0 => Ok(Self::RedirectToClient),
159+
1 => Ok(Self::PlayOnServer),
160+
2 => Ok(Self::Disabled),
161+
_ => Err(UnknownAudioMode(value)),
162+
}
163+
}
164+
}
165+
166+
/// Error returned when an `audiomode` value is not a recognized variant.
167+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168+
pub struct UnknownAudioMode(pub i64);
169+
170+
impl core::fmt::Display for UnknownAudioMode {
171+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
172+
write!(f, "unknown audiomode value: {}", self.0)
173+
}
174+
}
175+
176+
impl core::error::Error for UnknownAudioMode {}
177+
6178
pub trait PropertySetExt {
7-
fn full_address(&self) -> Option<&str>;
179+
fn full_address(&self) -> Result<Option<TargetAddr>, ParseTargetAddrError>;
180+
181+
fn server_port(&self) -> Result<Option<u16>, InvalidServerPort>;
182+
183+
fn alternate_full_address(&self) -> Result<Option<TargetAddr>, ParseTargetAddrError>;
8184

9-
fn server_port(&self) -> Option<i64>;
185+
fn domain(&self) -> Option<&str>;
10186

11-
fn alternate_full_address(&self) -> Option<&str>;
187+
fn enable_credssp_support(&self) -> Option<bool>;
188+
189+
fn compression(&self) -> Option<bool>;
12190

13191
fn gateway_hostname(&self) -> Option<&str>;
14192

193+
fn gateway_usage_method(&self) -> Result<Option<GatewayUsageMethod>, UnknownGatewayUsageMethod>;
194+
195+
fn gateway_credentials_source(&self) -> Result<Option<GatewayCredentialsSource>, UnknownGatewayCredentialsSource>;
196+
197+
fn gateway_username(&self) -> Option<&str>;
198+
199+
fn gateway_password(&self) -> Option<&str>;
200+
201+
fn desktop_width(&self) -> Result<Option<u16>, InvalidDesktopSize>;
202+
203+
fn desktop_height(&self) -> Result<Option<u16>, InvalidDesktopSize>;
204+
205+
fn desktop_scale_factor(&self) -> Result<Option<u32>, InvalidDesktopSize>;
206+
207+
fn alternate_shell(&self) -> Option<&str>;
208+
209+
fn shell_working_directory(&self) -> Option<&str>;
210+
211+
fn redirect_clipboard(&self) -> Option<bool>;
212+
213+
fn audio_mode(&self) -> Result<Option<AudioMode>, UnknownAudioMode>;
214+
15215
fn remote_application_name(&self) -> Option<&str>;
16216

17217
fn remote_application_program(&self) -> Option<&str>;
18218

19219
fn kdc_proxy_url(&self) -> Option<&str>;
20220

221+
fn kdc_proxy_name(&self) -> Option<&str>;
222+
21223
fn username(&self) -> Option<&str>;
22224

23225
/// Target RDP server password - use for testing only
24226
fn clear_text_password(&self) -> Option<&str>;
25227
}
26228

27229
impl PropertySetExt for PropertySet {
28-
fn full_address(&self) -> Option<&str> {
29-
self.get::<&str>("full address")
230+
fn full_address(&self) -> Result<Option<TargetAddr>, ParseTargetAddrError> {
231+
self.get::<&str>("full address").map(|s| s.parse()).transpose()
30232
}
31233

32-
fn server_port(&self) -> Option<i64> {
234+
fn server_port(&self) -> Result<Option<u16>, InvalidServerPort> {
33235
self.get::<i64>("server port")
236+
.map(|p| u16::try_from(p).ok().filter(|&p| p != 0).ok_or(InvalidServerPort))
237+
.transpose()
34238
}
35239

36-
fn alternate_full_address(&self) -> Option<&str> {
240+
fn alternate_full_address(&self) -> Result<Option<TargetAddr>, ParseTargetAddrError> {
37241
self.get::<&str>("alternate full address")
242+
.map(|s| s.parse())
243+
.transpose()
244+
}
245+
246+
fn domain(&self) -> Option<&str> {
247+
self.get::<&str>("domain")
248+
}
249+
250+
fn enable_credssp_support(&self) -> Option<bool> {
251+
self.get::<bool>("enablecredsspsupport")
252+
}
253+
254+
fn compression(&self) -> Option<bool> {
255+
self.get::<bool>("compression")
38256
}
39257

40258
fn gateway_hostname(&self) -> Option<&str> {
41259
self.get::<&str>("gatewayhostname")
42260
}
43261

262+
fn gateway_usage_method(&self) -> Result<Option<GatewayUsageMethod>, UnknownGatewayUsageMethod> {
263+
self.get::<i64>("gatewayusagemethod")
264+
.map(GatewayUsageMethod::try_from)
265+
.transpose()
266+
}
267+
268+
fn gateway_credentials_source(&self) -> Result<Option<GatewayCredentialsSource>, UnknownGatewayCredentialsSource> {
269+
self.get::<i64>("gatewaycredentialssource")
270+
.map(GatewayCredentialsSource::try_from)
271+
.transpose()
272+
}
273+
274+
fn gateway_username(&self) -> Option<&str> {
275+
self.get::<&str>("gatewayusername")
276+
}
277+
278+
fn gateway_password(&self) -> Option<&str> {
279+
self.get::<&str>("GatewayPassword")
280+
.or_else(|| self.get::<&str>("gatewaypassword"))
281+
}
282+
283+
fn desktop_width(&self) -> Result<Option<u16>, InvalidDesktopSize> {
284+
self.get::<i64>("desktopwidth")
285+
.map(|v| u16::try_from(v).map_err(|_| InvalidDesktopSize))
286+
.transpose()
287+
}
288+
289+
fn desktop_height(&self) -> Result<Option<u16>, InvalidDesktopSize> {
290+
self.get::<i64>("desktopheight")
291+
.map(|v| u16::try_from(v).map_err(|_| InvalidDesktopSize))
292+
.transpose()
293+
}
294+
295+
fn desktop_scale_factor(&self) -> Result<Option<u32>, InvalidDesktopSize> {
296+
self.get::<i64>("desktopscalefactor")
297+
.map(|v| u32::try_from(v).map_err(|_| InvalidDesktopSize))
298+
.transpose()
299+
}
300+
301+
fn alternate_shell(&self) -> Option<&str> {
302+
self.get::<&str>("alternate shell")
303+
}
304+
305+
fn shell_working_directory(&self) -> Option<&str> {
306+
self.get::<&str>("shell working directory")
307+
}
308+
309+
fn redirect_clipboard(&self) -> Option<bool> {
310+
self.get::<bool>("redirectclipboard")
311+
}
312+
313+
fn audio_mode(&self) -> Result<Option<AudioMode>, UnknownAudioMode> {
314+
self.get::<i64>("audiomode").map(AudioMode::try_from).transpose()
315+
}
316+
44317
fn remote_application_name(&self) -> Option<&str> {
45318
self.get::<&str>("remoteapplicationname")
46319
}
@@ -51,6 +324,11 @@ impl PropertySetExt for PropertySet {
51324

52325
fn kdc_proxy_url(&self) -> Option<&str> {
53326
self.get::<&str>("kdcproxyurl")
327+
.or_else(|| self.get::<&str>("KDCProxyURL"))
328+
}
329+
330+
fn kdc_proxy_name(&self) -> Option<&str> {
331+
self.get::<&str>("kdcproxyname")
54332
}
55333

56334
fn username(&self) -> Option<&str> {

0 commit comments

Comments
 (0)