use std::{ io::{Read, Write}, net::TcpListener, thread, }; use super::*; #[test] fn builds_announce_and_scrape_urls() { let request = TrackerRequestModel { announce_url: "https://tracker.example.org/announce".to_owned(), info_hash: "0123456789abcdef0123456789abcdef01234567".to_owned(), peer_id: "89abcdef0123456789abcdef0123456789abcdef".to_owned(), port: 6881, uploaded: 1, downloaded: 2, left: 3, event: Some("started".to_owned()), compact: true, numwant: Some(50), }; let announce = request.announce_url().expect("announce url should build"); let scrape = request.scrape_url().expect("scrape url should build"); assert!(announce.starts_with("https://tracker.example.org/announce?")); assert!(announce.contains("info_hash=%")); assert!(announce.contains("peer_id=%")); assert!(announce.contains("event=started")); assert!(scrape.starts_with("https://tracker.example.org/scrape?")); assert!(scrape.contains("info_hash=%")); } #[test] fn parses_dict_announce_response_with_peer_metadata() { let response = announce_bytes( 1800, Some("tracker-01"), vec![peer_dict( "127.0.0.1", 6881, Some("qBittorrent 4.6.5"), true, false, Some([1_u8; 20]), )], None, ); let parsed = TrackerResponseModel::from_announce_bytes(&response).expect("should parse announce"); assert_eq!(parsed.peers.interval_sec, 1800); assert_eq!(parsed.peers.tracker_id.as_deref(), Some("tracker-01")); assert_eq!(parsed.peers.peers.len(), 1); assert_eq!(parsed.peers.peers[0].ip, "127.0.0.1"); assert_eq!(parsed.peers.peers[0].port, 6881); assert_eq!( parsed.peers.peers[0].client_name.as_deref(), Some("qBittorrent 4.6.5") ); assert!(parsed.peers.peers[0].choked); assert!(!parsed.peers.peers[0].interested); assert_eq!(parsed.peers.peers[0].peer_id, Some([1_u8; 20])); } #[test] fn parses_tracker_id_and_scrape_metadata_from_announce_response() { let response = announce_bytes( 900, Some("tracker-02"), vec![peer_dict("127.0.0.2", 6882, None, false, true, None)], Some((7, 3, 11)), ); let parsed = TrackerResponseModel::from_announce_bytes(&response).expect("should parse announce"); assert_eq!(parsed.peers.tracker_id.as_deref(), Some("tracker-02")); assert_eq!(parsed.peers.interval_sec, 900); let scrape = parsed.scrape.expect("scrape metadata should be present"); assert_eq!(scrape.complete, Some(7)); assert_eq!(scrape.incomplete, Some(3)); assert_eq!(scrape.downloaded, Some(11)); assert!(scrape.files.is_empty()); } #[test] fn parses_scrape_response_with_binary_info_hash_keys() { let info_hash_a = [0x11_u8; 20]; let info_hash_b = [0x22_u8; 20]; let response = bencode_dict(vec![( "files".to_owned(), bencode_binary_key_dict(vec![ ( info_hash_a.to_vec(), bencode_dict(vec![ ("complete".to_owned(), bencode_int(7)), ("downloaded".to_owned(), bencode_int(9)), ("incomplete".to_owned(), bencode_int(3)), ]), ), ( info_hash_b.to_vec(), bencode_dict(vec![ ("complete".to_owned(), bencode_int(4)), ("downloaded".to_owned(), bencode_int(5)), ("incomplete".to_owned(), bencode_int(6)), ]), ), ]), )]); let parsed = TrackerResponseModel::from_scrape_bytes(&response).expect("should parse scrape"); assert_eq!(parsed.files.len(), 2); assert_eq!(parsed.files[0].info_hash, hex_encode(&info_hash_a)); assert_eq!(parsed.files[0].complete, Some(7)); assert_eq!(parsed.files[0].downloaded, Some(9)); assert_eq!(parsed.files[0].incomplete, Some(3)); assert_eq!(parsed.files[1].info_hash, hex_encode(&info_hash_b)); assert_eq!(parsed.files[1].complete, Some(4)); assert_eq!(parsed.files[1].downloaded, Some(5)); assert_eq!(parsed.files[1].incomplete, Some(6)); } #[test] fn udp_connect_request_serializes_expected_wire_format() { let request = UdpTrackerConnectRequest { transaction_id: UdpTrackerTransactionId::new(0x1020_3040), }; assert_eq!( request.encode(), vec![ 0x00, 0x00, 0x04, 0x17, 0x27, 0x10, 0x19, 0x80, 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x30, 0x40, ] ); } #[test] fn udp_connect_response_parses_header_and_connection_id() { let response = [ 0x00, 0x00, 0x00, 0x00, 0xaa, 0xbb, 0xcc, 0xdd, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, ]; let parsed = UdpTrackerConnectResponse::decode(&response).expect("connect response should parse"); assert_eq!( parsed.transaction_id, UdpTrackerTransactionId::new(0xaabb_ccdd) ); assert_eq!(parsed.connection_id, 0x0102_0304_0506_0708); } #[test] fn udp_response_header_rejects_invalid_action_id() { let error = UdpTrackerResponseHeader::decode(&[0x00, 0x00, 0x00, 0x09, 0xaa, 0xbb, 0xcc, 0xdd]) .expect_err("invalid action id should fail"); assert!(matches!(error, TrackerParseError::InvalidUdpAction(9))); } #[test] fn udp_response_header_rejects_truncated_payload() { let error = UdpTrackerResponseHeader::decode(&[0x00, 0x00, 0x00]) .expect_err("truncated header should fail"); assert!(matches!(error, TrackerParseError::InvalidUdpPacket(_))); } #[test] fn udp_response_header_rejects_transaction_id_mismatch() { let header = UdpTrackerResponseHeader { action: UdpTrackerAction::Announce, transaction_id: UdpTrackerTransactionId::new(7), }; let error = header .expect_transaction_id(UdpTrackerTransactionId::new(8)) .expect_err("mismatched transaction id should fail"); assert!(matches!( error, TrackerParseError::TransactionIdMismatch { expected: 8, actual: 7 } )); } #[test] fn udp_announce_request_serializes_expected_wire_format() { let request = UdpTrackerAnnounceRequest { connection_id: 0x0102_0304_0506_0708, transaction_id: UdpTrackerTransactionId::new(0x5566_7788), info_hash: [0x11_u8; 20], peer_id: [0x22_u8; 20], downloaded: 0x1122_3344_5566_7788, left: 0x8877_6655_4433_2211, uploaded: 0x0101_0202_0303_0404, event: UdpTrackerAnnounceEvent::Started, ip_address: 0, key: 0x1234_5678, numwant: -1, port: 6881, }; let encoded = request.encode(); assert_eq!(encoded.len(), 98); assert_eq!( &encoded[0..8], &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] ); assert_eq!(&encoded[8..12], &[0x00, 0x00, 0x00, 0x01]); assert_eq!(&encoded[12..16], &[0x55, 0x66, 0x77, 0x88]); assert_eq!(&encoded[16..36], &[0x11_u8; 20]); assert_eq!(&encoded[36..56], &[0x22_u8; 20]); assert_eq!(&encoded[80..84], &[0x00, 0x00, 0x00, 0x02]); assert_eq!(&encoded[88..92], &[0x12, 0x34, 0x56, 0x78]); assert_eq!(&encoded[92..96], &[0xff, 0xff, 0xff, 0xff]); assert_eq!(&encoded[96..98], &[0x1a, 0xe1]); } #[test] fn udp_announce_response_parses_interval_counts_and_peers() { let response = udp_announce_response_bytes( UdpTrackerTransactionId::new(0x0102_0304), 1800, 4, 9, &[(192, 168, 1, 10, 6881), (10, 0, 0, 2, 51413)], ); let parsed = UdpTrackerAnnounceResponse::decode(&response).expect("announce response should parse"); assert_eq!( parsed.transaction_id, UdpTrackerTransactionId::new(0x0102_0304) ); assert_eq!(parsed.interval_sec, 1800); assert_eq!(parsed.leechers, 4); assert_eq!(parsed.seeders, 9); assert_eq!(parsed.peers.len(), 2); assert_eq!(parsed.peers[0].ip, "192.168.1.10"); assert_eq!(parsed.peers[0].port, 6881); assert_eq!(parsed.peers[1].ip, "10.0.0.2"); assert_eq!(parsed.peers[1].port, 51413); } #[test] fn udp_announce_response_rejects_malformed_compact_peer_blob() { let mut response = vec![ 0x00, 0x00, 0x00, 0x01, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0x07, 0x08, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x06, ]; response.extend_from_slice(&[127, 0, 0, 1, 0x1a]); let error = UdpTrackerAnnounceResponse::decode(&response).expect_err("malformed peers should fail"); assert!(matches!(error, TrackerParseError::InvalidPeer(_))); } #[test] fn udp_scrape_request_serializes_multiple_info_hashes() { let request = UdpTrackerScrapeRequest { connection_id: 0x1112_1314_1516_1718, transaction_id: UdpTrackerTransactionId::new(0x99aa_bbcc), info_hashes: vec![[0x44_u8; 20], [0x55_u8; 20]], }; let encoded = request.encode().expect("scrape request should encode"); assert_eq!(encoded.len(), 56); assert_eq!( &encoded[0..8], &[0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18] ); assert_eq!(&encoded[8..12], &[0x00, 0x00, 0x00, 0x02]); assert_eq!(&encoded[12..16], &[0x99, 0xaa, 0xbb, 0xcc]); assert_eq!(&encoded[16..36], &[0x44_u8; 20]); assert_eq!(&encoded[36..56], &[0x55_u8; 20]); } #[test] fn udp_scrape_response_maps_multiple_entries_to_scrape_model() { let response = udp_scrape_response_bytes( UdpTrackerTransactionId::new(0x0bad_f00d), &[(7, 9, 3), (4, 5, 6)], ); let parsed = UdpTrackerScrapeResponse::decode(&response).expect("scrape response should parse"); let scrape = parsed .to_scrape_model(&[[0x33_u8; 20], [0x44_u8; 20]]) .expect("scrape model should build"); assert_eq!( parsed.transaction_id, UdpTrackerTransactionId::new(0x0bad_f00d) ); assert_eq!(scrape.files.len(), 2); assert_eq!(scrape.files[0].info_hash, hex_encode(&[0x33_u8; 20])); assert_eq!(scrape.files[0].complete, Some(7)); assert_eq!(scrape.files[0].downloaded, Some(9)); assert_eq!(scrape.files[0].incomplete, Some(3)); assert_eq!(scrape.files[1].info_hash, hex_encode(&[0x44_u8; 20])); assert_eq!(scrape.files[1].complete, Some(4)); assert_eq!(scrape.files[1].downloaded, Some(5)); assert_eq!(scrape.files[1].incomplete, Some(6)); } #[test] fn udp_scrape_response_rejects_truncated_payload() { let error = UdpTrackerScrapeResponse::decode(&[ 0x00, 0x00, 0x00, 0x02, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, ]) .expect_err("truncated scrape payload should fail"); assert!(matches!(error, TrackerParseError::InvalidUdpPacket(_))); } #[test] fn reqwest_tracker_transport_executes_live_announce_request() { let listener = TcpListener::bind("127.0.0.1:0").expect("local listener should bind"); let addr = listener.local_addr().expect("local addr should exist"); let handle = thread::spawn(move || { let (mut stream, _) = listener.accept().expect("client should connect"); let mut request = [0_u8; 2048]; let read = stream.read(&mut request).expect("request should read"); let request_text = String::from_utf8_lossy(&request[..read]); assert!(request_text.starts_with("GET /announce?")); assert!(request_text.contains("info_hash=")); assert!(request_text.contains("peer_id=")); assert!(request_text.contains("compact=1")); assert!(request_text.contains("event=started")); let payload = announce_bytes( 1200, Some("live-tracker"), vec![peer_dict( "127.0.0.1", 6881, Some("local-peer"), false, true, None, )], Some((5, 2, 9)), ); let response = format!( "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: text/plain\r\n\r\n", payload.len() ); stream .write_all(response.as_bytes()) .expect("headers should write"); stream.write_all(&payload).expect("payload should write"); }); let transport = ReqwestTrackerTransport::new().expect("tracker transport should build"); let response = transport .announce(&TrackerRequestModel { announce_url: format!("http://{addr}/announce"), info_hash: "0123456789abcdef0123456789abcdef01234567".to_owned(), peer_id: "89abcdef0123456789abcdef0123456789abcdef".to_owned(), port: 6881, uploaded: 0, downloaded: 0, left: 1024, event: Some("started".to_owned()), compact: true, numwant: Some(25), }) .expect("announce should succeed"); assert_eq!(response.peers.interval_sec, 1200); assert_eq!(response.peers.tracker_id.as_deref(), Some("live-tracker")); assert_eq!(response.peers.peers.len(), 1); assert_eq!(response.peers.peers[0].ip, "127.0.0.1"); let scrape = response.scrape.expect("scrape stats should be present"); assert_eq!(scrape.complete, Some(5)); assert_eq!(scrape.incomplete, Some(2)); assert_eq!(scrape.downloaded, Some(9)); handle.join().expect("server thread should join"); } #[test] fn dht_node_model_parses_and_formats_ipv4_and_ipv6_specs() { let ipv4 = DhtNodeModel::from_spec("198.51.100.9:51413").expect("ipv4 should parse"); assert_eq!(ipv4.address, "198.51.100.9"); assert_eq!(ipv4.port, 51413); assert_eq!(ipv4.to_spec(), "198.51.100.9:51413"); let ipv6 = DhtNodeModel::from_spec("[2001:db8::9]:6881").expect("ipv6 should parse"); assert_eq!(ipv6.address, "2001:db8::9"); assert_eq!(ipv6.port, 6881); assert_eq!(ipv6.to_spec(), "[2001:db8::9]:6881"); } #[test] fn tracker_request_converts_into_udp_announce_request() { let request = TrackerRequestModel { announce_url: "udp://tracker.example.org:6969".to_owned(), info_hash: "00112233445566778899aabbccddeeff00112233".to_owned(), peer_id: "89abcdef0123456789abcdef0123456789abcdef".to_owned(), port: 51413, uploaded: 11, downloaded: 22, left: 33, event: Some("started".to_owned()), compact: true, numwant: Some(40), }; let udp = request .to_udp_announce_request(0x1122_3344_5566_7788, UdpTrackerTransactionId::new(77)) .expect("tracker request should convert"); assert_eq!(udp.connection_id, 0x1122_3344_5566_7788); assert_eq!(udp.transaction_id, UdpTrackerTransactionId::new(77)); assert_eq!(udp.info_hash[0], 0x00); assert_eq!(udp.info_hash[19], 0x33); assert_eq!(udp.peer_id[0], 0x89); assert_eq!(udp.peer_id[19], 0xef); assert_eq!(udp.event, UdpTrackerAnnounceEvent::Started); assert_eq!(udp.numwant, 40); assert_eq!(udp.port, 51413); } fn announce_bytes( interval: i64, tracker_id: Option<&str>, peers: Vec>, scrape: Option<(i64, i64, i64)>, ) -> Vec { let mut fields = Vec::new(); fields.push(("interval".to_owned(), bencode_int(interval))); if let Some(tracker_id) = tracker_id { fields.push(( "tracker id".to_owned(), bencode_bytes(tracker_id.as_bytes()), )); } fields.push(("peers".to_owned(), bencode_list(peers))); if let Some((complete, incomplete, downloaded)) = scrape { fields.push(("complete".to_owned(), bencode_int(complete))); fields.push(("incomplete".to_owned(), bencode_int(incomplete))); fields.push(("downloaded".to_owned(), bencode_int(downloaded))); } bencode_dict(fields) } fn peer_dict( ip: &str, port: i64, client: Option<&str>, choked: bool, interested: bool, peer_id: Option<[u8; 20]>, ) -> Vec { let mut fields = vec![ ("ip".to_owned(), bencode_bytes(ip.as_bytes())), ("port".to_owned(), bencode_int(port)), ( "choked".to_owned(), bencode_int(i64::from(u8::from(choked))), ), ( "interested".to_owned(), bencode_int(i64::from(u8::from(interested))), ), ]; if let Some(client) = client { fields.push(("client".to_owned(), bencode_bytes(client.as_bytes()))); } if let Some(peer_id) = peer_id { fields.push(("peer id".to_owned(), bencode_bytes(&peer_id))); } bencode_dict(fields) } fn bencode_dict(fields: Vec<(String, Vec)>) -> Vec { let mut out = Vec::from(b"d".as_slice()); for (key, value) in fields { out.extend_from_slice(key.len().to_string().as_bytes()); out.push(b':'); out.extend_from_slice(key.as_bytes()); out.extend_from_slice(&value); } out.push(b'e'); out } fn bencode_binary_key_dict(fields: Vec<(Vec, Vec)>) -> Vec { let mut out = Vec::from(b"d".as_slice()); for (key, value) in fields { out.extend_from_slice(key.len().to_string().as_bytes()); out.push(b':'); out.extend_from_slice(&key); out.extend_from_slice(&value); } out.push(b'e'); out } fn bencode_list(values: Vec>) -> Vec { let mut out = Vec::from(b"l".as_slice()); for value in values { out.extend_from_slice(&value); } out.push(b'e'); out } fn bencode_int(value: i64) -> Vec { format!("i{value}e").into_bytes() } fn bencode_bytes(value: &[u8]) -> Vec { let mut out = Vec::new(); out.extend_from_slice(value.len().to_string().as_bytes()); out.push(b':'); out.extend_from_slice(value); out } fn udp_announce_response_bytes( transaction_id: UdpTrackerTransactionId, interval_sec: u32, leechers: u32, seeders: u32, peers: &[(u8, u8, u8, u8, u16)], ) -> Vec { let mut out = Vec::new(); out.extend_from_slice(&1_u32.to_be_bytes()); out.extend_from_slice(&transaction_id.get().to_be_bytes()); out.extend_from_slice(&interval_sec.to_be_bytes()); out.extend_from_slice(&leechers.to_be_bytes()); out.extend_from_slice(&seeders.to_be_bytes()); for (a, b, c, d, port) in peers { out.extend_from_slice(&[*a, *b, *c, *d]); out.extend_from_slice(&port.to_be_bytes()); } out } fn udp_scrape_response_bytes( transaction_id: UdpTrackerTransactionId, entries: &[(u32, u32, u32)], ) -> Vec { let mut out = Vec::new(); out.extend_from_slice(&2_u32.to_be_bytes()); out.extend_from_slice(&transaction_id.get().to_be_bytes()); for (complete, downloaded, incomplete) in entries { out.extend_from_slice(&complete.to_be_bytes()); out.extend_from_slice(&downloaded.to_be_bytes()); out.extend_from_slice(&incomplete.to_be_bytes()); } out }