Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
a74924e
Branches
Tags
main
apple-crates
airdrop
src
receiver
tests.rs
Refactor and unify a lot of things for another project and implement airdrop support
Dadoum
commited
a74924e
at 2026-10-25 22:15:58
tests.rs
Blame
History
Raw
use std::net::{Ipv6Addr, SocketAddr, TcpListener}; use std::sync::{Arc, Mutex}; use std::time::Duration; use super::*; use crate::connect::Endpoint; use crate::identity::Identity; use crate::sender::{Client, Outgoing, Reply, Sender}; use crate::tls::{ClientAuth, server_config}; #[derive(Default)] struct Recorder { asks: Mutex<Vec<AskRequest>>, stored: Mutex<Vec<Stored>>, failed: Mutex<Vec<String>>, progress: Mutex<Vec<(u64, u64)>>, decline: bool, visibility: Visibility, } impl Handler for Recorder { fn visibility(&self) -> Visibility { self.visibility } fn name(&self) -> (String, String) { ("Test Receiver".into(), "Linux1,1".into()) } fn ask(&self, request: &AskRequest) -> Decision { self.asks.lock().unwrap().push(request.clone()); if self.decline { Decision::Decline } else { Decision::Accept } } fn progress(&self, _: &str, received: u64, total: u64) { self.progress.lock().unwrap().push((received, total)); } fn stored(&self, stored: &Stored) { self.stored.lock().unwrap().push(stored.clone()); } fn failed(&self, _: &str, reason: &str) { self.failed.lock().unwrap().push(reason.to_owned()); } } struct Rig { dest: tempfile::TempDir, recorder: Arc<Recorder>, sender: Sender, port: u16, } /// Generating an RSA key is slow in a debug build: do it once. fn identities() -> &'static (Identity, Identity) { static IDS: std::sync::OnceLock<(Identity, Identity)> = std::sync::OnceLock::new(); IDS.get_or_init(|| { ( Identity::self_signed("receiver").unwrap(), Identity::self_signed("sender").unwrap(), ) }) } fn rig(recorder: Recorder) -> Rig { let dest = tempfile::tempdir().unwrap(); let recorder = Arc::new(recorder); let (receiver_id, sender_id) = identities(); let tls = server_config( receiver_id, ClientAuth::Request { hint_subjects: vec![], }, ) .unwrap(); let mut settings = Settings::new(dest.path()); settings.min_free_bytes = 0; settings.limits.upload_wait = Duration::from_secs(5); let receiver = Arc::new(Receiver::new(settings, tls, recorder.clone())); let listener = TcpListener::bind(SocketAddr::from((Ipv6Addr::LOCALHOST, 0))).unwrap(); let port = listener.local_addr().unwrap().port(); std::thread::spawn(move || receiver.serve(listener)); let sender = Sender::new(sender_id, "Test Sender", "Mac1,1", "aabbccddeeff", "lo").unwrap(); Rig { dest, recorder, sender, port, } } fn client(rig: &Rig) -> Client { Client::new( rig.sender.clone(), Endpoint::resolve("::1", rig.port, "lo").unwrap(), ) } fn write(dir: &Path, name: &str, data: &[u8]) -> Outgoing { let path = dir.join(name); std::fs::write(&path, data).unwrap(); Outgoing::new(&path).unwrap() } /// Incompressible bytes, so DVZip stores some blocks raw. fn noise(n: usize) -> Vec<u8> { let mut x = 0x2545_f491_4f6c_dd1du64; (0..n) .map(|_| { x ^= x << 13; x ^= x >> 7; x ^= x << 17; (x >> 24) as u8 }) .collect() } #[test] fn a_sender_and_receiver_exchange_files() { let rig = rig(Recorder::default()); let src = tempfile::tempdir().unwrap(); let text = b"hello hello hello hello hello ".repeat(5000); let blob = noise(300_000); let files = [ write(src.path(), "notes.txt", &text), write(src.path(), "blob.bin", &blob), ]; let mut c = client(&rig); let found = c.discover().unwrap(); assert_eq!(found.name.as_deref(), Some("Test Receiver")); assert_eq!(found.model.as_deref(), Some("Linux1,1")); c.begin_transfer().unwrap(); assert_eq!(c.ask(&files).unwrap(), Reply::Accepted); let mut last = 0; assert!( c.upload(&files, |sent| { last = sent; true }) .unwrap() ); assert!(last > 0); assert_eq!( std::fs::read(rig.dest.path().join("notes.txt")).unwrap(), text ); assert_eq!( std::fs::read(rig.dest.path().join("blob.bin")).unwrap(), blob ); let asks = rig.recorder.asks.lock().unwrap(); assert_eq!(asks.len(), 1); assert_eq!(asks[0].sender_name, "Test Sender"); assert_eq!(asks[0].files.len(), 2); assert_eq!(asks[0].files[0].name, "notes.txt"); assert!( asks[0].account.is_none(), "a self-signed sender proves no account" ); let stored = rig.recorder.stored.lock().unwrap(); assert_eq!(stored.len(), 1); assert_eq!(stored[0].names, ["notes.txt", "blob.bin"]); assert!(rig.recorder.failed.lock().unwrap().is_empty()); assert!(!rig.recorder.progress.lock().unwrap().is_empty()); } #[test] fn declining_is_a_406() { let rig = rig(Recorder { decline: true, ..Recorder::default() }); let src = tempfile::tempdir().unwrap(); let files = [write(src.path(), "a.txt", b"a")]; let mut c = client(&rig); c.begin_transfer().unwrap(); assert_eq!(c.ask(&files).unwrap(), Reply::Declined); assert!(!rig.dest.path().join("a.txt").exists()); } #[test] fn contacts_only_hides_from_and_refuses_a_stranger() { let rig = rig(Recorder { visibility: Visibility::Contacts, ..Recorder::default() }); let src = tempfile::tempdir().unwrap(); let files = [write(src.path(), "a.txt", b"a")]; // A sender with no Apple record fails open at /Discover (it cannot be told // from a device we have never measured) and is refused at /Ask. let mut c = client(&rig); assert!(c.discover().is_ok()); c.begin_transfer().unwrap(); assert_eq!(c.ask(&files).unwrap(), Reply::Refused); assert!(rig.recorder.asks.lock().unwrap().is_empty()); } #[test] fn an_upload_without_an_accepted_ask_is_refused_in_contacts_only() { let rig = rig(Recorder { visibility: Visibility::Contacts, ..Recorder::default() }); let src = tempfile::tempdir().unwrap(); let files = [write(src.path(), "a.txt", b"a")]; let mut c = client(&rig); c.begin_transfer().unwrap(); // Straight to /Upload, as an attacker who skips the Ask would. let err = c.upload(&files, |_| true); assert!(matches!(err, Err(_) | Ok(false))); assert!(!rig.dest.path().join("a.txt").exists()); } #[test] fn a_link_is_stored_as_a_shortcut_and_never_opened() { let rig = rig(Recorder::default()); let mut c = client(&rig); c.begin_transfer().unwrap(); let reply = c .ask_links(&[ "https://example.org/some/page".to_owned(), "javascript:alert(1)".to_owned(), ]) .unwrap(); assert_eq!(reply, Reply::Accepted); let stored = rig.recorder.stored.lock().unwrap(); assert_eq!(stored.len(), 1); assert_eq!(stored[0].links.len(), 1); let text = std::fs::read_to_string(rig.dest.path().join(&stored[0].links[0].0)).unwrap(); assert!(text.contains("URL=https://example.org/some/page")); } #[test] fn transfer_labels_are_safe_to_log() { assert_eq!(transfer_label("A1B2-C3.d"), "A1B2-C3.d"); assert_eq!(transfer_label("../../etc/passwd"), "etcpasswd"); assert_eq!(transfer_label("...."), "upload"); assert_eq!(transfer_label(&"x".repeat(200)).len(), 64); } #[test] fn the_budget_never_eats_the_reserve() { assert_eq!(ReceiveBudget::compute(100, 90, 20).unwrap().byte_limit, 80); assert!(ReceiveBudget::compute(10, 90, 20).is_err()); }