Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions crates/ironrdp-cfg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,129 @@ impl core::fmt::Display for DvcPipeSpecMissingDelimiter {

impl core::error::Error for DvcPipeSpecMissingDelimiter {}

/// Local drives selected for redirection.
///
/// Corresponds to the `drivestoredirect` `.rdp` property, using the mstsc syntax: entries are
/// separated by `;`, `*` selects every drive (including drives connected later), `DynamicDrives`
/// selects drives connected later, and `C:\` (or `C:`, `C`) selects one drive letter.
/// Unrecognized entries, such as FreeRDP-specific paths, are ignored.
///
/// Selecting every drive implies dynamic drives, because `*` cannot express one without the other.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DriveRedirection {
/// INVARIANT: only the low 26 bits are used; bit `n` selects the letter `'A' + n`.
letters: u32,
all: bool,
dynamic: bool,
}

impl DriveRedirection {
const DYNAMIC_DRIVES: &str = "DynamicDrives";

/// Selects every local drive, including drives connected later (`*`).
pub fn all() -> Self {
Self {
letters: 0,
all: true,
dynamic: true,
}
}

/// Parses a `drivestoredirect` value.
pub fn parse(value: &str) -> Self {
let mut selection = Self::default();
for entry in value.split(';').map(str::trim).filter(|entry| !entry.is_empty()) {
if entry == "*" {
selection.all = true;
selection.dynamic = true;
} else if entry.eq_ignore_ascii_case(Self::DYNAMIC_DRIVES) {
selection.dynamic = true;
} else if let Some(index) = drive_letter_index(entry) {
selection.letters |= 1 << index;
}
}
Comment thread
mamoreau-devolutions marked this conversation as resolved.
if selection.all {
// `*` already selects every letter.
selection.letters = 0;
}
selection
}

/// Returns this selection with drives connected later enabled or disabled.
///
/// Has no effect on a selection of every drive.
#[must_use]
pub fn with_dynamic(mut self, enabled: bool) -> Self {
self.dynamic = enabled || self.all;
self
}

/// Whether every local drive is selected.
pub fn redirects_all(&self) -> bool {
self.all
}

/// Whether drives connected during the session are selected.
pub fn redirects_dynamic(&self) -> bool {
self.dynamic
}

/// Returns whether the drive letter is selected explicitly or through [`Self::redirects_all`].
pub fn contains_letter(&self, letter: char) -> bool {
self.all
|| u8::try_from(letter)
.ok()
.and_then(|letter| drive_letter_index_of(letter.to_ascii_uppercase()))
.is_some_and(|index| self.letters & (1 << index) != 0)
}
Comment thread
mamoreau-devolutions marked this conversation as resolved.
Outdated

/// Returns the uppercase drive letters selected explicitly, in alphabetical order.
pub fn letters(&self) -> impl Iterator<Item = char> + '_ {
(b'A'..=b'Z')
.zip(0u32..)
.filter(|(_, index)| self.letters & (1 << index) != 0)
.map(|(letter, _)| char::from(letter))
}

/// Whether no drive is selected.
pub fn is_empty(&self) -> bool {
self.letters == 0 && !self.all && !self.dynamic
}
}

impl core::fmt::Display for DriveRedirection {
/// Formats the selection as a `drivestoredirect` value.
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.all {
return f.write_str("*");
}

let mut separator = "";
for letter in self.letters() {
write!(f, "{separator}{letter}:\\")?;
separator = ";";
}
if self.dynamic {
write!(f, "{separator}{}", Self::DYNAMIC_DRIVES)?;
}
Ok(())
}
}

/// Returns the bit index of a `C`, `C:`, `C:\` or `C:/` drive entry.
fn drive_letter_index(entry: &str) -> Option<u32> {
match entry.as_bytes() {
[letter] | [letter, b':'] | [letter, b':', b'\\' | b'/'] => drive_letter_index_of(letter.to_ascii_uppercase()),
_ => None,
}
}

fn drive_letter_index_of(uppercase_letter: u8) -> Option<u32> {
uppercase_letter
.is_ascii_uppercase()
.then(|| u32::from(uppercase_letter - b'A'))
}
Comment thread
mamoreau-devolutions marked this conversation as resolved.
Outdated

/// Typed accessors for the RDP properties IronRDP understands.
///
/// Every property is exposed as a triplet of methods sharing the same underlying key: a getter
Expand Down Expand Up @@ -411,6 +534,15 @@ pub trait PropertySetExt {
/// Removes the `domain` property.
fn clear_domain(&mut self);

/// Raw local drive selection (`drivestoredirect`); see [`DriveRedirection`] for the syntax.
///
/// Prefer [`Self::drive_redirection`], which also honors the legacy `redirectdrives` key.
fn drives_to_redirect(&self) -> Option<&str>;
/// Sets the `drivestoredirect` property.
fn set_drives_to_redirect(&mut self, value: impl Into<String>);
/// Removes the `drivestoredirect` property.
fn clear_drives_to_redirect(&mut self);

/// Whether CredSSP/NLA support is enabled (`enablecredsspsupport`).
fn enable_credssp_support(&self) -> Option<bool>;
/// Sets the `enablecredsspsupport` property.
Expand Down Expand Up @@ -485,6 +617,15 @@ pub trait PropertySetExt {
/// Removes the `redirectclipboard` property.
fn clear_redirect_clipboard(&mut self);

/// Whether every local drive is redirected (`redirectdrives`, legacy).
///
/// Prefer [`Self::drive_redirection`], which gives `drivestoredirect` precedence.
fn redirect_drives(&self) -> Option<bool>;
/// Sets the `redirectdrives` property.
fn set_redirect_drives(&mut self, value: bool);
/// Removes the `redirectdrives` property.
fn clear_redirect_drives(&mut self);

/// Whether WebAuthn redirection is requested (`redirectwebauthn`).
fn redirect_webauthn(&self) -> Option<bool>;
/// Sets the `redirectwebauthn` property.
Expand Down Expand Up @@ -681,6 +822,14 @@ pub trait PropertySetExt {
/// `gatewayusername`, and both `GatewayPassword` casings).
fn clear_gateway(&mut self);

/// Local drive selection resolved from `drivestoredirect` and the legacy `redirectdrives`.
///
/// A nonempty `drivestoredirect` takes precedence. Otherwise `redirectdrives:i:1` selects every
/// drive, as in FreeRDP. Returns `None` when neither key is present.
fn drive_redirection(&self) -> Option<DriveRedirection>;
/// Writes `drivestoredirect` and `redirectdrives` from a drive selection.
fn set_drive_redirection(&mut self, value: DriveRedirection);

/// Removes every RDCleanPath-related key (`ironrdp_rdcleanpathurl` and
/// `ironrdp_rdcleanpathtoken`).
fn clear_rdcleanpath(&mut self);
Expand Down Expand Up @@ -831,6 +980,18 @@ impl PropertySetExt for PropertySet {
self.remove("domain");
}

fn drives_to_redirect(&self) -> Option<&str> {
self.get::<&str>("drivestoredirect")
}

fn set_drives_to_redirect(&mut self, value: impl Into<String>) {
self.insert("drivestoredirect", value.into());
}

fn clear_drives_to_redirect(&mut self) {
self.remove("drivestoredirect");
}

fn enable_credssp_support(&self) -> Option<bool> {
self.get::<bool>("enablecredsspsupport")
}
Expand Down Expand Up @@ -961,6 +1122,18 @@ impl PropertySetExt for PropertySet {
self.remove("redirectclipboard");
}

fn redirect_drives(&self) -> Option<bool> {
self.get::<bool>("redirectdrives")
}

fn set_redirect_drives(&mut self, value: bool) {
self.insert("redirectdrives", value);
}

fn clear_redirect_drives(&mut self) {
self.remove("redirectdrives");
}

fn redirect_webauthn(&self) -> Option<bool> {
self.get::<bool>("redirectwebauthn")
}
Expand Down Expand Up @@ -1340,6 +1513,22 @@ impl PropertySetExt for PropertySet {
self.remove("GatewayPassword");
}

fn drive_redirection(&self) -> Option<DriveRedirection> {
let drives_to_redirect = self.drives_to_redirect();
let redirect_drives = self.redirect_drives();
match (drives_to_redirect.map(str::trim), redirect_drives) {
(Some(value), _) if !value.is_empty() => Some(DriveRedirection::parse(value)),
(_, Some(true)) => Some(DriveRedirection::all()),
(None, None) => None,
_ => Some(DriveRedirection::default()),
}
}

fn set_drive_redirection(&mut self, value: DriveRedirection) {
self.set_drives_to_redirect(value.to_string());
self.set_redirect_drives(!value.is_empty());
}
Comment thread
mamoreau-devolutions marked this conversation as resolved.

fn clear_rdcleanpath(&mut self) {
self.remove("ironrdp_rdcleanpathurl");
self.remove("ironrdp_rdcleanpathtoken");
Expand Down
24 changes: 24 additions & 0 deletions crates/ironrdp-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ use ironrdp_propertyset::PropertySet;
use ironrdp_rail::pdu::ExecutePdu;
use url::Url;

/// Local drive selection, as configured with [`ConfigBuilder::with_drive_redirection`].
#[cfg(feature = "rdpdr")]
pub use ironrdp_cfg::DriveRedirection;

/// RDP-UDP protocol version, as offered with [`ConfigBuilder::with_udp_offer_version`].
#[cfg(feature = "udp")]
pub use ironrdp_rdpeudp::pdu::UdpVersion;
Expand Down Expand Up @@ -435,6 +439,12 @@ pub struct RdpdrConfig {
/// Enable smart-card redirection within RDPDR.
#[cfg(feature = "smartcard")]
pub smartcard: bool,

/// Local drives requested for redirection.
///
/// The client does not enumerate drives itself: frontends read this selection to configure
/// their native RDPDR backend factory.
pub drives: DriveRedirection,
}

#[cfg(feature = "rdpdr")]
Expand All @@ -444,6 +454,7 @@ impl Default for RdpdrConfig {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: true,
drives: DriveRedirection::default(),
}
}
}
Expand Down Expand Up @@ -1484,6 +1495,15 @@ impl ConfigBuilder {
self
}

/// Selects the local drives to redirect (`drivestoredirect`).
#[cfg(feature = "rdpdr")]
#[must_use]
pub fn with_drive_redirection(mut self, drives: DriveRedirection) -> Self {
self.channels.rdpdr.drives = drives;
self.properties.set_drive_redirection(drives);
self
}

/// Enable or disable smart-card redirection within RDPDR.
#[cfg(feature = "smartcard")]
#[must_use]
Expand Down Expand Up @@ -2337,6 +2357,10 @@ impl ConfigBuilder {
if let Some(enabled) = ps.enable_rdpdr() {
self.channels.rdpdr.enabled = enabled;
}
#[cfg(feature = "rdpdr")]
if let Some(drives) = ps.drive_redirection() {
self.channels.rdpdr.drives = drives;
}
#[cfg(feature = "smartcard")]
if let Some(enabled) = ps.enable_smartcard() {
self.channels.rdpdr.smartcard = enabled;
Expand Down
10 changes: 10 additions & 0 deletions crates/ironrdp-client/src/rdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4916,6 +4916,7 @@ mod tests {
enabled: false,
#[cfg(feature = "smartcard")]
smartcard: false,
drives: Default::default(),
};

assert!(
Expand All @@ -4936,6 +4937,7 @@ mod tests {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: false,
drives: Default::default(),
};

assert!(
Expand All @@ -4956,6 +4958,7 @@ mod tests {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: false,
drives: Default::default(),
};

let mut rdpdr = build_rdpdr_channel(Some(&factory), &config, RdpdrDevicePolicy::ALLOW_ALL)
Expand Down Expand Up @@ -5025,6 +5028,7 @@ mod tests {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: false,
drives: Default::default(),
};

assert!(build_rdpdr_channel(Some(&factory), &config, RdpdrDevicePolicy::ALLOW_ALL).is_err());
Expand All @@ -5038,6 +5042,7 @@ mod tests {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: false,
drives: Default::default(),
};

let mut rdpdr = build_rdpdr_channel(Some(&factory), &config, RdpdrDevicePolicy::ALLOW_ALL)
Expand Down Expand Up @@ -5077,6 +5082,7 @@ mod tests {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: false,
drives: Default::default(),
};
let policy = RdpdrDevicePolicy {
drives: true,
Expand All @@ -5099,6 +5105,7 @@ mod tests {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: false,
drives: Default::default(),
};

assert!(
Expand All @@ -5116,6 +5123,7 @@ mod tests {
let config = crate::config::RdpdrConfig {
enabled: true,
smartcard: true,
drives: Default::default(),
};

let mut rdpdr = build_rdpdr_channel(Some(&factory), &config, DRIVES_RESTRICTED)
Expand Down Expand Up @@ -5209,6 +5217,7 @@ mod tests {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: false,
drives: Default::default(),
};

let first = build_rdpdr_channel(Some(&factory), &config, RdpdrDevicePolicy::ALLOW_ALL)
Expand Down Expand Up @@ -5243,6 +5252,7 @@ mod tests {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: false,
drives: Default::default(),
};
let mut rdpdr = build_rdpdr_channel(Some(&factory), &config, RdpdrDevicePolicy::ALLOW_ALL)
.expect("RDPDR channel should build")
Expand Down
5 changes: 5 additions & 0 deletions crates/ironrdp-daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ Smartcard is also honored from connect/overlay property `ironrdp_smartcard`:
- connect-time `ironrdp_smartcard:i:1` can enable smartcard-only without a startup flag
- connect-time `ironrdp_smartcard:i:0` disables smartcard for that session

Connect properties `drivestoredirect` and `redirectdrives` select drive letters with the mstsc
syntax, as in `.rdp` files. With `*` or `DynamicDrives`, the session also announces drives
connected while it runs. Configured volumes use device IDs from `27`, because `1` to `26` belong to
drive letters.

Smartcard-only sessions use an empty drive list with device ID `0` reserved for the smartcard device.
Loading
Loading