mod http;
mod into_url;
mod udp;
pub use torrent_core::tracker::{
AnnounceEvent, AnnounceRequest, AnnounceResponse, parse_compact_peers_ipv4,
};
pub use url::Url;
pub use self::http::HttpTracker;
pub use self::into_url::IntoUrl;
pub use self::udp::UdpTracker;
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinSet;
use crate::error::{Error, ErrorKind};
use crate::spec::TorrentSpec;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
#[derive(Debug, Clone)]
pub struct Tracker {
trackers: Vec<Inner>,
default_timeout: Duration,
}
impl Tracker {
pub fn single(url: impl IntoUrl) -> Option<Self> {
Self::single_with_timeout(url, DEFAULT_TIMEOUT)
}
pub fn single_with_timeout(url: impl IntoUrl, timeout: Duration) -> Option<Self> {
let inner = Inner::from_url(url.into_url().ok()?, timeout).ok()?;
Some(Tracker {
trackers: vec![inner],
default_timeout: timeout,
})
}
pub fn multi<I: IntoIterator>(urls: I) -> Option<Self>
where
I::Item: IntoUrl,
{
Self::multi_with_timeout(urls, DEFAULT_TIMEOUT)
}
pub fn multi_with_timeout<I: IntoIterator>(urls: I, timeout: Duration) -> Option<Self>
where
I::Item: IntoUrl,
{
let mut seen: HashSet<String> = HashSet::new();
let mut trackers: Vec<Inner> = Vec::new();
for url in urls {
if let Ok(url) = url.into_url()
&& seen.insert(url.as_str().into())
&& let Ok(inner) = Inner::from_url(url, timeout)
{
trackers.push(inner);
}
}
if trackers.is_empty() {
None
} else {
Some(Tracker {
trackers,
default_timeout: timeout,
})
}
}
pub fn from_torrent(spec: impl Into<TorrentSpec>) -> Option<Self> {
Self::from_torrent_with_timeout(spec, DEFAULT_TIMEOUT)
}
pub fn from_torrent_with_timeout(
spec: impl Into<TorrentSpec>, timeout: Duration,
) -> Option<Self> {
Self::multi_with_timeout(spec.into().trackers(), timeout)
}
pub fn len(&self) -> usize {
self.trackers.len()
}
pub fn is_empty(&self) -> bool {
self.trackers.is_empty()
}
pub fn add(&mut self, url: impl IntoUrl) -> Result<(), Error> {
let url = url.into_url()?;
if self.trackers.iter().any(|t| t.url() == url.as_str()) {
return Ok(());
}
self.trackers
.push(Inner::from_url(url, self.default_timeout)?);
Ok(())
}
pub fn add_all<I: IntoIterator>(&mut self, urls: I) -> Result<(), Error>
where
I::Item: IntoUrl,
{
let mut seen: HashSet<String> = self.trackers.iter().map(|t| t.url().into()).collect();
let mut new_trackers: Vec<Inner> = Vec::new();
for url in urls {
let url = url.into_url()?;
if seen.insert(url.as_str().into()) {
new_trackers.push(Inner::from_url(url, self.default_timeout)?);
}
}
self.trackers.extend(new_trackers);
Ok(())
}
pub fn remove(&mut self, url: &str) -> bool {
let len_before = self.trackers.len();
self.trackers.retain(|inner| inner.url() != url);
self.trackers.len() < len_before
}
pub fn clear(&mut self) {
self.trackers.clear();
}
pub fn urls(&self) -> Vec<&str> {
self.trackers.iter().map(|inner| inner.url()).collect()
}
pub async fn announce(&self, req: &AnnounceRequest) -> Result<AnnounceResponse, Error> {
self.announce_first(req).await
}
pub async fn announce_first(&self, req: &AnnounceRequest) -> Result<AnnounceResponse, Error> {
let mut set = self.announce_into_set(req);
let mut last_err = None;
while let Some(result) = set.join_next().await {
match result {
Ok(Ok(resp)) => return Ok(resp),
Ok(Err(e)) => last_err = Some(e),
Err(_) => last_err = Some(Error::new(ErrorKind::TrackerRequestFailed)),
}
}
Err(last_err.unwrap_or_else(|| Error::new(ErrorKind::TrackerRequestFailed)))
}
pub async fn announce_all(&self, req: &AnnounceRequest) -> Vec<AnnounceResponse> {
let mut set = self.announce_into_set(req);
let mut results = Vec::new();
while let Some(result) = set.join_next().await {
if let Ok(Ok(resp)) = result {
results.push(resp);
}
}
results
}
pub fn announce_into_set(
&self, req: &AnnounceRequest,
) -> JoinSet<Result<AnnounceResponse, Error>> {
let mut set = JoinSet::new();
let req = Arc::new(req.clone());
for inner in &self.trackers {
let inner = inner.clone();
let req = Arc::clone(&req);
set.spawn(async move { inner.announce(&req).await });
}
set
}
}
#[derive(Debug, Clone)]
enum Inner {
Http(HttpTracker),
Udp(UdpTracker),
}
impl Inner {
fn url(&self) -> &str {
match self {
Inner::Http(t) => t.url().as_str(),
Inner::Udp(t) => t.url().as_str(),
}
}
fn from_url(url: Url, timeout: Duration) -> Result<Self, Error> {
match url.scheme() {
"http" | "https" => Ok(Inner::Http(HttpTracker::with_timeout(url, timeout)?)),
"udp" => Ok(Inner::Udp(UdpTracker::with_timeout(url, timeout)?)),
_ => Err(Error::new(ErrorKind::InvalidInput)),
}
}
async fn announce(&self, req: &AnnounceRequest) -> Result<AnnounceResponse, Error> {
match self {
Inner::Http(t) => t.announce(req).await,
Inner::Udp(t) => t.announce(req).await,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::peer::PeerId;
#[test]
fn test_tracker_single_http() {
let t = Tracker::single("http://tracker.example.com:6969/announce").unwrap();
assert_eq!(t.trackers.len(), 1);
}
#[test]
fn test_tracker_single_udp() {
let t = Tracker::single("udp://tracker.example.com:6969").unwrap();
assert_eq!(t.trackers.len(), 1);
}
#[test]
fn test_tracker_single_https() {
let t = Tracker::single("https://tracker.example.com/announce").unwrap();
assert_eq!(t.trackers.len(), 1);
}
#[test]
fn test_tracker_multi_mixed_http_https() {
let result = Tracker::multi([
"http://tracker.a.com/announce",
"https://tracker.b.com/announce",
]);
let t = result.unwrap();
assert_eq!(t.trackers.len(), 2);
}
#[test]
fn test_tracker_single_invalid_scheme() {
assert!(Tracker::single("ftp://tracker.example.com").is_none());
}
#[test]
fn test_tracker_single_invalid_url() {
assert!(Tracker::single("not a url").is_none());
}
#[test]
fn test_tracker_multi_valid() {
let t =
Tracker::multi(["http://tracker.a.com/announce", "udp://tracker.b.com:6969"]).unwrap();
assert_eq!(t.trackers.len(), 2);
}
#[test]
fn test_tracker_multi_skips_invalid() {
let t = Tracker::multi(["http://tracker.a.com/announce", "not a url"]).unwrap();
assert_eq!(t.trackers.len(), 1);
}
#[test]
fn test_tracker_multi_all_invalid_scheme() {
assert!(Tracker::multi(["ftp://tracker.example.com"]).is_none());
}
#[tokio::test]
async fn test_tracker_multi_empty() {
let urls: Vec<&str> = Vec::new();
assert!(Tracker::multi(urls).is_none());
}
#[tokio::test]
async fn test_tracker_announce_into_set_type() {
let t = Tracker::single("http://tracker.example.com/announce").unwrap();
let mut req = AnnounceRequest::new([0u8; 20], PeerId::random(), 6881);
req.compact = false;
req.numwant = None;
let set: JoinSet<Result<AnnounceResponse, Error>> = t.announce_into_set(&req);
assert!(!set.is_empty());
}
#[test]
fn test_tracker_multi_from_vec_string() {
let urls = vec![
"http://tracker.a.com/announce".to_string(),
"udp://tracker.b.com:6969".to_string(),
];
let t = Tracker::multi(urls).unwrap();
assert_eq!(t.trackers.len(), 2);
}
#[test]
fn test_tracker_multi_from_urls() {
let url1 = Url::parse("http://tracker.a.com/announce").unwrap();
let url2 = Url::parse("udp://tracker.b.com:6969").unwrap();
let t = Tracker::multi([url1, url2]).unwrap();
assert_eq!(t.trackers.len(), 2);
}
#[test]
fn test_tracker_from_torrent() {
use crate::metainfo::{Bytes, Info, Metainfo, Mode, RawInfo};
let info = Info {
piece_length: 262144,
pieces: vec![[0u8; 20]],
mode: Mode::Single {
name: "test.txt".into(),
length: 1024,
},
raw_info: RawInfo::Bytes(Bytes::new()),
};
let meta = Metainfo {
announce: "http://tracker.a.com/announce".into(),
announce_list: vec![vec!["udp://tracker.b.com:6969".into()]],
info,
creation_date: None,
comment: None,
created_by: None,
encoding: None,
};
let t = Tracker::from_torrent(meta).unwrap();
assert_eq!(t.trackers.len(), 2);
}
#[test]
fn test_tracker_from_torrent_skip_invalid() {
use crate::metainfo::{Bytes, Info, Metainfo, Mode, RawInfo};
let info = Info {
piece_length: 262144,
pieces: vec![[0u8; 20]],
mode: Mode::Single {
name: "test.txt".into(),
length: 1024,
},
raw_info: RawInfo::Bytes(Bytes::new()),
};
let meta = Metainfo {
announce: "ftp://tracker.a.com/announce".into(),
announce_list: vec![vec!["http://tracker.b.com/announce".into()]],
info,
creation_date: None,
comment: None,
created_by: None,
encoding: None,
};
let t = Tracker::from_torrent(meta).unwrap();
assert_eq!(t.trackers.len(), 1);
assert_eq!(t.urls(), &["http://tracker.b.com/announce"]);
}
#[test]
fn test_tracker_add_and_remove() {
let mut t = Tracker::single("http://tracker.a.com/announce").unwrap();
assert_eq!(t.len(), 1);
assert!(!t.is_empty());
assert_eq!(t.urls(), &["http://tracker.a.com/announce"]);
t.add("udp://tracker.b.com:6969").unwrap();
assert_eq!(t.len(), 2);
assert_eq!(
t.urls(),
&["http://tracker.a.com/announce", "udp://tracker.b.com:6969"]
);
assert!(t.remove("http://tracker.a.com/announce"));
assert_eq!(t.len(), 1);
assert!(!t.remove("http://tracker.a.com/announce"));
t.clear();
assert!(t.is_empty());
assert!(t.urls().is_empty());
}
#[test]
fn test_tracker_multi_dedup() {
let t = Tracker::multi([
"http://tracker.a.com/announce",
"udp://tracker.b.com:6969",
"http://tracker.a.com/announce", ])
.unwrap();
assert_eq!(t.trackers.len(), 2);
assert_eq!(
t.urls(),
&["http://tracker.a.com/announce", "udp://tracker.b.com:6969"]
);
}
#[test]
fn test_tracker_multi_dedup_all_same() {
let t = Tracker::multi([
"http://tracker.a.com/announce",
"http://tracker.a.com/announce",
"http://tracker.a.com/announce",
])
.unwrap();
assert_eq!(t.trackers.len(), 1);
}
#[test]
fn test_tracker_add_dedup() {
let mut t = Tracker::single("http://tracker.a.com/announce").unwrap();
assert_eq!(t.len(), 1);
t.add("http://tracker.a.com/announce").unwrap();
assert_eq!(t.len(), 1);
}
#[test]
fn test_tracker_from_torrent_dedup_across_tiers() {
use crate::metainfo::{Bytes, Info, Metainfo, Mode, RawInfo};
let info = Info {
piece_length: 262144,
pieces: vec![[0u8; 20]],
mode: Mode::Single {
name: "test.txt".into(),
length: 1024,
},
raw_info: RawInfo::Bytes(Bytes::new()),
};
let meta = Metainfo {
announce: "http://tracker.a.com/announce".into(),
announce_list: vec![
vec!["http://tracker.a.com/announce".into()], vec!["udp://tracker.b.com:6969".into()],
],
info,
creation_date: None,
comment: None,
created_by: None,
encoding: None,
};
let t = Tracker::from_torrent(meta).unwrap();
assert_eq!(t.trackers.len(), 2);
assert_eq!(
t.urls(),
&["http://tracker.a.com/announce", "udp://tracker.b.com:6969"]
);
}
#[test]
fn test_tracker_add_all_dedup() {
let mut t = Tracker::single("http://tracker.a.com/announce").unwrap();
t.add_all([
"udp://tracker.b.com:6969",
"http://tracker.c.com/announce",
"udp://tracker.b.com:6969", ])
.unwrap();
assert_eq!(t.trackers.len(), 3);
assert_eq!(
t.urls(),
&[
"http://tracker.a.com/announce",
"udp://tracker.b.com:6969",
"http://tracker.c.com/announce"
]
);
}
#[test]
fn test_tracker_add_all_dedup_with_existing() {
let mut t = Tracker::single("http://tracker.a.com/announce").unwrap();
t.add_all([
"http://tracker.a.com/announce", "udp://tracker.b.com:6969", "http://tracker.a.com/announce", "udp://tracker.b.com:6969", "http://tracker.c.com/announce", ])
.unwrap();
assert_eq!(t.trackers.len(), 3);
}
#[test]
fn test_tracker_add_all_invalid_url() {
let mut t = Tracker::single("http://tracker.a.com/announce").unwrap();
let result = t.add_all([
"udp://tracker.b.com:6969",
"not a url",
"http://tracker.c.com/announce",
]);
assert!(result.is_err());
assert_eq!(t.len(), 1);
}
#[test]
fn test_tracker_add_all_empty() {
let mut t = Tracker::single("http://tracker.a.com/announce").unwrap();
let urls: Vec<&str> = Vec::new();
t.add_all(urls).unwrap();
assert_eq!(t.trackers.len(), 1); }
#[test]
fn test_tracker_from_torrent_preserves_order() {
use crate::metainfo::{Bytes, Info, Metainfo, Mode, RawInfo};
let info = Info {
piece_length: 262144,
pieces: vec![[0u8; 20]],
mode: Mode::Single {
name: "test.txt".into(),
length: 1024,
},
raw_info: RawInfo::Bytes(Bytes::new()),
};
let meta = Metainfo {
announce: "http://tracker.a.com/announce".into(),
announce_list: vec![
vec!["udp://tracker.b.com:6969".into()],
vec!["http://tracker.c.com/announce".into()],
],
info,
creation_date: None,
comment: None,
created_by: None,
encoding: None,
};
let t = Tracker::from_torrent(meta).unwrap();
assert_eq!(t.trackers.len(), 3);
assert_eq!(
t.urls(),
&[
"http://tracker.a.com/announce",
"udp://tracker.b.com:6969",
"http://tracker.c.com/announce"
]
);
}
#[test]
fn test_tracker_remove_nonexistent() {
let mut t = Tracker::single("http://tracker.a.com/announce").unwrap();
assert!(t.remove("http://tracker.a.com/announce"));
assert!(t.is_empty());
assert!(!t.remove("http://tracker.a.com/announce"));
assert!(t.is_empty());
assert!(!t.remove("udp://tracker.b.com:6969"));
assert!(t.is_empty());
}
}