diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a48593c9e..189587161 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -209,8 +209,6 @@ This is to keep iteration time short. Contains all integration tests for code living in the extra tier, in a single binary, organized in modules. -(WIP: this crate does not exist yet.) - #### [`crates/ironrdp-fuzzing`](./crates/ironrdp-fuzzing) Provides test case generators and oracles for use with fuzzing. diff --git a/Cargo.lock b/Cargo.lock index 29119a8e0..d173fb463 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2721,6 +2721,22 @@ dependencies = [ "rstest", ] +[[package]] +name = "ironrdp-testsuite-extra" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "ironrdp", + "ironrdp-async", + "ironrdp-tls", + "ironrdp-tokio", + "semver", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "ironrdp-tls" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 050564427..b06800108 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ ironrdp-session-generators = { path = "crates/ironrdp-session-generators" } ironrdp-session = { version = "0.2", path = "crates/ironrdp-session" } ironrdp-svc = { version = "0.1", path = "crates/ironrdp-svc" } ironrdp-testsuite-core = { path = "crates/ironrdp-testsuite-core" } +ironrdp-testsuite-extra = { path = "crates/ironrdp-testsuite-extra" } ironrdp-tls = { version = "0.1", path = "crates/ironrdp-tls" } ironrdp-tokio = { version = "0.2", path = "crates/ironrdp-tokio" } ironrdp = { version = "0.7", path = "crates/ironrdp" } diff --git a/crates/ironrdp-acceptor/src/connection.rs b/crates/ironrdp-acceptor/src/connection.rs index f4777e8e0..06177ceea 100644 --- a/crates/ironrdp-acceptor/src/connection.rs +++ b/crates/ironrdp-acceptor/src/connection.rs @@ -1,4 +1,3 @@ -use core::any::TypeId; use core::mem; use ironrdp_connector::{ @@ -34,6 +33,7 @@ pub struct Acceptor { static_channels: StaticChannelSet, saved_for_reactivation: AcceptorState, pub(crate) creds: Option, + reactivation: bool, } #[derive(Debug)] @@ -43,6 +43,7 @@ pub struct AcceptorResult { pub input_events: Vec>, pub user_channel_id: u16, pub io_channel_id: u16, + pub reactivation: bool, } impl Acceptor { @@ -62,10 +63,15 @@ impl Acceptor { static_channels: StaticChannelSet::new(), saved_for_reactivation: Default::default(), creds, + reactivation: false, } } - pub fn new_deactivation_reactivation(mut consumed: Acceptor, desktop_size: DesktopSize) -> Self { + pub fn new_deactivation_reactivation( + mut consumed: Acceptor, + static_channels: StaticChannelSet, + desktop_size: DesktopSize, + ) -> Self { let AcceptorState::CapabilitiesSendServer { early_capability, channels, @@ -95,9 +101,10 @@ impl Acceptor { io_channel_id: consumed.io_channel_id, desktop_size, server_capabilities: consumed.server_capabilities, - static_channels: StaticChannelSet::new(), + static_channels, saved_for_reactivation, creds: consumed.creds, + reactivation: true, } } @@ -105,18 +112,7 @@ impl Acceptor { where T: SvcServerProcessor + 'static, { - let channel_name = channel.channel_name(); - self.static_channels.insert(channel); - - // Restore channel id if it was already attached. - if let AcceptorState::CapabilitiesSendServer { channels, .. } = &self.state { - for (channel_id, c) in channels { - if c.name == channel_name { - self.static_channels.attach_channel_id(TypeId::of::(), *channel_id); - } - } - } } pub fn reached_security_upgrade(&self) -> Option { @@ -155,6 +151,7 @@ impl Acceptor { input_events, user_channel_id: self.user_channel_id, io_channel_id: self.io_channel_id, + reactivation: self.reactivation, }), previous_state => { self.state = previous_state; @@ -291,7 +288,9 @@ impl Sequence for Acceptor { } fn step(&mut self, input: &[u8], output: &mut WriteBuf) -> ConnectorResult { - let (written, next_state) = match mem::take(&mut self.state) { + let prev_state = mem::take(&mut self.state); + + let (written, next_state) = match prev_state { AcceptorState::InitiationWaitRequest => { let connection_request = decode::>(input) .map_err(ConnectorError::decode) @@ -315,6 +314,8 @@ impl Sequence for Acceptor { SecurityProtocol::HYBRID } else if protocols.intersects(SecurityProtocol::SSL) { SecurityProtocol::SSL + } else if self.security.is_empty() { + SecurityProtocol::empty() } else { return Err(ConnectorError::general("failed to negotiate security protocol")); }; @@ -639,15 +640,38 @@ impl Sequence for Acceptor { ) } - AcceptorState::CapabilitiesWaitConfirm { channels } => { + AcceptorState::CapabilitiesWaitConfirm { ref channels } => { let message = decode::>>(input) .map_err(ConnectorError::decode) - .map(|p| p.0)?; - + .map(|p| p.0); + let message = match message { + Ok(msg) => msg, + Err(e) => { + if self.reactivation { + debug!("Dropping unexpected PDU during reactivation"); + self.state = prev_state; + return Ok(Written::Nothing); + } else { + return Err(e); + } + } + }; match message { mcs::McsMessage::SendDataRequest(data) => { let capabilities_confirm = decode::(data.user_data.as_ref()) - .map_err(ConnectorError::decode)?; + .map_err(ConnectorError::decode); + let capabilities_confirm = match capabilities_confirm { + Ok(capabilities_confirm) => capabilities_confirm, + Err(e) => { + if self.reactivation { + debug!("Dropping unexpected PDU during reactivation"); + self.state = prev_state; + return Ok(Written::Nothing); + } else { + return Err(e); + } + } + }; debug!(message = ?capabilities_confirm, "Received"); @@ -659,7 +683,7 @@ impl Sequence for Acceptor { ( Written::Nothing, AcceptorState::ConnectionFinalization { - channels, + channels: channels.clone(), finalization: FinalizationSequence::new(self.user_channel_id, self.io_channel_id), client_capabilities: confirm.pdu.capability_sets, }, @@ -673,7 +697,7 @@ impl Sequence for Acceptor { _ => { warn!(?message, "Unexpected MCS message received"); - (Written::Nothing, AcceptorState::CapabilitiesWaitConfirm { channels }) + (Written::Nothing, prev_state) } } } @@ -684,6 +708,7 @@ impl Sequence for Acceptor { client_capabilities, } => { let written = finalization.step(input, output)?; + let state = if finalization.is_done() { AcceptorState::Accepted { channels, diff --git a/crates/ironrdp-acceptor/src/lib.rs b/crates/ironrdp-acceptor/src/lib.rs index a8091768c..e42566f3c 100644 --- a/crates/ironrdp-acceptor/src/lib.rs +++ b/crates/ironrdp-acceptor/src/lib.rs @@ -4,7 +4,6 @@ #[macro_use] extern crate tracing; -use ironrdp_async::bytes::Bytes; use ironrdp_async::{single_sequence_step, Framed, FramedRead, FramedWrite, StreamWrapper}; use ironrdp_connector::credssp::KerberosConfig; use ironrdp_connector::sspi::credssp::EarlyUserAuthResult; @@ -50,7 +49,7 @@ where return Ok(result); } - single_sequence_step(&mut framed, acceptor, &mut buf, None).await?; + single_sequence_step(&mut framed, acceptor, &mut buf).await?; } } @@ -84,7 +83,6 @@ where pub async fn accept_finalize( mut framed: Framed, acceptor: &mut Acceptor, - mut unmatched: Option<&mut Vec>, ) -> ConnectorResult<(Framed, AcceptorResult)> where S: FramedRead + FramedWrite, @@ -95,7 +93,7 @@ where if let Some(result) = acceptor.get_result() { return Ok((framed, result)); } - single_sequence_step(&mut framed, acceptor, &mut buf, unmatched.as_deref_mut()).await?; + single_sequence_step(&mut framed, acceptor, &mut buf).await?; } } @@ -152,7 +150,7 @@ where ); let pdu = framed - .read_by_hint(next_pdu_hint, None) + .read_by_hint(next_pdu_hint) .await .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; diff --git a/crates/ironrdp-async/src/connector.rs b/crates/ironrdp-async/src/connector.rs index 23a300f4c..4d1b085dc 100644 --- a/crates/ironrdp-async/src/connector.rs +++ b/crates/ironrdp-async/src/connector.rs @@ -23,7 +23,7 @@ where info!("Begin connection procedure"); while !connector.should_perform_security_upgrade() { - single_sequence_step(framed, connector, &mut buf, None).await?; + single_sequence_step(framed, connector, &mut buf).await?; } Ok(ShouldUpgrade) @@ -73,7 +73,7 @@ where } let result = loop { - single_sequence_step(framed, &mut connector, &mut buf, None).await?; + single_sequence_step(framed, &mut connector, &mut buf).await?; if let ClientConnectorState::Connected { result } = connector.state { break result; @@ -171,7 +171,7 @@ where ); let pdu = framed - .read_by_hint(next_pdu_hint, None) + .read_by_hint(next_pdu_hint) .await .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; diff --git a/crates/ironrdp-async/src/framed.rs b/crates/ironrdp-async/src/framed.rs index 370532c92..f628338b7 100644 --- a/crates/ironrdp-async/src/framed.rs +++ b/crates/ironrdp-async/src/framed.rs @@ -165,11 +165,7 @@ where /// `tokio::select!` statement and some other branch /// completes first, then it is safe to drop the future and re-create it later. /// Data may have been read, but it will be stored in the internal buffer. - pub async fn read_by_hint( - &mut self, - hint: &dyn PduHint, - mut unmatched: Option<&mut Vec>, - ) -> io::Result { + pub async fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result { loop { match hint .find_size(self.peek()) @@ -179,10 +175,8 @@ where let bytes = self.read_exact(length).await?.freeze(); if matched { return Ok(bytes); - } else if let Some(ref mut unmatched) = unmatched { - unmatched.push(bytes); } else { - warn!("Received and lost an unexpected PDU"); + debug!("Received and lost an unexpected PDU"); } } None => { @@ -236,13 +230,12 @@ pub async fn single_sequence_step( framed: &mut Framed, sequence: &mut dyn Sequence, buf: &mut WriteBuf, - unmatched: Option<&mut Vec>, ) -> ConnectorResult<()> where S: FramedWrite + FramedRead, { buf.clear(); - let written = single_sequence_step_read(framed, sequence, buf, unmatched).await?; + let written = single_sequence_step_read(framed, sequence, buf).await?; single_sequence_step_write(framed, buf, written).await } @@ -250,7 +243,6 @@ pub async fn single_sequence_step_read( framed: &mut Framed, sequence: &mut dyn Sequence, buf: &mut WriteBuf, - unmatched: Option<&mut Vec>, ) -> ConnectorResult where S: FramedRead, @@ -265,7 +257,7 @@ where ); let pdu = framed - .read_by_hint(next_pdu_hint, unmatched) + .read_by_hint(next_pdu_hint) .await .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; diff --git a/crates/ironrdp-blocking/src/connector.rs b/crates/ironrdp-blocking/src/connector.rs index f98915c9d..765560e6e 100644 --- a/crates/ironrdp-blocking/src/connector.rs +++ b/crates/ironrdp-blocking/src/connector.rs @@ -1,6 +1,5 @@ use std::io::{Read, Write}; -use bytes::Bytes; use ironrdp_connector::credssp::{CredsspProcessGenerator, CredsspSequence, KerberosConfig}; use ironrdp_connector::sspi::credssp::ClientState; use ironrdp_connector::sspi::generator::GeneratorState; @@ -26,7 +25,7 @@ where info!("Begin connection procedure"); while !connector.should_perform_security_upgrade() { - single_sequence_step(framed, connector, &mut buf, None)?; + single_sequence_step(framed, connector, &mut buf)?; } Ok(ShouldUpgrade) @@ -79,7 +78,7 @@ where debug!("Remaining of connection sequence"); let result = loop { - single_sequence_step(framed, &mut connector, &mut buf, None)?; + single_sequence_step(framed, &mut connector, &mut buf)?; if let ClientConnectorState::Connected { result } = connector.state { break result; @@ -168,7 +167,7 @@ where ); let pdu = framed - .read_by_hint(next_pdu_hint, None) + .read_by_hint(next_pdu_hint) .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; trace!(length = pdu.len(), "PDU received"); @@ -189,7 +188,6 @@ pub fn single_sequence_step( framed: &mut Framed, connector: &mut ClientConnector, buf: &mut WriteBuf, - unmatched: Option<&mut Vec>, ) -> ConnectorResult<()> where S: Read + Write, @@ -204,7 +202,7 @@ where ); let pdu = framed - .read_by_hint(next_pdu_hint, unmatched) + .read_by_hint(next_pdu_hint) .map_err(|e| ironrdp_connector::custom_err!("read frame by hint", e))?; trace!(length = pdu.len(), "PDU received"); diff --git a/crates/ironrdp-blocking/src/framed.rs b/crates/ironrdp-blocking/src/framed.rs index bcc2242d1..e12483a92 100644 --- a/crates/ironrdp-blocking/src/framed.rs +++ b/crates/ironrdp-blocking/src/framed.rs @@ -87,7 +87,7 @@ where } /// Reads a frame using the provided PduHint. - pub fn read_by_hint(&mut self, hint: &dyn PduHint, mut unmatched: Option<&mut Vec>) -> io::Result { + pub fn read_by_hint(&mut self, hint: &dyn PduHint) -> io::Result { loop { match hint .find_size(self.peek()) @@ -97,10 +97,8 @@ where let bytes = self.read_exact(length)?.freeze(); if matched { return Ok(bytes); - } else if let Some(ref mut unmatched) = unmatched { - unmatched.push(bytes); } else { - warn!("Received and lost an unexpected PDU"); + debug!("Received and lost an unexpected PDU"); } } None => { diff --git a/crates/ironrdp-client/src/rdp.rs b/crates/ironrdp-client/src/rdp.rs index 6c5cf28c3..fedd012eb 100644 --- a/crates/ironrdp-client/src/rdp.rs +++ b/crates/ironrdp-client/src/rdp.rs @@ -295,10 +295,9 @@ async fn active_session( debug!("Received Server Deactivate All PDU, executing Deactivation-Reactivation Sequence"); let mut buf = WriteBuf::new(); 'activation_seq: loop { - let written = - single_sequence_step_read(&mut reader, &mut *connection_activation, &mut buf, None) - .await - .map_err(|e| session::custom_err!("read deactivation-reactivation sequence step", e))?; + let written = single_sequence_step_read(&mut reader, &mut *connection_activation, &mut buf) + .await + .map_err(|e| session::custom_err!("read deactivation-reactivation sequence step", e))?; if written.size().is_some() { writer.write_all(buf.filled()).await.map_err(|e| { diff --git a/crates/ironrdp-dvc/src/pdu.rs b/crates/ironrdp-dvc/src/pdu.rs index 097a6675c..fdb0e944a 100644 --- a/crates/ironrdp-dvc/src/pdu.rs +++ b/crates/ironrdp-dvc/src/pdu.rs @@ -538,6 +538,7 @@ pub struct CreationStatus(u32); impl CreationStatus { pub const OK: Self = Self(0x00000000); + pub const NOT_FOUND: Self = Self(0xC0000225); pub const NO_LISTENER: Self = Self(0xC0000001); fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> { diff --git a/crates/ironrdp-dvc/src/server.rs b/crates/ironrdp-dvc/src/server.rs index e16918faf..cfe63883f 100644 --- a/crates/ironrdp-dvc/src/server.rs +++ b/crates/ironrdp-dvc/src/server.rs @@ -163,6 +163,7 @@ impl SvcProcessor for DrdynvcServer { let channel_id = data.channel_id(); let c = self.channel_by_id(channel_id).map_err(|e| decode_err!(e))?; if c.state != ChannelState::Opened { + debug!(?channel_id, ?c.state, "Invalid channel state"); return Err(pdu_other_err!("invalid channel state")); } if let Some(complete) = c.complete_data.process_data(data).map_err(|e| decode_err!(e))? { diff --git a/crates/ironrdp-server/src/capabilities.rs b/crates/ironrdp-server/src/capabilities.rs index 089a0321a..0e2df9345 100644 --- a/crates/ironrdp-server/src/capabilities.rs +++ b/crates/ironrdp-server/src/capabilities.rs @@ -28,7 +28,9 @@ fn bitmap_capabilities(size: &DesktopSize) -> capability_sets::Bitmap { pref_bits_per_pix: 32, desktop_width: size.width, desktop_height: size.height, - desktop_resize_flag: false, + // This makes freerdp keep the flag up and handle desktop resize/deactivation-reactivation. + // Likely okay to advertize if the server doesn't resize anyway. + desktop_resize_flag: true, drawing_flags: capability_sets::BitmapDrawingFlags::empty(), } } diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index c6c5ec647..69f72fb41 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -436,20 +436,7 @@ impl RdpServer { DisplayUpdate::PointerPosition(pos) => encoder.pointer_position(pos), DisplayUpdate::Resize(desktop_size) => { debug!(?desktop_size, "Display resize"); - let pdu = ShareControlPdu::ServerDeactivateAll(ServerDeactivateAll); - let pdu = rdp::headers::ShareControlHeader { - share_id: 0, - pdu_source: io_channel_id, - share_control_pdu: pdu, - }; - let user_data = encode_vec(&pdu)?.into(); - let pdu = SendDataIndication { - initiator_id: user_channel_id, - channel_id: io_channel_id, - user_data, - }; - let msg = encode_vec(&X224(pdu))?; - writer.write_all(&msg).await?; + deactivate_all(io_channel_id, user_channel_id, writer).await?; return Ok((RunState::DeactivationReactivation { desktop_size }, encoder)); } DisplayUpdate::RGBAPointer(ptr) => encoder.rgba_pointer(ptr), @@ -670,14 +657,16 @@ impl RdpServer { } self.static_channels = result.static_channels; - for (_type_id, channel, channel_id) in self.static_channels.iter_mut() { - debug!(?channel, ?channel_id, "Start"); - let Some(channel_id) = channel_id else { - continue; - }; - let svc_responses = channel.start()?; - let response = server_encode_svc_messages(svc_responses, channel_id, result.user_channel_id)?; - writer.write_all(&response).await?; + if !result.reactivation { + for (_type_id, channel, channel_id) in self.static_channels.iter_mut() { + debug!(?channel, ?channel_id, "Start"); + let Some(channel_id) = channel_id else { + continue; + }; + let svc_responses = channel.start()?; + let response = server_encode_svc_messages(svc_responses, channel_id, result.user_channel_id)?; + writer.write_all(&response).await?; + } } let mut rfxcodec = None; @@ -690,6 +679,28 @@ impl RdpServer { bail!("Fastpath output not supported!"); } } + CapabilitySet::Bitmap(b) => { + if !b.desktop_resize_flag { + debug!("Desktop resize is not supported by the client"); + continue; + } + + let client_size = DesktopSize { + width: b.desktop_width, + height: b.desktop_height, + }; + let display_size = self.display.lock().await.size().await; + + // It's problematic when the client didn't resize, as we send bitmap updates that don't fit. + // The client will likely drop the connection. + if client_size.width < display_size.width || client_size.height < display_size.height { + // TODO: we may have different behaviour instead, such as clipping or scaling? + warn!( + "Client size doesn't fit the server size: {:?} < {:?}", + client_size, display_size + ); + } + } CapabilitySet::SurfaceCommands(c) => { surface_flags = c.flags; } @@ -901,32 +912,27 @@ impl RdpServer { where S: AsyncRead + AsyncWrite + Sync + Send + Unpin, { - let mut other_pdus = None; - loop { - let (new_framed, result) = ironrdp_acceptor::accept_finalize(framed, &mut acceptor, other_pdus.as_mut()) + let (new_framed, result) = ironrdp_acceptor::accept_finalize(framed, &mut acceptor) .await .context("failed to accept client during finalize")?; - let (stream, mut leftover) = new_framed.into_inner(); - - if let Some(pdus) = other_pdus.take() { - let unmatched_frames = pdus.into_iter().flatten(); - let previous_leftover = leftover.split(); - leftover.extend(unmatched_frames); - leftover.extend_from_slice(&previous_leftover); - } - - let (mut reader, mut writer) = split_tokio_framed(TokioFramed::new_with_leftover(stream, leftover)); + let (mut reader, mut writer) = split_tokio_framed(new_framed); match self.client_accepted(&mut reader, &mut writer, result).await? { RunState::Continue => { unreachable!(); } RunState::DeactivationReactivation { desktop_size } => { - other_pdus = Some(Vec::new()); - acceptor = Acceptor::new_deactivation_reactivation(acceptor, desktop_size); - self.attach_channels(&mut acceptor); + // No description of such behavior was found in the + // specification, but apparently, we must keep the channel + // state as they were during reactivation. This fixes + // various state issues during client resize. + acceptor = Acceptor::new_deactivation_reactivation( + acceptor, + core::mem::take(&mut self.static_channels), + desktop_size, + ); framed = unsplit_tokio_framed(reader, writer); continue; } @@ -943,6 +949,28 @@ impl RdpServer { } } +async fn deactivate_all( + io_channel_id: u16, + user_channel_id: u16, + writer: &mut impl FramedWrite, +) -> Result<(), anyhow::Error> { + let pdu = ShareControlPdu::ServerDeactivateAll(ServerDeactivateAll); + let pdu = rdp::headers::ShareControlHeader { + share_id: 0, + pdu_source: io_channel_id, + share_control_pdu: pdu, + }; + let user_data = encode_vec(&pdu)?.into(); + let pdu = SendDataIndication { + initiator_id: user_channel_id, + channel_id: io_channel_id, + user_data, + }; + let msg = encode_vec(&X224(pdu))?; + writer.write_all(&msg).await?; + Ok(()) +} + struct SharedWriter<'w, W: FramedWrite> { writer: Rc>, } diff --git a/crates/ironrdp-testsuite-extra/Cargo.toml b/crates/ironrdp-testsuite-extra/Cargo.toml new file mode 100644 index 000000000..a1db3af88 --- /dev/null +++ b/crates/ironrdp-testsuite-extra/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ironrdp-testsuite-extra" +version = "0.1.0" +edition.workspace = true +description = "IronRDP extra test suite" +publish = false +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[dev-dependencies] +anyhow = "1.0" +async-trait = "0.1" +ironrdp = { workspace = true, features = ["server", "pdu", "connector", "session", "connector"] } +ironrdp-async.workspace = true +ironrdp-tokio.workspace = true +ironrdp-tls = { workspace = true, features = ["rustls"] } +semver = "1.0" +tracing.workspace = true +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tokio = { version = "1", features = ["sync", "time"] } + +[lints] +workspace = true diff --git a/crates/ironrdp-testsuite-extra/src/lib.rs b/crates/ironrdp-testsuite-extra/src/lib.rs new file mode 100644 index 000000000..d04a355d2 --- /dev/null +++ b/crates/ironrdp-testsuite-extra/src/lib.rs @@ -0,0 +1 @@ +#![allow(unused_crate_dependencies)] diff --git a/crates/ironrdp-testsuite-extra/tests/certs/Makefile b/crates/ironrdp-testsuite-extra/tests/certs/Makefile new file mode 100644 index 000000000..94bdc6e6d --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/certs/Makefile @@ -0,0 +1,17 @@ +CERT_KEY=server-key.pem +CERT_FILE=server-cert.pem +DAYS=365 +RSA_BITS=2048 +SUBJECT=/C=US/ST=Test/L=Test/O=Test/OU=Test/CN=localhost + +.PHONY: all clean certs + +all: $(CERT_KEY) $(CERT_FILE) + +$(CERT_KEY) $(CERT_FILE): + openssl req -x509 -nodes -days $(DAYS) -newkey rsa:$(RSA_BITS) \ + -keyout $(CERT_KEY) -out $(CERT_FILE) \ + -subj "$(SUBJECT)" + +clean: + rm -f $(CERT_KEY) $(CERT_FILE) diff --git a/crates/ironrdp-testsuite-extra/tests/certs/README.md b/crates/ironrdp-testsuite-extra/tests/certs/README.md new file mode 100644 index 000000000..7d0de119d --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/certs/README.md @@ -0,0 +1,3 @@ +The server-cert.pem and server-key.pem provided in this repository are +self-signed and for testing purposes only. They should not be used in production +environments. diff --git a/crates/ironrdp-testsuite-extra/tests/certs/server-cert.pem b/crates/ironrdp-testsuite-extra/tests/certs/server-cert.pem new file mode 100644 index 000000000..c49f234e9 --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/certs/server-cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDmzCCAoOgAwIBAgIULZzx65W6IGBs2QEidYv0gEmDNP0wDQYJKoZIhvcNAQEL +BQAwXTELMAkGA1UEBhMCVVMxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3Qx +DTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxEjAQBgNVBAMMCWxvY2FsaG9z +dDAeFw0yNTAxMjEwODA1MjFaFw0yNjAxMjEwODA1MjFaMF0xCzAJBgNVBAYTAlVT +MQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MQ0wCwYDVQQKDARUZXN0MQ0w +CwYDVQQLDARUZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQDGzEhx7mcSqPmCjcYFNqmCi6JZijXsmm28fU3yQaQm +ez3/sZ3SxKucCARDBlS74YHjbcDsFq+w58cOs4XT+beKVsDSxhF1Ac+RpkXGtg2V +n/BjnN73aOUNs+GSmupA5GL6kRThKlzkV56M/5Nl0MUVwsurJ9kxLxUa7724NyYX +InJRzQENQDt9Z/QkiJC+C2G7O8W/LNTCUtqvH/BEKMzvBHxkaNGyUtfpHXu2BL43 +y2G366nIAiJ1JLhBCV7cnvoMrCpzwZbfh8pc2fRKurXY2BWuqBwZHkHM+ajE+3hj +y/0Flsa8GQ1xC7zo7MVkw9sXE40wJ8Gc9Ur61jpl8925AgMBAAGjUzBRMB0GA1Ud +DgQWBBQ9sSyrLM0nc/jBpH4A/mN2sdU2mTAfBgNVHSMEGDAWgBQ9sSyrLM0nc/jB +pH4A/mN2sdU2mTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAn +VjBEnjNR/GMcCF+JYw6EBc3sYjDZjjIjdvvbSXnF8rIdR+hxC+HI7A/6I3p+CWU1 +T3jZ9f76hqj4BjFAdqI1swypRM16qU21+4NPrA3raGZj9PRmpk1pH4fva0NaHCOA +l9tl2wSHF/wzV12Juh8hv5HTHjik2p6Ym9qKS9nkCp41CvRHVgylpQjGRDNR78n1 +yCBrQhdjIZxdFLVbIx3tIvQU1AS4igbTwTOTPuniZ1/QDRRiWS8hXM8KyhduXLB5 +0LdiupVChR9M4D7XxlMU3DQogGSxt/X9Kf4BckwztVXSGzUcTVOUizqpKIh3Gkho +DkD+onTOFXzLszn1UuzH +-----END CERTIFICATE----- diff --git a/crates/ironrdp-testsuite-extra/tests/certs/server-key.pem b/crates/ironrdp-testsuite-extra/tests/certs/server-key.pem new file mode 100644 index 000000000..05005f9f9 --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/certs/server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDGzEhx7mcSqPmC +jcYFNqmCi6JZijXsmm28fU3yQaQmez3/sZ3SxKucCARDBlS74YHjbcDsFq+w58cO +s4XT+beKVsDSxhF1Ac+RpkXGtg2Vn/BjnN73aOUNs+GSmupA5GL6kRThKlzkV56M +/5Nl0MUVwsurJ9kxLxUa7724NyYXInJRzQENQDt9Z/QkiJC+C2G7O8W/LNTCUtqv +H/BEKMzvBHxkaNGyUtfpHXu2BL43y2G366nIAiJ1JLhBCV7cnvoMrCpzwZbfh8pc +2fRKurXY2BWuqBwZHkHM+ajE+3hjy/0Flsa8GQ1xC7zo7MVkw9sXE40wJ8Gc9Ur6 +1jpl8925AgMBAAECggEAT7ZhBCISfWZ46dL8SGHjNV/VIO8s8Sr4/oAGDbIpZm67 +bPgk7vsCTsXeI5v5xP5G7VE4btIn75j4ddohOt6iLFvd5IYcQN0RhHb1+phMOSdR +JjgkJXOPiN+MfxMUBCIv2AXtp92rMro5bpMaYNSF+lRKA16ulayp20uvOJsQcGyg +EDhhaLB+pEwEDGrHLxP1O+DY1Ukc13sWObNwVEjW7qtTf+cach9DqliWU9nkyO6X +SZ8mSX5ki0zVJPCtBw4z95m869Alzr/vKk91zvkdKJM1PZTppon2nRvq2AIvgPOa +NAbY9zSgBoVHILmzl6vlpkHHLV+D1JyPlOdT9xh8swKBgQDm7SUpqOw0ZDQFK6ql +5P3I2YglbUR+/MCNPUANRtoYOkEzwBXyhtx1/0C1Ieh63H9W/5wlnswSYWv+ZMnY +ZULzxZH+jtmyspJFKnSaoCDH9lAa1+50kgJI5/B1jNpRq2jZLP4Kl5bPhb9Mu29D +bPbRqXLJtr+JL41bq+CKKdJ/bwKBgQDcYhgVzpVBcHQsJX2q2HoabgRXBDZbHT5Z +uzbRP1rwgY6w5D5aJKub4E2gwHF1b6JfIXCda4qXkaywyH+WcgdJBBEfAViqh4z1 +HPoxsknPtTLwyEvNthQJOyD684mD3D2uaEmpKQIyvm4ESYTjjYhVocqKvIu2c3Or ++nIRdDbhVwKBgQCPM+aE1CVORAliX3bek4exwvxDwWPln9XEgIQ094gN2CpQ7kBt ++qXCYrz81n81mYE6MR7i0XvZtiJjSptFH16Kjy1+/5UO1OASFkbjEIPjnOKGEvvj +vBvAnFyoeOV2GebWLqmHZgP2wwkji2RvGqZg1ETDxBk4+I0fmRGQfGj17wKBgQDV +P1oc58vHCYBwI0rpYRUts90hMiNCoRZvD1eovAxMAqFHC2RGJ4uihjW3Yd+nigDs +2le1C5WMuloGqcvDkMz52ySSAuSABi/gEk0Kf4EqqiQDl1y6TgAvOnbcPYGIBTnu +JF16gQLuhRPBtD4RTido7OgmvPDX9/kqpWlw+CoOewKBgQCbVMk+exyz1e4BrxB8 +vcZzVfOfwiDn8GnCoD32aL/JbO5ize6PIVm+zTsIuzv3hLDHoIs2/CxX7O3gI8eL +Hxn6JF9mwOE4uWcCTVYXZIwpqvrsTu0EUhRrCqLdA05pbBxIfODhb45RlB5vSn7G +Lt3YRdYjbU/R2YL3e8HMaUSwyA== +-----END PRIVATE KEY----- diff --git a/crates/ironrdp-testsuite-extra/tests/tests.rs b/crates/ironrdp-testsuite-extra/tests/tests.rs new file mode 100644 index 000000000..d041b6f64 --- /dev/null +++ b/crates/ironrdp-testsuite-extra/tests/tests.rs @@ -0,0 +1,304 @@ +#![allow(unused_crate_dependencies)] // false positives because there is both a library and a binary + +use core::future::Future; +use std::path::Path; +use std::sync::Arc; + +use anyhow::Result; +use ironrdp::connector; +use ironrdp::pdu::rdp::capability_sets::MajorPlatformType; +use ironrdp::pdu::{self, gcc}; +use ironrdp::server::{ + self, DesktopSize, DisplayUpdate, KeyboardEvent, MouseEvent, PixelFormat, RdpServer, RdpServerDisplay, + RdpServerDisplayUpdates, RdpServerInputHandler, ServerEvent, TlsIdentityCtx, +}; +use ironrdp::session::image::DecodedImage; +use ironrdp::session::{self, ActiveStage, ActiveStageOutput}; +use ironrdp_async::{Framed, FramedWrite}; +use ironrdp_testsuite_extra as _; +use ironrdp_tls::TlsStream; +use ironrdp_tokio::TokioStream; +use tokio::net::TcpStream; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use tokio::sync::{oneshot, Mutex}; +use tracing::debug; + +const DESKTOP_WIDTH: u16 = 1024; +const DESKTOP_HEIGHT: u16 = 768; +const USERNAME: &str = ""; +const PASSWORD: &str = ""; + +#[tokio::test] +async fn test_client_server() { + client_server(default_client_config(), |stage, framed, _display_tx| async { + (stage, framed) + }) + .await +} + +#[tokio::test] +async fn test_deactivation_reactivation() { + let client_config = default_client_config(); + let mut image = DecodedImage::new( + PixelFormat::RgbA32, + client_config.desktop_size.width, + client_config.desktop_size.height, + ); + client_server(client_config, |mut stage, mut framed, display_tx| async move { + display_tx + .send(DisplayUpdate::Resize(DesktopSize { + width: 2048, + height: 2048, + })) + .unwrap(); + { + let (action, payload) = framed.read_pdu().await.expect("valid PDU"); + let outputs = stage.process(&mut image, action, &payload).expect("stage process"); + let out = outputs.into_iter().next().unwrap(); + match out { + ActiveStageOutput::DeactivateAll(mut connection_activation) => { + // TODO: factor this out in common client code + // Execute the Deactivation-Reactivation Sequence: + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432 + debug!("Received Server Deactivate All PDU, executing Deactivation-Reactivation Sequence"); + let mut buf = pdu::WriteBuf::new(); + 'activation_seq: loop { + let written = ironrdp_async::single_sequence_step_read( + &mut framed, + &mut *connection_activation, + &mut buf, + ) + .await + .map_err(|e| session::custom_err!("read deactivation-reactivation sequence step", e)) + .unwrap(); + + if written.size().is_some() { + framed + .write_all(buf.filled()) + .await + .map_err(|e| session::custom_err!("write deactivation-reactivation sequence step", e)) + .unwrap(); + } + + if let connector::connection_activation::ConnectionActivationState::Finalized { + io_channel_id, + user_channel_id, + desktop_size, + no_server_pointer, + pointer_software_rendering, + } = connection_activation.state + { + debug!(?desktop_size, "Deactivation-Reactivation Sequence completed"); + // Update image size with the new desktop size. + // image = DecodedImage::new(PixelFormat::RgbA32, desktop_size.width, desktop_size.height); + // Update the active stage with the new channel IDs and pointer settings. + stage.set_fastpath_processor( + session::fast_path::ProcessorBuilder { + io_channel_id, + user_channel_id, + no_server_pointer, + pointer_software_rendering, + } + .build(), + ); + stage.set_no_server_pointer(no_server_pointer); + break 'activation_seq; + } + } + } + _ => unreachable!(), + } + } + (stage, framed) + }) + .await +} + +type DisplayUpdatesRx = Arc>>; + +struct TestDisplayUpdates { + rx: DisplayUpdatesRx, +} + +#[async_trait::async_trait] +impl RdpServerDisplayUpdates for TestDisplayUpdates { + async fn next_update(&mut self) -> Option { + let mut rx = self.rx.lock().await; + + rx.recv().await + } +} + +struct TestDisplay { + rx: DisplayUpdatesRx, +} + +#[async_trait::async_trait] +impl RdpServerDisplay for TestDisplay { + async fn size(&mut self) -> DesktopSize { + DesktopSize { + width: DESKTOP_WIDTH, + height: DESKTOP_HEIGHT, + } + } + + async fn updates(&mut self) -> Result> { + Ok(Box::new(TestDisplayUpdates { + rx: Arc::clone(&self.rx), + })) + } +} + +struct TestInputHandler; +impl RdpServerInputHandler for TestInputHandler { + fn keyboard(&mut self, _: KeyboardEvent) {} + fn mouse(&mut self, _: MouseEvent) {} +} + +async fn client_server(client_config: connector::Config, clientfn: F) +where + F: FnOnce(ActiveStage, Framed>>, UnboundedSender) -> Fut + 'static, + Fut: Future>>)>, +{ + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init(); + + let cert_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/certs/server-cert.pem"); + let key_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/certs/server-key.pem"); + let identity = TlsIdentityCtx::init_from_paths(&cert_path, &key_path).expect("failed to init TLS identity"); + let acceptor = identity.make_acceptor().expect("failed to build TLS acceptor"); + + let (display_tx, display_rx) = mpsc::unbounded_channel(); + let mut server = RdpServer::builder() + .with_addr(([127, 0, 0, 1], 0)) + .with_tls(acceptor) + .with_input_handler(TestInputHandler) + .with_display_handler(TestDisplay { + rx: Arc::new(Mutex::new(display_rx)), + }) + .build(); + server.set_credentials(Some(server::Credentials { + username: USERNAME.into(), + password: PASSWORD.into(), + domain: None, + })); + let ev = server.event_sender().clone(); + + let local = tokio::task::LocalSet::new(); + local + .run_until(async move { + let server = tokio::task::spawn_local(async move { + server.run().await.unwrap(); + }); + + let client = tokio::task::spawn_local(async move { + let (tx, rx) = oneshot::channel(); + ev.send(ServerEvent::GetLocalAddr(tx)).unwrap(); + let addr = rx.await.unwrap().unwrap(); + let tcp_stream = TcpStream::connect(addr).await.expect("TCP connect"); + let mut framed = ironrdp_tokio::TokioFramed::new(tcp_stream); + let mut connector = connector::ClientConnector::new(client_config).with_server_addr(addr); + let should_upgrade = ironrdp_async::connect_begin(&mut framed, &mut connector) + .await + .expect("begin connection"); + let initial_stream = framed.into_inner_no_leftover(); + let (upgraded_stream, server_public_key) = ironrdp_tls::upgrade(initial_stream, "localhost") + .await + .expect("TLS upgrade"); + let upgraded = ironrdp_tokio::mark_as_upgraded(should_upgrade, &mut connector); + let mut upgraded_framed = ironrdp_tokio::TokioFramed::new(upgraded_stream); + let connection_result = ironrdp_async::connect_finalize( + upgraded, + &mut upgraded_framed, + connector, + "localhost".into(), + server_public_key, + None, + None, + ) + .await + .expect("finalize connection"); + + let active_stage = ActiveStage::new(connection_result); + let (active_stage, mut upgraded_framed) = clientfn(active_stage, upgraded_framed, display_tx).await; + let outputs = active_stage.graceful_shutdown().expect("shutdown"); + for out in outputs { + match out { + ActiveStageOutput::ResponseFrame(frame) => { + upgraded_framed.write_all(&frame).await.expect("write frame"); + } + _ => unimplemented!(), + } + } + + // server should probably send TLS close_notify + while let Ok(pdu) = upgraded_framed.read_pdu().await { + debug!(?pdu); + } + ev.send(ServerEvent::Quit("bye".into())).unwrap(); + }); + + tokio::try_join!(server, client).expect("join"); + }) + .await; +} + +// Maybe implement Default for Config +fn default_client_config() -> connector::Config { + connector::Config { + desktop_size: DesktopSize { + width: DESKTOP_WIDTH, + height: DESKTOP_HEIGHT, + }, + desktop_scale_factor: 0, // Default to 0 per FreeRDP + enable_tls: true, + enable_credssp: true, + credentials: connector::Credentials::UsernamePassword { + username: USERNAME.into(), + password: PASSWORD.into(), + }, + domain: None, + client_build: semver::Version::parse(env!("CARGO_PKG_VERSION")) + .map(|version| version.major * 100 + version.minor * 10 + version.patch) + .unwrap_or(0) + .try_into() + .unwrap(), + client_name: "ironrdp".into(), + keyboard_type: gcc::KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_layout: 0, + keyboard_functional_keys_count: 12, + ime_file_name: "".into(), + bitmap: None, + dig_product_id: "".into(), + // NOTE: hardcode this value like in freerdp + // https://github.com/FreeRDP/FreeRDP/blob/4e24b966c86fdf494a782f0dfcfc43a057a2ea60/libfreerdp/core/settings.c#LL49C34-L49C70 + client_dir: "C:\\Windows\\System32\\mstscax.dll".into(), + #[cfg(windows)] + platform: MajorPlatformType::WINDOWS, + #[cfg(target_os = "macos")] + platform: MajorPlatformType::MACINTOSH, + #[cfg(target_os = "ios")] + platform: MajorPlatformType::IOS, + #[cfg(target_os = "linux")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "android")] + platform: MajorPlatformType::ANDROID, + #[cfg(target_os = "freebsd")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "dragonfly")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "openbsd")] + platform: MajorPlatformType::UNIX, + #[cfg(target_os = "netbsd")] + platform: MajorPlatformType::UNIX, + hardware_id: None, + request_data: None, + autologon: false, + license_cache: None, + no_server_pointer: true, + pointer_software_rendering: true, + performance_flags: Default::default(), + } +} diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index 2105ae67f..65251817d 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -653,7 +653,7 @@ impl Session { let mut buf = WriteBuf::new(); 'activation_seq: loop { let written = - single_sequence_step_read(&mut framed, &mut *box_connection_activation, &mut buf, None) + single_sequence_step_read(&mut framed, &mut *box_connection_activation, &mut buf) .await?; if written.size().is_some() { @@ -1018,7 +1018,7 @@ where // RDCleanPath response let rdcleanpath_res = framed - .read_by_hint(&RDCLEANPATH_HINT, None) + .read_by_hint(&RDCLEANPATH_HINT) .await .context("read RDCleanPath request")?; diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml index 9a47905d4..f0e27358a 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -74,7 +74,7 @@ required-features = ["session", "connector", "graphics"] [[example]] name = "server" doc-scrape-examples = true -required-features = ["cliprdr", "rdpsnd", "server"] +required-features = ["cliprdr", "connector", "rdpsnd", "server"] [lints] workspace = true diff --git a/crates/ironrdp/examples/server.rs b/crates/ironrdp/examples/server.rs index 6d46f0aa9..ee71d3bd4 100644 --- a/crates/ironrdp/examples/server.rs +++ b/crates/ironrdp/examples/server.rs @@ -6,8 +6,8 @@ #[macro_use] extern crate tracing; +use core::num::NonZeroU16; use std::net::SocketAddr; -use std::num::NonZeroU16; use std::path::PathBuf; use std::sync::{Arc, Mutex}; @@ -16,13 +16,12 @@ use ironrdp::cliprdr::backend::{CliprdrBackend, CliprdrBackendFactory}; use ironrdp::connector::DesktopSize; use ironrdp::rdpsnd::pdu::ClientAudioFormatPdu; use ironrdp::rdpsnd::server::{RdpsndServerHandler, RdpsndServerMessage}; -use ironrdp::server::tokio; use ironrdp::server::tokio::sync::mpsc::UnboundedSender; use ironrdp::server::tokio::time::{self, sleep, Duration}; use ironrdp::server::{ - BitmapUpdate, CliprdrServerFactory, Credentials, DisplayUpdate, KeyboardEvent, MouseEvent, PixelFormat, PixelOrder, - RdpServer, RdpServerDisplay, RdpServerDisplayUpdates, RdpServerInputHandler, ServerEvent, ServerEventSender, - SoundServerFactory, TlsIdentityCtx, + tokio, BitmapUpdate, CliprdrServerFactory, Credentials, DisplayUpdate, KeyboardEvent, MouseEvent, PixelFormat, + PixelOrder, RdpServer, RdpServerDisplay, RdpServerDisplayUpdates, RdpServerInputHandler, ServerEvent, + ServerEventSender, SoundServerFactory, TlsIdentityCtx, }; use ironrdp_cliprdr_native::StubCliprdrBackend; use rand::prelude::*; @@ -269,7 +268,7 @@ impl RdpsndServerHandler for SndHandler { fn start(&mut self, client_format: &ClientAudioFormatPdu) -> Option { async fn generate_sine_wave(sample_rate: u32, frequency: f32, duration_ms: u64) -> Vec { - use std::f32::consts::PI; + use core::f32::consts::PI; let total_samples = u64::from(sample_rate / 1000).checked_mul(duration_ms).unwrap(); let samples_per_wave_length = sample_rate as f32 / frequency;