From c26fab4a450d945f75bf6fcc710e40994b1b0bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Date: Mon, 20 Jan 2025 17:50:51 +0400 Subject: [PATCH] test(extra): add some client-server tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ARCHITECTURE.md | 2 - Cargo.lock | 16 + Cargo.toml | 1 + crates/ironrdp-testsuite-extra/Cargo.toml | 27 ++ crates/ironrdp-testsuite-extra/src/lib.rs | 1 + .../tests/certs/Makefile | 17 + .../tests/certs/README.md | 3 + .../tests/certs/server-cert.pem | 22 ++ .../tests/certs/server-key.pem | 28 ++ crates/ironrdp-testsuite-extra/tests/tests.rs | 304 ++++++++++++++++++ 10 files changed, 419 insertions(+), 2 deletions(-) create mode 100644 crates/ironrdp-testsuite-extra/Cargo.toml create mode 100644 crates/ironrdp-testsuite-extra/src/lib.rs create mode 100644 crates/ironrdp-testsuite-extra/tests/certs/Makefile create mode 100644 crates/ironrdp-testsuite-extra/tests/certs/README.md create mode 100644 crates/ironrdp-testsuite-extra/tests/certs/server-cert.pem create mode 100644 crates/ironrdp-testsuite-extra/tests/certs/server-key.pem create mode 100644 crates/ironrdp-testsuite-extra/tests/tests.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a48593c9..18958716 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 29119a8e..d173fb46 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 05056442..b0680010 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-testsuite-extra/Cargo.toml b/crates/ironrdp-testsuite-extra/Cargo.toml new file mode 100644 index 00000000..a1db3af8 --- /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 00000000..d04a355d --- /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 00000000..94bdc6e6 --- /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 00000000..7d0de119 --- /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 00000000..c49f234e --- /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 00000000..05005f9f --- /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 00000000..d041b6f6 --- /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(), + } +}