test(extra): add some client-server tests

This is just some basic tests, but hopefully it will grow to be more
friendly and cover more behaviours.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
This commit is contained in:
Marc-André Lureau
2025-01-27 07:35:36 -05:00
committed by Benoît Cortier
parent 82c7c2f5b0
commit c26fab4a45
10 changed files with 419 additions and 2 deletions
-2
View File
@@ -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.
Generated
+16
View File
@@ -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"
+1
View File
@@ -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" }
+27
View File
@@ -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
@@ -0,0 +1 @@
#![allow(unused_crate_dependencies)]
@@ -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)
@@ -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.
@@ -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-----
@@ -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-----
@@ -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<Mutex<UnboundedReceiver<DisplayUpdate>>>;
struct TestDisplayUpdates {
rx: DisplayUpdatesRx,
}
#[async_trait::async_trait]
impl RdpServerDisplayUpdates for TestDisplayUpdates {
async fn next_update(&mut self) -> Option<DisplayUpdate> {
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<Box<dyn RdpServerDisplayUpdates>> {
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<F, Fut>(client_config: connector::Config, clientfn: F)
where
F: FnOnce(ActiveStage, Framed<TokioStream<TlsStream<TcpStream>>>, UnboundedSender<DisplayUpdate>) -> Fut + 'static,
Fut: Future<Output = (ActiveStage, Framed<TokioStream<TlsStream<TcpStream>>>)>,
{
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(),
}
}