Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
216 changes: 216 additions & 0 deletions crates/ironrdp-cfg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,153 @@ 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; see
/// [`Self::unrecognized_entries`].
///
/// 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 Self::entries(value) {
match entry {
Some(DriveEntry::All) => {
selection.all = true;
selection.dynamic = true;
}
Some(DriveEntry::Dynamic) => selection.dynamic = true,
Some(DriveEntry::Letter(index)) => selection.letters |= 1 << index,
None => {}
}
}
Comment thread
mamoreau-devolutions marked this conversation as resolved.
if selection.all {
// `*` already selects every letter.
selection.letters = 0;
}
selection
}

/// Returns the entries of a `drivestoredirect` value that [`Self::parse`] ignores, so callers
/// can report them.
pub fn unrecognized_entries(value: &str) -> impl Iterator<Item = &str> {
Self::entries(value).filter_map(|(text, entry)| entry.is_none().then_some(text))
}

/// Splits a `drivestoredirect` value into its nonempty entries and their meaning.
fn entries(value: &str) -> impl Iterator<Item = (&str, Option<DriveEntry>)> {
value
.split(';')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(|entry| {
let kind = if entry == "*" {
Some(DriveEntry::All)
} else if entry.eq_ignore_ascii_case(Self::DYNAMIC_DRIVES) {
Some(DriveEntry::Dynamic)
} else {
drive_letter_index(entry).map(DriveEntry::Letter)
};
(entry, kind)
})
}

Comment thread
mamoreau-devolutions marked this conversation as resolved.
/// 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 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(())
}
}

/// A recognized `drivestoredirect` entry.
#[derive(Clone, Copy)]
enum DriveEntry {
/// `*`
All,
/// `DynamicDrives`
Dynamic,
/// A drive letter, as its bit index.
Letter(u32),
}

/// 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'/'] if letter.is_ascii_alphabetic() => {
Some(u32::from(letter.to_ascii_uppercase() - b'A'))
}
_ => None,
}
}

/// 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 +558,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 +641,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 @@ -688,6 +853,16 @@ pub trait PropertySetExt {
/// `gatewayusername`, and both `GatewayPassword` casings).
fn clear_gateway(&mut self);

/// Local drive selection resolved from `drivestoredirect` and the legacy `redirectdrives`.
///
/// An explicit `redirectdrives:i:0` disables drive redirection, so clearing the legacy flag
/// cannot leave a stale `drivestoredirect` list exposed. Otherwise a nonempty
/// `drivestoredirect` takes precedence, and `redirectdrives:i:1` alone 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 @@ -838,6 +1013,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 @@ -968,6 +1155,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 @@ -1359,6 +1558,23 @@ 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(false)) => Some(DriveRedirection::default()),
(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 @@ -449,6 +453,12 @@ pub struct RdpdrConfig {
#[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,

/// Announce the serial and parallel ports the RDPDR backend factory provides.
///
/// Mirrors the `.rdp` `redirectcomports` property. Which local ports exist and are
Expand All @@ -463,6 +473,7 @@ impl Default for RdpdrConfig {
enabled: true,
#[cfg(feature = "smartcard")]
smartcard: true,
drives: DriveRedirection::default(),
ports: true,
}
}
Expand Down Expand Up @@ -1504,6 +1515,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 serial and parallel port redirection within RDPDR (`redirectcomports`).
#[cfg(feature = "rdpdr")]
#[must_use]
Expand Down Expand Up @@ -2403,6 +2423,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
Loading
Loading