Skip to content

Commit

Permalink
Adds a test to confirm that infinite timeout IOCTL_GETSTATUSCHANGEW. …
Browse files Browse the repository at this point in the history
…getting SCARD_IOCTL_CANCEL-ed works properly.

Also Updates RUST_VERSION to latest, and updates IronRDP hash to the latest with
some updates required for the test to work.
  • Loading branch information
ibeckermayer committed Feb 29, 2024
1 parent 7a76aea commit 0b21d3f
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 26 deletions.
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 10 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ codegen-units = 1

[workspace.dependencies]
# Note: To use a local IronRDP repository as a crate (for example, ironrdp-cliprdr), define the dependency as follows:
# ironrdp-cliprdr = { path = "/path/to/local/IronRDP/crates/ironrdp-cliprdr" }
ironrdp-cliprdr = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08" }
ironrdp-connector = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08" }
ironrdp-graphics = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08" }
ironrdp-pdu = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08" }
ironrdp-rdpdr = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08" }
ironrdp-rdpsnd = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08" }
ironrdp-session = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08" }
ironrdp-svc = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08" }
ironrdp-tls = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08", features = ["rustls"]}
ironrdp-tokio = { git = "https://github.com/Devolutions/IronRDP", rev = "c944674ecbb0952f9e7ecb964a6c79cdca669a08" }
ironrdp-cliprdr = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376" }
ironrdp-connector = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376" }
ironrdp-graphics = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376" }
ironrdp-pdu = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376" }
ironrdp-rdpdr = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376" }
ironrdp-rdpsnd = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376" }
ironrdp-session = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376" }
ironrdp-svc = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376" }
ironrdp-tls = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376", features = ["rustls"]}
ironrdp-tokio = { git = "https://github.com/Devolutions/IronRDP", rev = "86b8e1429fd5c951cac6e983c8b7504140aca376" }
2 changes: 1 addition & 1 deletion build.assets/versions.mk
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ GOLANGCI_LINT_VERSION ?= v1.56.2
NODE_VERSION ?= 18.19.1

# Run lint-rust check locally before merging code after you bump this.
RUST_VERSION ?= 1.71.1
RUST_VERSION ?= 1.76.0
WASM_PACK_VERSION ?= 0.12.1
LIBBPF_VERSION ?= 1.2.2
LIBPCSCLITE_VERSION ?= 1.9.9-teleport
Expand Down
9 changes: 7 additions & 2 deletions lib/srv/desktop/rdp/rdpclient/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Duration;
use tokio::io::{split, ReadHalf, WriteHalf};
use tokio::net::TcpStream as TokioTcpStream;
use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::{channel, error::SendError, Receiver, Sender};
use tokio::task::JoinError;
// Export this for crate level use.
Expand Down Expand Up @@ -831,7 +832,7 @@ impl Drop for Client {
///
/// This enum is used by [`ClientHandle`]'s methods to dispatch function calls to the corresponding [`Client`] instance.
#[derive(Debug)]
enum ClientFunction {
pub enum ClientFunction {
/// Corresponds to [`Client::write_rdp_pointer`]
WriteRdpPointer(CGOMousePointerEvent),
/// Corresponds to [`Client::write_rdp_key`]
Expand Down Expand Up @@ -875,7 +876,7 @@ pub struct ClientHandle(Sender<ClientFunction>);

impl ClientHandle {
/// Creates a new `ClientHandle` and corresponding [`FunctionReceiver`] with a buffer of size `buffer`.
fn new(buffer: usize) -> (Self, FunctionReceiver) {
pub fn new(buffer: usize) -> (Self, FunctionReceiver) {
let (sender, receiver) = channel(buffer);
(Self(sender), FunctionReceiver(receiver))
}
Expand Down Expand Up @@ -1092,6 +1093,10 @@ impl FunctionReceiver {
async fn recv(&mut self) -> Option<ClientFunction> {
self.0.recv().await
}

pub fn try_recv(&mut self) -> Result<ClientFunction, TryRecvError> {
self.0.try_recv()
}
}

type RdpReadStream = Framed<TokioStream<ReadHalf<TlsStream<TokioTcpStream>>>>;
Expand Down
210 changes: 210 additions & 0 deletions lib/srv/desktop/rdp/rdpclient/src/rdpdr/scard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,13 @@ impl Contexts {
Ok(self.get_internal_mut(id)?.take_scard_cancel_response())
}

fn has_scard_cancel_response(&self, id: u32) -> bool {
self.contexts
.get(&id)
.map(|ctx| ctx.scard_cancel_response.is_some())
.unwrap_or(false)
}

fn get_card(&mut self, handle: &ScardHandle) -> PduResult<&mut piv::Card<TRANSMIT_DATA_LIMIT>> {
self.get_internal_mut(handle.context.value)?
.get(handle.value)
Expand Down Expand Up @@ -703,3 +710,206 @@ impl std::fmt::Display for SmartcardBackendError {
}

impl std::error::Error for SmartcardBackendError {}

#[cfg(test)]
mod tests {
use super::*;
use crate::client::{ClientFunction, FunctionReceiver};
use ironrdp_rdpdr::pdu::{
efs::{DeviceIoRequest, DeviceIoResponse, MajorFunction, MinorFunction},
esc::{EstablishContextCall, ReaderState, Scope},
};

#[tokio::test]
async fn test_establish_context() {
let (scard, fr) = create_scard_and_fr();
establish_context(scard, fr).await;
}

#[tokio::test]
async fn test_infinite_timeout_cancel() {
let (scard, fr) = create_scard_and_fr();
let (scard, mut fr, context_id) = establish_context(scard, fr).await;

// Simulate receiving a SCARD_IOCTL_GETSTATUSCHANGEW with an infinite timeout.
let req1 = create_device_control_request(1, ScardIoCtlCode::GetStatusChangeW);
let call1 = create_infinite_status_change_call(context_id);

let scard = scard_handle(
scard,
req1.clone(),
ScardCall::GetStatusChangeCall(call1.clone()),
)
.await;

// Nothing should have been sent back to the server, we should have a pending cancel response.
fr.try_recv().expect_err("expected error");
assert!(scard.contexts.has_scard_cancel_response(context_id));

// Simulate receiving a SCARD_IOCTL_CANCEL.
let req2 = create_device_control_request(2, ScardIoCtlCode::Cancel);
let call2 = ScardCall::ContextCall(create_context_call(context_id));
let scard = scard_handle(scard, req2.clone(), call2).await;

// The pending cancel response should have been sent back to the server,
// so it should no longer be pending.
assert!(!scard.contexts.has_scard_cancel_response(context_id));

// Check that we sent the no longer pending response for the original SCARD_IOCTL_GETSTATUSCHANGEW.
check_device_control_response_sent(
&mut fr,
DeviceControlResponse {
device_io_reply: DeviceIoResponse {
device_id: SCARD_DEVICE_ID,
completion_id: req1.header.completion_id,
io_status: NtStatus::SUCCESS,
},
output_buffer: Some(Box::new(GetStatusChangeReturn::new(
ReturnCode::Cancelled,
ScardBackend::create_get_status_change_return(call1)
.into_inner()
.reader_states,
))),
},
);

// Check that we also sent the response for the SCARD_IOCTL_CANCEL.
check_device_control_response_sent(
&mut fr,
DeviceControlResponse {
device_io_reply: DeviceIoResponse {
device_id: SCARD_DEVICE_ID,
completion_id: req2.header.completion_id,
io_status: NtStatus::SUCCESS,
},
output_buffer: Some(Box::new(LongReturn::new(ReturnCode::Success))),
},
);
}

fn create_scard_and_fr() -> (ScardBackend, FunctionReceiver) {
let (ch, fr) = ClientHandle::new(10);
let scard = ScardBackend::new(ch, vec![], vec![], "1234".to_string());
(scard, fr)
}

/// [`ScardBackend::handle`] ultimately calls a `blocking_send` which must be wrapped in a
/// `tokio::task::spawn_blocking` to avoid blocking the tokio runtime. This function is a
/// wrapper around that pattern.
async fn scard_handle(
mut scard: ScardBackend,
req: DeviceControlRequest<ScardIoCtlCode>,
call: ScardCall,
) -> ScardBackend {
tokio::task::spawn_blocking(|| {
scard.handle(req, call).unwrap();
scard
})
.await
.unwrap()
}

/// Establishes a context and returns the passed ScardBackend and the context ID.
async fn establish_context(
scard: ScardBackend,
mut fr: FunctionReceiver,
) -> (ScardBackend, FunctionReceiver, u32) {
let req = create_device_control_request(1, ScardIoCtlCode::EstablishContext);
let call = create_establish_context_call();
let expected_id = scard.contexts.next_id;
let scard = scard_handle(scard, req, call).await;
assert!(scard.contexts.exists(expected_id));
match fr.try_recv().unwrap() {
ClientFunction::WriteRdpdr(pdu) => match pdu {
ironrdp_rdpdr::pdu::RdpdrPdu::DeviceControlResponse(resp) => {
assert_eq!(resp.device_io_reply.device_id, SCARD_DEVICE_ID);
assert_eq!(resp.device_io_reply.completion_id, 1);
assert_eq!(resp.device_io_reply.io_status, NtStatus::SUCCESS);
}
_ => panic!("unexpected pdu"),
},
_ => panic!("unexpected function"),
}
(scard, fr, expected_id)
}

fn create_establish_context_call() -> ScardCall {
ScardCall::EstablishContextCall(EstablishContextCall {
scope: Scope::System,
})
}

fn create_context_call(context_id: u32) -> ContextCall {
ContextCall {
context: ScardContext { value: context_id },
}
}

fn create_device_control_request(
completion_id: u32,
io_control_code: ScardIoCtlCode,
) -> DeviceControlRequest<ScardIoCtlCode> {
DeviceControlRequest {
header: DeviceIoRequest {
device_id: SCARD_DEVICE_ID,
file_id: 1,
completion_id,
major_function: MajorFunction::DeviceControl,
minor_function: MinorFunction::from(0x00000000),
},
output_buffer_length: 2048,
input_buffer_length: 2048,
io_control_code,
}
}

fn create_infinite_status_change_call(context_id: u32) -> GetStatusChangeCall {
GetStatusChangeCall {
context: ScardContext { value: context_id },
timeout: TIMEOUT_INFINITE,
states_ptr_length: 2,
states_ptr: 131076,
states_length: 2,
states: vec![
ReaderState {
reader: "\\\\?PnP?\\Notification".to_string(),
common: ReaderStateCommonCall {
current_state: CardStateFlags::empty(),
event_state: CardStateFlags::empty(),
atr_length: 0,
atr: [0; 36],
},
},
ReaderState {
reader: TELEPORT_READER_NAME.to_string(),
common: ReaderStateCommonCall {
current_state: CardStateFlags::SCARD_STATE_CHANGED
| CardStateFlags::SCARD_STATE_PRESENT,
event_state: CardStateFlags::empty(),
atr_length: 11,
atr: [
59, 149, 19, 129, 1, 128, 115, 255, 1, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
},
},
],
}
}

/// Checks whether the `expected` DeviceControlResponse was sent to the `fr`.
fn check_device_control_response_sent(
fr: &mut FunctionReceiver,
expected: DeviceControlResponse,
) {
match fr.try_recv().expect("expected function") {
ClientFunction::WriteRdpdr(pdu) => match pdu {
ironrdp_rdpdr::pdu::RdpdrPdu::DeviceControlResponse(resp) => {
assert_eq!(resp, expected);
}
_ => panic!("unexpected pdu"),
},
_ => panic!("unexpected function"),
}
}
}

0 comments on commit 0b21d3f

Please sign in to comment.