Files
aria2-rust-pro/crates/aria2-rust-pro-protocol/src/tracker/tracker_tests.rs
T

568 lines
18 KiB
Rust

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<Vec<u8>>,
scrape: Option<(i64, i64, i64)>,
) -> Vec<u8> {
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<u8> {
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<u8>)>) -> Vec<u8> {
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<u8>, Vec<u8>)>) -> Vec<u8> {
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<u8>>) -> Vec<u8> {
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<u8> {
format!("i{value}e").into_bytes()
}
fn bencode_bytes(value: &[u8]) -> Vec<u8> {
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<u8> {
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<u8> {
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
}