feat(rdpdr): drive announce (#234)

Initial work for drive redirection.

Co-authored-by: Benoît CORTIER <bcortier@proton.me>
This commit is contained in:
Isaiah Becker-Mayer 2023-10-25 04:04:28 +00:00 committed by GitHub
parent a2cb31a3d7
commit 2075833ff2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 123 additions and 29 deletions

View file

@ -7,6 +7,7 @@ use crate::pdu::{
use ironrdp_svc::AsAny;
pub trait ClipboardError: std::error::Error + Send + Sync + 'static {}
impl<T> ClipboardError for T where T: std::error::Error + Send + Sync + 'static {}
/// Message sent by the OS clipboard backend event loop.
@ -39,12 +40,12 @@ pub enum ClipboardMessage {
/// Proxy to send messages from the os clipboard backend to the main application event loop
/// (e.g. winit event loop).
pub trait ClipboardMessageProxy: std::fmt::Debug + Send + Sync {
pub trait ClipboardMessageProxy: std::fmt::Debug + Send {
fn send_clipboard_message(&self, message: ClipboardMessage);
}
/// OS-specific clipboard backend interface.
pub trait CliprdrBackend: AsAny + std::fmt::Debug + Send + Sync + 'static {
pub trait CliprdrBackend: AsAny + std::fmt::Debug + Send {
/// Returns path to local temporary directory where clipboard-transferred files should be
/// stored.
fn temporary_directory(&self) -> &str;

View file

@ -6,9 +6,10 @@ use crate::pdu::{
};
use core::fmt;
use ironrdp_pdu::PduResult;
use ironrdp_svc::AsAny;
/// OS-specific device redirection backend inteface.
pub trait RdpdrBackend: fmt::Debug + Send {
pub trait RdpdrBackend: AsAny + fmt::Debug + Send {
fn handle_server_device_announce_response(&mut self, pdu: ServerDeviceAnnounceResponse) -> PduResult<()>;
fn handle_scard_call(&mut self, req: DeviceControlRequest<ScardIoCtlCode>, call: ScardCall) -> PduResult<()>;
}

View file

@ -4,10 +4,13 @@ use crate::pdu::{
esc::{ScardCall, ScardIoCtlCode},
};
use ironrdp_pdu::PduResult;
use ironrdp_svc::impl_as_any;
#[derive(Debug)]
pub struct NoopRdpdrBackend;
impl_as_any!(NoopRdpdrBackend);
impl RdpdrBackend for NoopRdpdrBackend {
fn handle_server_device_announce_response(&mut self, _pdu: ServerDeviceAnnounceResponse) -> PduResult<()> {
Ok(())

View file

@ -10,13 +10,14 @@ extern crate tracing;
pub mod backend;
pub mod pdu;
use crate::pdu::efs::MajorFunction;
pub use backend::{noop::NoopRdpdrBackend, RdpdrBackend};
use ironrdp_pdu::{cursor::ReadCursor, decode_cursor, gcc::ChannelName, other_err, PduResult};
use ironrdp_svc::{impl_as_any, CompressionCondition, StaticVirtualChannelProcessor, SvcMessage};
use pdu::efs::{
Capabilities, ClientDeviceListAnnounce, ClientNameRequest, ClientNameRequestUnicodeFlag, CoreCapability,
CoreCapabilityKind, DeviceControlRequest, DeviceIoRequest, Devices, ServerDeviceAnnounceResponse, VersionAndIdPdu,
VersionAndIdPduKind,
CoreCapabilityKind, DeviceControlRequest, DeviceIoRequest, DeviceType, Devices, ServerDeviceAnnounceResponse,
VersionAndIdPdu, VersionAndIdPduKind,
};
use pdu::esc::{ScardCall, ScardIoCtlCode};
use pdu::RdpdrPdu;
@ -41,6 +42,8 @@ pub struct Rdpdr {
backend: Box<dyn RdpdrBackend>,
}
impl_as_any!(Rdpdr);
impl Rdpdr {
pub const NAME: ChannelName = ChannelName::from_static(b"rdpdr\0\0\0");
@ -56,11 +59,42 @@ impl Rdpdr {
#[must_use]
pub fn with_smartcard(mut self, device_id: u32) -> Self {
self.device_list.add_smartcard(device_id);
self.capabilities.add_smartcard();
self.device_list.add_smartcard(device_id);
self
}
/// Adds drive redirection capability.
///
/// Callers may also include `initial_drives` to pre-configure the list of drives to announce to the server.
/// Note that drives do not need to be pre-configured in order to be redirected, a new drive can be announced
/// at any time during a session by calling [`Self::add_drive`].
#[must_use]
pub fn with_drives(mut self, initial_drives: Option<Vec<(u32, String)>>) -> Self {
self.capabilities.add_drive();
if let Some(initial_drives) = initial_drives {
for (device_id, path) in initial_drives {
self.device_list.add_drive(device_id, path);
}
}
self
}
/// Users should call this method to announce a new drive to the server. It's the caller's responsibility
/// to take the returned [`ClientDeviceListAnnounce`] and send it to the server.
pub fn add_drive(&mut self, device_id: u32, name: String) -> ClientDeviceListAnnounce {
self.device_list.add_drive(device_id, name.clone());
ClientDeviceListAnnounce::new_drive(device_id, name)
}
pub fn downcast_backend<T: RdpdrBackend>(&self) -> Option<&T> {
self.backend.as_any().downcast_ref::<T>()
}
pub fn downcast_backend_mut<T: RdpdrBackend>(&mut self) -> Option<&mut T> {
self.backend.as_any_mut().downcast_mut::<T>()
}
fn handle_server_announce(&mut self, req: VersionAndIdPdu) -> PduResult<Vec<SvcMessage>> {
let client_announce_reply = RdpdrPdu::VersionAndIdPdu(VersionAndIdPdu::new_client_announce_reply(req)?);
trace!("sending {:?}", client_announce_reply);
@ -104,28 +138,43 @@ impl Rdpdr {
pdu: DeviceIoRequest,
payload: &mut ReadCursor<'_>,
) -> PduResult<Vec<SvcMessage>> {
if self.is_for_smartcard(&pdu) {
let req = DeviceControlRequest::<ScardIoCtlCode>::decode(pdu, payload)?;
let call = ScardCall::decode(req.io_control_code, payload)?;
match self.device_list.for_device_type(pdu.device_id)? {
DeviceType::Smartcard => {
let req = DeviceControlRequest::<ScardIoCtlCode>::decode(pdu, payload)?;
let call = ScardCall::decode(req.io_control_code, payload)?;
debug!(?req);
debug!(?req.io_control_code, ?call);
debug!(?req);
debug!(?req.io_control_code, ?call);
self.backend.handle_scard_call(req, call)?;
self.backend.handle_scard_call(req, call)?;
Ok(Vec::new())
} else {
Err(other_err!("Rdpdr", "received unexpected packet"))
Ok(Vec::new())
}
DeviceType::Filesystem => {
debug!("received filesystem packet");
match pdu.major_function {
MajorFunction::Create => todo!(),
MajorFunction::Close => todo!(),
MajorFunction::Read => todo!(),
MajorFunction::Write => todo!(),
MajorFunction::DeviceControl => todo!(),
MajorFunction::QueryVolumeInformation => todo!(),
MajorFunction::SetVolumeInformation => todo!(),
MajorFunction::QueryInformation => todo!(),
MajorFunction::SetInformation => todo!(),
MajorFunction::DirectoryControl => todo!(),
MajorFunction::LockControl => todo!(),
}
}
_ => {
// This should never happen, as we only announce devices that we support.
warn!(?pdu, "received packet for unsupported device type");
Ok(Vec::new())
}
}
}
fn is_for_smartcard(&self, pdu: &DeviceIoRequest) -> bool {
self.device_list.is_smartcard(pdu.device_id)
}
}
impl_as_any!(Rdpdr);
impl StaticVirtualChannelProcessor for Rdpdr {
fn channel_name(&self) -> ChannelName {
Self::NAME

View file

@ -273,13 +273,17 @@ impl Capabilities {
this
}
pub fn clone_inner(&mut self) -> Vec<CapabilityMessage> {
self.0.clone()
}
pub fn add_smartcard(&mut self) {
self.push(CapabilityMessage::new_smartcard());
self.increment_special_devices();
}
pub fn clone_inner(&mut self) -> Vec<CapabilityMessage> {
self.0.clone()
pub fn add_drive(&mut self) {
self.push(CapabilityMessage::new_drive());
}
fn add_general(&mut self, special_type_device_cap: u32) {
@ -721,6 +725,13 @@ pub struct ClientDeviceListAnnounce {
impl ClientDeviceListAnnounce {
const FIXED_PART_SIZE: usize = size_of::<u32>(); // DeviceCount
/// Library users should not typically call this directly, use [`Rdpdr::add_drive`] instead.
pub(crate) fn new_drive(device_id: u32, name: String) -> Self {
Self {
device_list: vec![DeviceAnnounceHeader::new_drive(device_id, name)],
}
}
pub fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> {
dst.write_u32(cast_length!(
"ClientDeviceListAnnounce",
@ -756,11 +767,21 @@ impl Devices {
self.push(DeviceAnnounceHeader::new_smartcard(device_id));
}
pub fn is_smartcard(&self, device_id: u32) -> bool {
// TODO: consider caching here
self.0
.iter()
.any(|d| d.device_id == device_id && d.device_type == DeviceType::Smartcard)
pub fn add_drive(&mut self, device_id: u32, name: String) {
self.push(DeviceAnnounceHeader::new_drive(device_id, name));
}
/// Returns the [`DeviceType`] for the given device ID.
pub fn for_device_type(&self, device_id: u32) -> PduResult<DeviceType> {
if let Some(device_type) = self.0.iter().find(|d| d.device_id == device_id).map(|d| d.device_type) {
Ok(device_type)
} else {
Err(invalid_message_err!(
"Devices::for_device_type",
"device_id",
"no device with that ID"
))
}
}
fn push(&mut self, device: DeviceAnnounceHeader) {
@ -802,6 +823,25 @@ impl DeviceAnnounceHeader {
}
}
fn new_drive(device_id: u32, name: String) -> Self {
// The spec says Unicode but empirically this wants null terminated UTF-8.
let mut device_data = name.into_bytes();
device_data.push(0u8);
Self {
device_type: DeviceType::Filesystem,
device_id,
// "The drive name MUST be specified in the PreferredDosName field; however, if the drive name is larger than the allocated size of the PreferredDosName field,
// then the drive name MUST be truncated to fit. If the client supports DRIVE_CAPABILITY_VERSION_02 in the Drive Capability Set, then the full name MUST also
// be specified in the DeviceData field, as a null-terminated Unicode string. If the DeviceDataLength field is nonzero, the content of the PreferredDosName field
// is ignored."
//
// Since we do support DRIVE_CAPABILITY_VERSION_02, we'll put the full name in the DeviceData field.
preferred_dos_name: PreferredDosName("ignored".to_owned()),
device_data,
}
}
fn encode(&self, dst: &mut WriteCursor<'_>) -> PduResult<()> {
dst.write_u32(self.device_type.into());
dst.write_u32(self.device_id);

View file

@ -321,7 +321,7 @@ pub fn make_channel_definition(channel: &StaticVirtualChannel) -> ChannelDef {
}
/// Type information ([`TypeId`]) may be retrieved at runtime for this type.
pub trait AsAny {
pub trait AsAny: 'static {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;