1use crate::prelude::{ObjectSource, ProtocolRemoteTargetExt, TargetLockedRemote};
53
54use citadel_proto::prelude::NetworkError;
55use citadel_proto::prelude::*;
56use citadel_types::crypto::SecurityLevel;
57use std::path::PathBuf;
58
59pub async fn write<T: ObjectSource, P: Into<PathBuf> + Send, R: Ratchet>(
61 remote: &impl TargetLockedRemote<R>,
62 source: T,
63 virtual_path: P,
64) -> Result<(), NetworkError> {
65 write_with_security_level(remote, source, Default::default(), virtual_path).await
66}
67
68pub async fn write_with_security_level<T: ObjectSource, P: Into<PathBuf> + Send, R: Ratchet>(
70 remote: &impl TargetLockedRemote<R>,
71 source: T,
72 security_level: SecurityLevel,
73 virtual_path: P,
74) -> Result<(), NetworkError> {
75 remote
76 .remote_encrypted_virtual_filesystem_push(source, virtual_path, security_level)
77 .await
78}
79
80pub async fn read<P: Into<PathBuf> + Send, R: Ratchet>(
82 remote: &impl TargetLockedRemote<R>,
83 virtual_path: P,
84) -> Result<PathBuf, NetworkError> {
85 read_with_security_level(remote, Default::default(), virtual_path).await
86}
87
88pub async fn read_with_security_level<P: Into<PathBuf> + Send, R: Ratchet>(
90 remote: &impl TargetLockedRemote<R>,
91 transfer_security_level: SecurityLevel,
92 virtual_path: P,
93) -> Result<PathBuf, NetworkError> {
94 remote
95 .remote_encrypted_virtual_filesystem_pull(virtual_path, transfer_security_level, false)
96 .await
97}
98
99pub async fn take<P: Into<PathBuf> + Send, R: Ratchet>(
101 remote: &impl TargetLockedRemote<R>,
102 virtual_path: P,
103) -> Result<PathBuf, NetworkError> {
104 remote
105 .remote_encrypted_virtual_filesystem_pull(virtual_path, Default::default(), true)
106 .await
107}
108
109pub async fn take_with_security_level<P: Into<PathBuf> + Send, R: Ratchet>(
111 remote: &impl TargetLockedRemote<R>,
112 transfer_security_level: SecurityLevel,
113 virtual_path: P,
114) -> Result<PathBuf, NetworkError> {
115 remote
116 .remote_encrypted_virtual_filesystem_pull(virtual_path, transfer_security_level, true)
117 .await
118}
119
120pub async fn delete<P: Into<PathBuf> + Send, R: Ratchet>(
122 remote: &impl TargetLockedRemote<R>,
123 virtual_path: P,
124) -> Result<(), NetworkError> {
125 remote
126 .remote_encrypted_virtual_filesystem_delete(virtual_path)
127 .await
128}
129
130#[cfg(all(test, not(target_family = "wasm")))]
131mod tests {
132 use crate::prefabs::client::single_connection::SingleClientServerConnectionKernel;
133 use crate::prefabs::server::accept_file_transfer_kernel::AcceptFileTransferKernel;
134
135 use crate::prefabs::client::peer_connection::FileTransferHandleRx;
136 #[cfg(feature = "localhost-testing")]
137 use crate::prefabs::client::peer_connection::PeerConnectionKernel;
138 use crate::prefabs::client::DefaultServerConnectionSettingsBuilder;
139 use crate::prelude::*;
140 #[cfg(feature = "localhost-testing")]
141 use crate::test_common::wait_for_peers;
142 use citadel_io::tokio;
143 use futures::StreamExt;
144 use rstest::rstest;
145 use std::net::SocketAddr;
146 use std::path::PathBuf;
147 use std::sync::atomic::{AtomicBool, Ordering};
148 use std::time::Duration;
149 use uuid::Uuid;
150
151 pub fn server_info<'a, R: Ratchet>() -> (NodeFuture<'a, AcceptFileTransferKernel<R>>, SocketAddr)
152 {
153 crate::test_common::server_test_node(AcceptFileTransferKernel::<R>::default(), |_| {})
154 }
155
156 #[rstest]
157 #[case(
158 EncryptionAlgorithm::AES_GCM_256,
159 KemAlgorithm::MlKem,
160 SigAlgorithm::None
161 )]
162 #[case(
163 EncryptionAlgorithm::MlKemHybrid,
164 KemAlgorithm::MlKem,
165 SigAlgorithm::MlDsa65
166 )]
167 #[timeout(Duration::from_secs(90))]
168 #[citadel_io::tokio::test]
169 async fn test_c2s_file_transfer_revfs(
170 #[case] enx: EncryptionAlgorithm,
171 #[case] kem: KemAlgorithm,
172 #[case] sig: SigAlgorithm,
173 #[values(SecurityLevel::Standard, SecurityLevel::Reinforced)] security_level: SecurityLevel,
174 ) {
175 citadel_logging::setup_log();
176 let client_success = &AtomicBool::new(false);
177 let (server, server_addr) = server_info::<StackedRatchet>();
178 let uuid = Uuid::new_v4();
179
180 let source_dir = PathBuf::from("../resources/TheBridge.pdf");
181
182 let session_security_settings = SessionSecuritySettingsBuilder::default()
183 .with_crypto_params(enx + kem + sig)
184 .with_security_level(security_level)
185 .build()
186 .unwrap();
187
188 let server_connection_settings =
189 DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
190 .disable_udp()
191 .with_session_security_settings(session_security_settings)
192 .build()
193 .unwrap();
194
195 let client_kernel = SingleClientServerConnectionKernel::new(
196 server_connection_settings,
197 |connection| async move {
198 log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
199
200 let sessions = connection.remote.list_sessions().await?;
202 log::info!(target: "citadel", "***CLIENT list_sessions() returned {} sessions***", sessions.sessions.len());
203 assert!(
204 !sessions.sessions.is_empty(),
205 "Should have at least one active session"
206 );
207 let our_session = sessions.sessions.iter().find(|s| s.cid == connection.cid);
209 assert!(our_session.is_some(), "Should find our session in the list");
210 let our_session = our_session.unwrap();
211 assert!(
212 !our_session.connections.is_empty(),
213 "Session should have at least one connection"
214 );
215 let c2s_conn = our_session
217 .connections
218 .iter()
219 .find(|c| c.peer_cid.is_none());
220 assert!(
221 c2s_conn.is_some(),
222 "Should have a C2S connection (peer_cid = None)"
223 );
224 let c2s_conn = c2s_conn.unwrap();
225 assert!(c2s_conn.connected, "C2S connection should be active");
226 log::info!(target: "citadel", "***CLIENT list_sessions() verification PASSED***");
227
228 let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
229 crate::fs::write_with_security_level(
231 &connection.remote,
232 source_dir.clone(),
233 security_level,
234 &virtual_path,
235 )
236 .await?;
237 log::info!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
238 let save_dir = crate::fs::read(&connection.remote, virtual_path).await?;
240 log::info!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
242 let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
243 let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
244 assert_eq!(original_bytes, revfs_pulled_bytes);
245 log::info!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
246 client_success.store(true, Ordering::Relaxed);
247 connection.shutdown_kernel().await
248 },
249 );
250
251 let client = DefaultNodeBuilder::default()
252 .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
253 .build(client_kernel)
254 .unwrap();
255
256 let result = citadel_io::tokio::select! {
257 res0 = client => res0.map(|_| ()),
258 res1 = server => res1.map(|_| ())
259 };
260
261 result.unwrap();
262
263 assert!(client_success.load(Ordering::Relaxed));
264 }
265
266 #[rstest]
267 #[case(
268 EncryptionAlgorithm::AES_GCM_256,
269 KemAlgorithm::MlKem,
270 SigAlgorithm::None
271 )]
272 #[timeout(std::time::Duration::from_secs(90))]
273 #[citadel_io::tokio::test]
274 async fn test_c2s_file_transfer_revfs_take(
275 #[case] enx: EncryptionAlgorithm,
276 #[case] kem: KemAlgorithm,
277 #[case] sig: SigAlgorithm,
278 #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
279 ) {
280 citadel_logging::setup_log();
281 let client_success = &AtomicBool::new(false);
282 let (server, server_addr) = server_info::<StackedRatchet>();
283 let uuid = Uuid::new_v4();
284
285 let source_dir = PathBuf::from("../resources/TheBridge.pdf");
286
287 let session_security_settings = SessionSecuritySettingsBuilder::default()
288 .with_crypto_params(enx + kem + sig)
289 .with_security_level(security_level)
290 .build()
291 .unwrap();
292
293 let server_connection_settings =
294 DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
295 .disable_udp()
296 .with_session_security_settings(session_security_settings)
297 .build()
298 .unwrap();
299
300 let client_kernel = SingleClientServerConnectionKernel::new(
301 server_connection_settings,
302 |connection| async move {
303 log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
304 let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
305 crate::fs::write_with_security_level(
307 &connection.remote,
308 source_dir.clone(),
309 security_level,
310 &virtual_path,
311 )
312 .await?;
313 log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
314 let save_dir = crate::fs::take(&connection.remote, &virtual_path).await?;
316 log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
318 let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
319 let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
320 assert_eq!(original_bytes, revfs_pulled_bytes);
321 log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
322 assert!(crate::fs::read(&connection.remote, &virtual_path)
324 .await
325 .is_err());
326 client_success.store(true, Ordering::Relaxed);
327 connection.shutdown_kernel().await
328 },
329 );
330
331 let client = DefaultNodeBuilder::default()
332 .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
333 .build(client_kernel)
334 .unwrap();
335
336 let result = citadel_io::tokio::select! {
337 res0 = client => res0.map(|_| ()),
338 res1 = server => res1.map(|_| ())
339 };
340
341 result.unwrap();
342
343 assert!(client_success.load(Ordering::Relaxed));
344 }
345
346 #[rstest]
347 #[case(
348 EncryptionAlgorithm::AES_GCM_256,
349 KemAlgorithm::MlKem,
350 SigAlgorithm::None
351 )]
352 #[timeout(std::time::Duration::from_secs(90))]
353 #[citadel_io::tokio::test]
354 async fn test_c2s_file_transfer_revfs_delete(
355 #[case] enx: EncryptionAlgorithm,
356 #[case] kem: KemAlgorithm,
357 #[case] sig: SigAlgorithm,
358 #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
359 ) {
360 citadel_logging::setup_log();
361 let client_success = &AtomicBool::new(false);
362 let (server, server_addr) = server_info::<StackedRatchet>();
363 let uuid = Uuid::new_v4();
364
365 let source_dir = PathBuf::from("../resources/TheBridge.pdf");
366
367 let session_security_settings = SessionSecuritySettingsBuilder::default()
368 .with_crypto_params(enx + kem + sig)
369 .with_security_level(security_level)
370 .build()
371 .unwrap();
372
373 let server_connection_settings =
374 DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
375 .disable_udp()
376 .with_session_security_settings(session_security_settings)
377 .build()
378 .unwrap();
379
380 let client_kernel = SingleClientServerConnectionKernel::new(
381 server_connection_settings,
382 |connection| async move {
383 log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
384 let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
385 crate::fs::write_with_security_level(
387 &connection.remote,
388 source_dir.clone(),
389 security_level,
390 &virtual_path,
391 )
392 .await?;
393 log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
394 let save_dir = crate::fs::read(&connection.remote, &virtual_path).await?;
396 log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
398 let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
399 let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
400 assert_eq!(original_bytes, revfs_pulled_bytes);
401 log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
402 crate::fs::delete(&connection.remote, &virtual_path).await?;
403 assert!(crate::fs::read(&connection.remote, &virtual_path)
405 .await
406 .is_err());
407 client_success.store(true, Ordering::Relaxed);
408 connection.shutdown_kernel().await
409 },
410 );
411
412 let client = DefaultNodeBuilder::default()
413 .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
414 .build(client_kernel)
415 .unwrap();
416
417 let result = citadel_io::tokio::select! {
418 res0 = client => res0.map(|_| ()),
419 res1 = server => res1.map(|_| ())
420 };
421
422 result.unwrap();
423
424 assert!(client_success.load(Ordering::Relaxed));
425 }
426
427 #[rstest]
428 #[case(SecrecyMode::BestEffort)]
429 #[timeout(Duration::from_secs(60))]
430 #[cfg(feature = "localhost-testing")]
431 #[citadel_io::tokio::test(flavor = "multi_thread")]
432 async fn test_p2p_file_transfer_revfs(
433 #[case] secrecy_mode: SecrecyMode,
434 #[values(KemAlgorithm::MlKem)] kem: KemAlgorithm,
435 #[values(EncryptionAlgorithm::AES_GCM_256)] enx: EncryptionAlgorithm,
436 ) {
437 citadel_logging::setup_log();
438 crate::test_common::TestBarrier::setup(2);
439 let client0_success = &AtomicBool::new(false);
440 let client1_success = &AtomicBool::new(false);
441
442 let (server, server_addr) = crate::test_common::server_info::<StackedRatchet>();
443
444 let uuid0 = Uuid::new_v4();
445 let uuid1 = Uuid::new_v4();
446 let session_security = SessionSecuritySettingsBuilder::default()
447 .with_secrecy_mode(secrecy_mode)
448 .with_crypto_params(kem + enx)
449 .build()
450 .unwrap();
451
452 let security_level = SecurityLevel::Standard;
453
454 let source_dir = &PathBuf::from("../resources/TheBridge.pdf");
455
456 let server_connection_settings =
457 DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid0)
458 .disable_udp()
459 .with_session_security_settings(session_security)
460 .build()
461 .unwrap();
462
463 let peer_conn_0 = PeerConnectionSetupAggregator::default()
464 .with_peer_custom(uuid1)
465 .ensure_registered()
466 .with_session_security_settings(session_security)
467 .enable_udp()
468 .add();
469
470 let client_kernel0 = PeerConnectionKernel::new(
472 server_connection_settings,
473 peer_conn_0,
474 move |mut connection, remote_outer| async move {
475 wait_for_peers().await;
476 let mut connection = connection.recv().await.unwrap()?;
477 let cid = connection.channel.get_session_cid();
478 wait_for_peers().await;
479 log::info!(target: "citadel", "***CLIENT A {cid} LOGIN SUCCESS :: File transfer next ***");
481 let remote = connection.remote.clone();
482 let handle_orig = connection.incoming_object_transfer_handles.take().unwrap();
483 let _file_transfer_task = accept_all(handle_orig);
484
485 let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
486 crate::fs::write_with_security_level(
488 &remote,
489 source_dir.clone(),
490 security_level,
491 &virtual_path,
492 )
493 .await?;
494 log::info!(target: "citadel", "***CLIENT A {cid} FILE TRANSFER SUCCESS***");
495 tokio::time::sleep(Duration::from_secs(1)).await;
496 wait_for_peers().await;
497 let save_dir = crate::fs::read(&remote, virtual_path).await?;
499 log::info!(target: "citadel", "***CLIENT A {cid} REVFS PULL SUCCESS");
501 let original_bytes = tokio::fs::read(&source_dir).await.unwrap();
502 let revfs_pulled_bytes = tokio::fs::read(&save_dir).await.unwrap();
503 assert_eq!(original_bytes, revfs_pulled_bytes);
504 log::info!(target: "citadel", "***CLIENT A {cid} REVFS PULL COMPARE SUCCESS");
505 wait_for_peers().await;
506 client0_success.store(true, Ordering::Relaxed);
507 remote_outer.shutdown_kernel().await
508 },
509 );
510
511 let server_connection_settings =
512 DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid1)
513 .disable_udp()
514 .with_session_security_settings(session_security)
515 .build()
516 .unwrap();
517
518 let peer_conn_1 = PeerConnectionSetupAggregator::default()
519 .with_peer_custom(uuid0)
520 .ensure_registered()
521 .with_session_security_settings(session_security)
522 .enable_udp()
523 .add();
524
525 let client_kernel1 = PeerConnectionKernel::new(
526 server_connection_settings,
527 peer_conn_1,
528 move |mut connection, remote_outer| async move {
529 wait_for_peers().await;
530 let mut connection = connection.recv().await.unwrap()?;
531 let cid = connection.channel.get_session_cid();
532 wait_for_peers().await;
533 let remote = connection.remote.clone();
534 let handle_orig = connection.incoming_object_transfer_handles.take().unwrap();
535 let _file_transfer_task = accept_all(handle_orig);
536 log::info!(target: "citadel", "***CLIENT B {cid} LOGIN SUCCESS :: File transfer next ***");
537 let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
538 crate::fs::write_with_security_level(
540 &remote,
541 source_dir.clone(),
542 security_level,
543 &virtual_path,
544 )
545 .await?;
546 log::info!(target: "citadel", "***CLIENT B {cid} FILE TRANSFER SUCCESS***");
547 tokio::time::sleep(Duration::from_secs(1)).await;
549 wait_for_peers().await;
550 let save_dir = crate::fs::read(&remote, virtual_path).await?;
552 log::info!(target: "citadel", "***CLIENT B {cid} REVFS PULL SUCCESS");
554 let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
555 let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
556 assert_eq!(original_bytes, revfs_pulled_bytes);
557 log::info!(target: "citadel", "***CLIENT B {cid} REVFS PULL COMPARE SUCCESS");
558 wait_for_peers().await;
559 client1_success.store(true, Ordering::Relaxed);
560 remote_outer.shutdown_kernel().await
561 },
562 );
563
564 let client0 = DefaultNodeBuilder::default()
565 .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
566 .build(client_kernel0)
567 .unwrap();
568 let client1 = DefaultNodeBuilder::default()
569 .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
570 .build(client_kernel1)
571 .unwrap();
572 let clients = futures::future::try_join(client0, client1);
573
574 let task = async move {
575 citadel_io::tokio::select! {
576 server_res = server => Err(citadel_io::error!(citadel_io::ErrorCode::PrefabServerEndedPrematurely, citadel_io::Dbg(server_res.map(|_| ())))),
577 client_res = clients => client_res.map(|_| ())
578 }
579 };
580
581 let _ = citadel_io::time::timeout(Duration::from_secs(120), task)
582 .await
583 .unwrap();
584
585 assert!(client0_success.load(Ordering::Relaxed));
586 assert!(client1_success.load(Ordering::Relaxed));
587 }
588
589 #[allow(dead_code)]
590 fn accept_all(mut rx: FileTransferHandleRx) -> citadel_io::tokio::task::JoinHandle<()> {
591 citadel_io::tokio::task::spawn(async move {
592 while let Some(mut handle) = rx.recv().await {
593 if let Err(err) = handle.accept() {
594 log::error!(target: "citadel", "Failed to accept file transfer: {err:?}");
595 continue;
596 }
597
598 exhaust_file_transfer_async(handle).await;
600 }
601 })
602 }
603
604 #[allow(dead_code)]
605 async fn exhaust_file_transfer_async(mut handle: ObjectTransferHandler) {
606 while let Some(evt) = handle.next().await {
607 log::info!(target: "citadel", "File Transfer Event: {evt:?}");
608 if let ObjectTransferStatus::Fail(err) = &evt {
609 log::error!(target: "citadel", "File Transfer Failed: {err:?}");
610 break;
611 } else if let ObjectTransferStatus::TransferComplete = &evt {
612 break;
613 } else if let ObjectTransferStatus::ReceptionComplete = &evt {
614 break;
615 }
616 }
617 }
618}