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(test)]
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::Kyber,
160 SigAlgorithm::None
161 )]
162 #[case(
163 EncryptionAlgorithm::KyberHybrid,
164 KemAlgorithm::Kyber,
165 SigAlgorithm::Dilithium65
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().build(client_kernel).unwrap();
252
253 let result = citadel_io::tokio::select! {
254 res0 = client => res0.map(|_| ()),
255 res1 = server => res1.map(|_| ())
256 };
257
258 result.unwrap();
259
260 assert!(client_success.load(Ordering::Relaxed));
261 }
262
263 #[rstest]
264 #[case(
265 EncryptionAlgorithm::AES_GCM_256,
266 KemAlgorithm::Kyber,
267 SigAlgorithm::None
268 )]
269 #[timeout(std::time::Duration::from_secs(90))]
270 #[citadel_io::tokio::test]
271 async fn test_c2s_file_transfer_revfs_take(
272 #[case] enx: EncryptionAlgorithm,
273 #[case] kem: KemAlgorithm,
274 #[case] sig: SigAlgorithm,
275 #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
276 ) {
277 citadel_logging::setup_log();
278 let client_success = &AtomicBool::new(false);
279 let (server, server_addr) = server_info::<StackedRatchet>();
280 let uuid = Uuid::new_v4();
281
282 let source_dir = PathBuf::from("../resources/TheBridge.pdf");
283
284 let session_security_settings = SessionSecuritySettingsBuilder::default()
285 .with_crypto_params(enx + kem + sig)
286 .with_security_level(security_level)
287 .build()
288 .unwrap();
289
290 let server_connection_settings =
291 DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
292 .disable_udp()
293 .with_session_security_settings(session_security_settings)
294 .build()
295 .unwrap();
296
297 let client_kernel = SingleClientServerConnectionKernel::new(
298 server_connection_settings,
299 |connection| async move {
300 log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
301 let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
302 crate::fs::write_with_security_level(
304 &connection.remote,
305 source_dir.clone(),
306 security_level,
307 &virtual_path,
308 )
309 .await?;
310 log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
311 let save_dir = crate::fs::take(&connection.remote, &virtual_path).await?;
313 log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
315 let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
316 let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
317 assert_eq!(original_bytes, revfs_pulled_bytes);
318 log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
319 assert!(crate::fs::read(&connection.remote, &virtual_path)
321 .await
322 .is_err());
323 client_success.store(true, Ordering::Relaxed);
324 connection.shutdown_kernel().await
325 },
326 );
327
328 let client = DefaultNodeBuilder::default().build(client_kernel).unwrap();
329
330 let result = citadel_io::tokio::select! {
331 res0 = client => res0.map(|_| ()),
332 res1 = server => res1.map(|_| ())
333 };
334
335 result.unwrap();
336
337 assert!(client_success.load(Ordering::Relaxed));
338 }
339
340 #[rstest]
341 #[case(
342 EncryptionAlgorithm::AES_GCM_256,
343 KemAlgorithm::Kyber,
344 SigAlgorithm::None
345 )]
346 #[timeout(std::time::Duration::from_secs(90))]
347 #[citadel_io::tokio::test]
348 async fn test_c2s_file_transfer_revfs_delete(
349 #[case] enx: EncryptionAlgorithm,
350 #[case] kem: KemAlgorithm,
351 #[case] sig: SigAlgorithm,
352 #[values(SecurityLevel::Standard)] security_level: SecurityLevel,
353 ) {
354 citadel_logging::setup_log();
355 let client_success = &AtomicBool::new(false);
356 let (server, server_addr) = server_info::<StackedRatchet>();
357 let uuid = Uuid::new_v4();
358
359 let source_dir = PathBuf::from("../resources/TheBridge.pdf");
360
361 let session_security_settings = SessionSecuritySettingsBuilder::default()
362 .with_crypto_params(enx + kem + sig)
363 .with_security_level(security_level)
364 .build()
365 .unwrap();
366
367 let server_connection_settings =
368 DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid)
369 .disable_udp()
370 .with_session_security_settings(session_security_settings)
371 .build()
372 .unwrap();
373
374 let client_kernel = SingleClientServerConnectionKernel::new(
375 server_connection_settings,
376 |connection| async move {
377 log::trace!(target: "citadel", "***CLIENT LOGIN SUCCESS :: File transfer next ***");
378 let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
379 crate::fs::write_with_security_level(
381 &connection.remote,
382 source_dir.clone(),
383 security_level,
384 &virtual_path,
385 )
386 .await?;
387 log::trace!(target: "citadel", "***CLIENT FILE TRANSFER SUCCESS***");
388 let save_dir = crate::fs::read(&connection.remote, &virtual_path).await?;
390 log::trace!(target: "citadel", "***CLIENT REVFS PULL SUCCESS");
392 let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
393 let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
394 assert_eq!(original_bytes, revfs_pulled_bytes);
395 log::trace!(target: "citadel", "***CLIENT REVFS PULL COMPARE SUCCESS");
396 crate::fs::delete(&connection.remote, &virtual_path).await?;
397 assert!(crate::fs::read(&connection.remote, &virtual_path)
399 .await
400 .is_err());
401 client_success.store(true, Ordering::Relaxed);
402 connection.shutdown_kernel().await
403 },
404 );
405
406 let client = DefaultNodeBuilder::default().build(client_kernel).unwrap();
407
408 let result = citadel_io::tokio::select! {
409 res0 = client => res0.map(|_| ()),
410 res1 = server => res1.map(|_| ())
411 };
412
413 result.unwrap();
414
415 assert!(client_success.load(Ordering::Relaxed));
416 }
417
418 #[rstest]
419 #[case(SecrecyMode::BestEffort)]
420 #[timeout(Duration::from_secs(60))]
421 #[cfg(feature = "localhost-testing")]
422 #[citadel_io::tokio::test(flavor = "multi_thread")]
423 async fn test_p2p_file_transfer_revfs(
424 #[case] secrecy_mode: SecrecyMode,
425 #[values(KemAlgorithm::Kyber)] kem: KemAlgorithm,
426 #[values(EncryptionAlgorithm::AES_GCM_256)] enx: EncryptionAlgorithm,
427 ) {
428 citadel_logging::setup_log();
429 crate::test_common::TestBarrier::setup(2);
430 let client0_success = &AtomicBool::new(false);
431 let client1_success = &AtomicBool::new(false);
432
433 let (server, server_addr) = crate::test_common::server_info::<StackedRatchet>();
434
435 let uuid0 = Uuid::new_v4();
436 let uuid1 = Uuid::new_v4();
437 let session_security = SessionSecuritySettingsBuilder::default()
438 .with_secrecy_mode(secrecy_mode)
439 .with_crypto_params(kem + enx)
440 .build()
441 .unwrap();
442
443 let security_level = SecurityLevel::Standard;
444
445 let source_dir = &PathBuf::from("../resources/TheBridge.pdf");
446
447 let server_connection_settings =
448 DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid0)
449 .disable_udp()
450 .with_session_security_settings(session_security)
451 .build()
452 .unwrap();
453
454 let peer_conn_0 = PeerConnectionSetupAggregator::default()
455 .with_peer_custom(uuid1)
456 .ensure_registered()
457 .with_session_security_settings(session_security)
458 .enable_udp()
459 .add();
460
461 let client_kernel0 = PeerConnectionKernel::new(
463 server_connection_settings,
464 peer_conn_0,
465 move |mut connection, remote_outer| async move {
466 wait_for_peers().await;
467 let mut connection = connection.recv().await.unwrap()?;
468 let cid = connection.channel.get_session_cid();
469 wait_for_peers().await;
470 log::info!(target: "citadel", "***CLIENT A {cid} LOGIN SUCCESS :: File transfer next ***");
472 let remote = connection.remote.clone();
473 let handle_orig = connection.incoming_object_transfer_handles.take().unwrap();
474 let _file_transfer_task = accept_all(handle_orig);
475
476 let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
477 crate::fs::write_with_security_level(
479 &remote,
480 source_dir.clone(),
481 security_level,
482 &virtual_path,
483 )
484 .await?;
485 log::info!(target: "citadel", "***CLIENT A {cid} FILE TRANSFER SUCCESS***");
486 tokio::time::sleep(Duration::from_secs(1)).await;
487 wait_for_peers().await;
488 let save_dir = crate::fs::read(&remote, virtual_path).await?;
490 log::info!(target: "citadel", "***CLIENT A {cid} REVFS PULL SUCCESS");
492 let original_bytes = tokio::fs::read(&source_dir).await.unwrap();
493 let revfs_pulled_bytes = tokio::fs::read(&save_dir).await.unwrap();
494 assert_eq!(original_bytes, revfs_pulled_bytes);
495 log::info!(target: "citadel", "***CLIENT A {cid} REVFS PULL COMPARE SUCCESS");
496 wait_for_peers().await;
497 client0_success.store(true, Ordering::Relaxed);
498 remote_outer.shutdown_kernel().await
499 },
500 );
501
502 let server_connection_settings =
503 DefaultServerConnectionSettingsBuilder::transient_with_id(server_addr, uuid1)
504 .disable_udp()
505 .with_session_security_settings(session_security)
506 .build()
507 .unwrap();
508
509 let peer_conn_1 = PeerConnectionSetupAggregator::default()
510 .with_peer_custom(uuid0)
511 .ensure_registered()
512 .with_session_security_settings(session_security)
513 .enable_udp()
514 .add();
515
516 let client_kernel1 = PeerConnectionKernel::new(
517 server_connection_settings,
518 peer_conn_1,
519 move |mut connection, remote_outer| async move {
520 wait_for_peers().await;
521 let mut connection = connection.recv().await.unwrap()?;
522 let cid = connection.channel.get_session_cid();
523 wait_for_peers().await;
524 let remote = connection.remote.clone();
525 let handle_orig = connection.incoming_object_transfer_handles.take().unwrap();
526 let _file_transfer_task = accept_all(handle_orig);
527 log::info!(target: "citadel", "***CLIENT B {cid} LOGIN SUCCESS :: File transfer next ***");
528 let virtual_path = PathBuf::from("/home/john.doe/TheBridge.pdf");
529 crate::fs::write_with_security_level(
531 &remote,
532 source_dir.clone(),
533 security_level,
534 &virtual_path,
535 )
536 .await?;
537 log::info!(target: "citadel", "***CLIENT B {cid} FILE TRANSFER SUCCESS***");
538 tokio::time::sleep(Duration::from_secs(1)).await;
540 wait_for_peers().await;
541 let save_dir = crate::fs::read(&remote, virtual_path).await?;
543 log::info!(target: "citadel", "***CLIENT B {cid} REVFS PULL SUCCESS");
545 let original_bytes = citadel_io::tokio::fs::read(&source_dir).await.unwrap();
546 let revfs_pulled_bytes = citadel_io::tokio::fs::read(&save_dir).await.unwrap();
547 assert_eq!(original_bytes, revfs_pulled_bytes);
548 log::info!(target: "citadel", "***CLIENT B {cid} REVFS PULL COMPARE SUCCESS");
549 wait_for_peers().await;
550 client1_success.store(true, Ordering::Relaxed);
551 remote_outer.shutdown_kernel().await
552 },
553 );
554
555 let client0 = DefaultNodeBuilder::default().build(client_kernel0).unwrap();
556 let client1 = DefaultNodeBuilder::default().build(client_kernel1).unwrap();
557 let clients = futures::future::try_join(client0, client1);
558
559 let task = async move {
560 citadel_io::tokio::select! {
561 server_res = server => Err(NetworkError::msg(format!("Server ended prematurely: {:?}", server_res.map(|_| ())))),
562 client_res = clients => client_res.map(|_| ())
563 }
564 };
565
566 let _ = citadel_io::tokio::time::timeout(Duration::from_secs(120), task)
567 .await
568 .unwrap();
569
570 assert!(client0_success.load(Ordering::Relaxed));
571 assert!(client1_success.load(Ordering::Relaxed));
572 }
573
574 #[allow(dead_code)]
575 fn accept_all(mut rx: FileTransferHandleRx) -> citadel_io::tokio::task::JoinHandle<()> {
576 citadel_io::tokio::task::spawn(async move {
577 while let Some(mut handle) = rx.recv().await {
578 if let Err(err) = handle.accept() {
579 log::error!(target: "citadel", "Failed to accept file transfer: {err:?}");
580 continue;
581 }
582
583 exhaust_file_transfer_async(handle).await;
585 }
586 })
587 }
588
589 #[allow(dead_code)]
590 async fn exhaust_file_transfer_async(mut handle: ObjectTransferHandler) {
591 while let Some(evt) = handle.next().await {
592 log::info!(target: "citadel", "File Transfer Event: {evt:?}");
593 if let ObjectTransferStatus::Fail(err) = &evt {
594 log::error!(target: "citadel", "File Transfer Failed: {err:?}");
595 break;
596 } else if let ObjectTransferStatus::TransferComplete = &evt {
597 break;
598 } else if let ObjectTransferStatus::ReceptionComplete = &evt {
599 break;
600 }
601 }
602 }
603}