You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
7.5 KiB
Rust
270 lines
7.5 KiB
Rust
use reqwest::blocking::Client;
|
|
use reqwest::{Method, Url};
|
|
use std::collections::HashMap;
|
|
use std::convert::TryInto;
|
|
use std::net::{IpAddr, SocketAddr};
|
|
use std::ops::Add;
|
|
use std::str::FromStr;
|
|
use std::time::{Duration, Instant};
|
|
use torment_bencode::BencodeValue;
|
|
use torment_core::infohash::v1::U160;
|
|
use torment_core::infohash::InfoHashCapable;
|
|
use torment_core::peer_storage::{PeerStorageHolder, PeerStorageSource};
|
|
use torment_core::CompactContact;
|
|
|
|
pub struct Tracker {
|
|
url: Url,
|
|
is_udp: bool,
|
|
// last_scrape: Option<Instant>,
|
|
last_announce: HashMap<U160, Instant>,
|
|
requested_interval: Duration,
|
|
failure: HashMap<U160, String>,
|
|
peer_storage: PeerStorageHolder,
|
|
// supports_scrape: bool,
|
|
}
|
|
|
|
pub enum TrackerAnnounceRequest {
|
|
HTTP(Url, U160),
|
|
UDP(Url, U160),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct TrackerAnnounceResponse {
|
|
info_hash: U160,
|
|
// url: Url,
|
|
interval: Option<Duration>,
|
|
source: PeerStorageSource,
|
|
result: Result<Vec<SocketAddr>, String>,
|
|
}
|
|
|
|
impl Tracker {
|
|
pub fn url(&self) -> &Url {
|
|
&self.url
|
|
}
|
|
|
|
pub fn new(url: Url, peer_storage: PeerStorageHolder) -> Tracker {
|
|
Tracker {
|
|
is_udp: url.scheme() == "udp",
|
|
url,
|
|
last_announce: HashMap::new(),
|
|
requested_interval: Duration::from_secs(30 * 60),
|
|
failure: HashMap::new(),
|
|
peer_storage,
|
|
}
|
|
}
|
|
|
|
pub fn needs_update(&self, info_hash: U160) -> bool {
|
|
self.last_announce
|
|
.get(&info_hash)
|
|
.map(|inst| inst.add(self.requested_interval) < Instant::now())
|
|
.unwrap_or(true)
|
|
}
|
|
|
|
pub fn process(&mut self, response: TrackerAnnounceResponse) {
|
|
match response.result {
|
|
Ok(peers) => {
|
|
self.peer_storage
|
|
.add_peers(response.info_hash, peers, response.source);
|
|
}
|
|
|
|
Err(err) => {
|
|
self.failure.insert(response.info_hash, err);
|
|
}
|
|
}
|
|
|
|
if let Some(interval) = response.interval {
|
|
self.requested_interval = interval;
|
|
} else {
|
|
self.requested_interval = Duration::from_secs(60 * 30);
|
|
}
|
|
}
|
|
|
|
pub fn create_update_request(
|
|
&mut self,
|
|
info_hash: U160,
|
|
peer_id: U160,
|
|
ip: IpAddr,
|
|
port: u16,
|
|
uploaded: usize,
|
|
downloaded: usize,
|
|
left: i64,
|
|
) -> TrackerAnnounceRequest {
|
|
if self.is_udp {
|
|
return TrackerAnnounceRequest::UDP(self.url.clone(), info_hash);
|
|
}
|
|
|
|
let mut url = self.url.clone();
|
|
info_hash.to_byte_array();
|
|
{
|
|
let mut query = url.query_pairs_mut();
|
|
query.append_pair("ip", &format!("{}", ip));
|
|
query.append_pair("port", &format!("{}", port));
|
|
query.append_pair("uploaded", &format!("{}", uploaded));
|
|
query.append_pair("downloaded", &format!("{}", downloaded));
|
|
query.append_pair("left", &format!("{}", left));
|
|
}
|
|
|
|
let encoded_info_hash = url::form_urlencoded::byte_serialize(&info_hash.to_bytes()).fold(
|
|
String::new(),
|
|
|mut coll, curr| {
|
|
coll.push_str(curr);
|
|
coll
|
|
},
|
|
);
|
|
|
|
let encoded_peer_id = url::form_urlencoded::byte_serialize(&peer_id.to_bytes()).fold(
|
|
String::new(),
|
|
|mut coll, curr| {
|
|
coll.push_str(curr);
|
|
coll
|
|
},
|
|
);
|
|
let query = url.query().unwrap_or("");
|
|
url.set_query(Some(&format!(
|
|
"{}&info_hash={}&peer_id={}",
|
|
query, encoded_info_hash, encoded_peer_id
|
|
)));
|
|
|
|
self.last_announce.insert(info_hash, Instant::now());
|
|
|
|
TrackerAnnounceRequest::HTTP(url, info_hash)
|
|
}
|
|
}
|
|
|
|
pub fn block_do_announce(
|
|
request: TrackerAnnounceRequest,
|
|
client: &Client,
|
|
) -> TrackerAnnounceResponse {
|
|
let (url, info_hash) = match request {
|
|
TrackerAnnounceRequest::HTTP(url, info_hash) => (url, info_hash),
|
|
TrackerAnnounceRequest::UDP(url, info_hash) => {
|
|
return TrackerAnnounceResponse {
|
|
info_hash,
|
|
interval: None,
|
|
source: PeerStorageSource::Tracker(url.clone()),
|
|
// url,
|
|
result: Err(format!("UDP trackers not supported")),
|
|
};
|
|
}
|
|
};
|
|
|
|
println!("Announcing {} on {}", info_hash, url);
|
|
|
|
let res = block_do_http_response_parse(url.clone(), client);
|
|
|
|
TrackerAnnounceResponse {
|
|
info_hash,
|
|
source: PeerStorageSource::Tracker(url.clone()),
|
|
// url,
|
|
interval: res.as_ref().map(|x| x.0).ok(),
|
|
result: res.map(|x| x.1),
|
|
}
|
|
}
|
|
|
|
pub fn block_do_http_response_parse(
|
|
url: Url,
|
|
http_client: &Client,
|
|
) -> Result<(Duration, Vec<SocketAddr>), String> {
|
|
let resp = match http_client.request(Method::GET, url).send() {
|
|
Ok(resp) => resp,
|
|
|
|
Err(err) => {
|
|
return Err(format!("{}", err));
|
|
}
|
|
};
|
|
|
|
let body = resp.bytes();
|
|
if let Err(err) = body {
|
|
return Err(format!("{}", err));
|
|
}
|
|
|
|
let body = body.unwrap();
|
|
let body = BencodeValue::decode(body);
|
|
if let Err(err) = body {
|
|
return Err(format!("Bencoding error: {}", err));
|
|
}
|
|
|
|
let body = body.unwrap();
|
|
let body = body.dict();
|
|
if body.is_none() {
|
|
return Err(format!("Tracker gave invalid response"));
|
|
}
|
|
|
|
let body = body.unwrap();
|
|
|
|
if let Some(failure) = body.get(b"failure reason") {
|
|
let failure = failure.string().and_then(|x| x.ok()).unwrap_or(format!(
|
|
"Tracker failed to respond with valid failure, but did respond with a failure"
|
|
));
|
|
|
|
return Err(failure);
|
|
}
|
|
|
|
let interval = body
|
|
.get(b"interval")
|
|
.and_then(|item| item.int())
|
|
.unwrap_or(30 * 60);
|
|
|
|
let interval = Duration::from_secs(interval as u64);
|
|
|
|
let peers = body.get(b"peers");
|
|
|
|
if peers.is_none() {
|
|
return Ok((interval, vec![]));
|
|
}
|
|
|
|
let peers = peers.unwrap();
|
|
|
|
let peers: Vec<SocketAddr> = match peers {
|
|
BencodeValue::List(old) => {
|
|
let mut buffer = vec![];
|
|
for item in old.iter() {
|
|
let dict = if let Some(dict) = item.dict() {
|
|
dict
|
|
} else {
|
|
continue;
|
|
};
|
|
|
|
let ip = if let Some(BencodeValue::Bytes(ip)) = dict.get(b"ip") {
|
|
ip.clone()
|
|
} else {
|
|
continue;
|
|
};
|
|
|
|
let ip = if let Some(ip) = IpAddr::from_str(&String::from_utf8_lossy(&ip)).ok() {
|
|
ip
|
|
} else {
|
|
continue;
|
|
};
|
|
|
|
let port: u16 = if let Some(BencodeValue::Int(nr)) = dict.get(b"port") {
|
|
if *nr < u16::MAX as i64 {
|
|
if let Ok(port) = (*nr).try_into() {
|
|
port
|
|
} else {
|
|
continue;
|
|
}
|
|
} else {
|
|
continue;
|
|
}
|
|
} else {
|
|
continue;
|
|
};
|
|
|
|
buffer.push(SocketAddr::new(ip, port));
|
|
}
|
|
|
|
buffer
|
|
}
|
|
BencodeValue::Bytes(compact) => compact
|
|
.chunks(6)
|
|
.filter_map(|x| SocketAddr::from_compact_contact(x).ok())
|
|
.collect(),
|
|
_ => {
|
|
return Err(format!("Tracker failed to respond with valid response"));
|
|
}
|
|
};
|
|
|
|
Ok((interval, peers))
|
|
}
|