Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ Torrust Tracker is a lightweight but incredibly powerful and feature-rich BitTor

### Features
* [X] UDP server
* [X] HTTP (optional SSL) server
* [X] HTTP and/or HTTPS (SSL) server
* [X] Multiple UDP and HTTP(S) blocks for socket binding possible
* [X] Full IPv4 and IPv6 support for both UDP and HTTP(S)
* [X] Private & Whitelisted mode
* [X] Built-in API
* [X] Torrent whitelisting
* [X] Peer authentication using time-bound keys
* [ ] NewTrackOn check supported
* [X] SQLite3 Persistent loading and saving of the torrent hashes and completed count

### Implemented BEPs
* [BEP 15](http://www.bittorrent.org/beps/bep_0015.html): UDP Tracker Protocol for BitTorrent
Expand Down Expand Up @@ -44,24 +48,27 @@ cargo build --release

* Edit the newly created config.toml file according to your liking, see [configuration documentation](https://torrust.github.io/torrust-documentation/torrust-tracker/config/). Eg:
```toml
log_level = "trace"
log_level = "info"
mode = "public"
db_path = "data.db"
persistence = false
cleanup_interval = 600
external_ip = "YOUR_EXTERNAL_IP"
cleanup_peerless = true
external_ip = "0.0.0.0"
announce_interval = 0
on_reverse_proxy = false

[udp_tracker]
[[udp_trackers]]
enabled = true
bind_address = "0.0.0.0:6969"
announce_interval = 120

[http_tracker]
[[http_trackers]]
enabled = true
bind_address = "0.0.0.0:6969"
on_reverse_proxy = false
announce_interval = 120
ssl_enabled = false
ssl_cert_path = ""
ssl_key_path = ""
ssl_enabled = true
ssl_bind_address = "0.0.0.0:6868"
ssl_cert_path = "cert.pem"
ssl_key_path = "key.pem"

[http_api]
enabled = true
Expand All @@ -78,7 +85,7 @@ admin = "MyAccessToken"
```

### Tracker URL
Your tracker announce URL will be **udp://{tracker-ip:port}** or **https://{tracker-ip:port}/announce** depending on your tracker mode.
Your tracker announce URL will be **udp://{tracker-ip:port}** and/or **http://{tracker-ip:port}/announce** and/or **https://{tracker-ip:port}/announce** depending on your bindings.
In private & private_listed mode, tracker keys are added after the tracker URL like: **https://{tracker-ip:port}/announce/{key}**.

### Built-in API
Expand All @@ -87,3 +94,4 @@ Read the API documentation [here](https://torrust.github.io/torrust-documentatio
### Credits
This project was a joint effort by [Nautilus Cyberneering GmbH](https://nautilus-cyberneering.de/) and [Dutch Bits](https://dutchbits.nl).
Also thanks to [Naim A.](https://github.com/naim94a/udpt) and [greatest-ape](https://github.com/greatest-ape/aquatic) for some parts of the code.
Further added features and functions thanks to [Power2All](https://github.com/power2all).
25 changes: 18 additions & 7 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ impl serde::ser::Serialize for InfoHash {
let mut buffer = [0u8; 40];
let bytes_out = binascii::bin2hex(&self.0, &mut buffer).ok().unwrap();
let str_out = std::str::from_utf8(bytes_out).unwrap();

serializer.serialize_str(str_out)
}
}
Expand Down Expand Up @@ -126,15 +125,27 @@ impl<'v> serde::de::Visitor<'v> for InfoHashVisitor {
}

#[derive(PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord)]
pub struct PeerId(pub String);
pub struct PeerId(pub [u8; 20]);

impl PeerId {
pub fn to_string(&self) -> String {
let mut buffer = [0u8; 20];
let bytes_out = binascii::bin2hex(&self.0, &mut buffer).ok();
return if let Some(bytes_out) = bytes_out {
String::from(std::str::from_utf8(bytes_out).unwrap())
} else {
"".to_string()
}
}
}

impl PeerId {
pub fn get_client_name(&self) -> Option<&'static str> {
if self.0.as_bytes()[0] == b'M' {
if self.0[0] == b'M' {
return Some("BitTorrent");
}
if self.0.as_bytes()[0] == b'-' {
let name = match &self.0.as_bytes()[1..3] {
if self.0[0] == b'-' {
let name = match &self.0[1..3] {
b"AG" => "Ares",
b"A~" => "Ares",
b"AR" => "Arctic",
Expand Down Expand Up @@ -211,9 +222,9 @@ impl Serialize for PeerId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer, {
let buff_size = self.0.as_bytes().len() * 2;
let buff_size = self.0.len() * 2;
let mut tmp: Vec<u8> = vec![0; buff_size];
binascii::bin2hex(&self.0.as_bytes(), &mut tmp).unwrap();
binascii::bin2hex(&self.0, &mut tmp).unwrap();
let id = std::str::from_utf8(&tmp).ok();

#[derive(Serialize)]
Expand Down
72 changes: 27 additions & 45 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,12 @@ pub enum TrackerServer {
pub struct UdpTrackerConfig {
pub enabled: bool,
pub bind_address: String,
pub announce_interval: u32,
}

#[derive(Serialize, Deserialize)]
pub struct HttpTrackerConfig {
pub enabled: bool,
pub bind_address: String,
pub on_reverse_proxy: bool,
pub announce_interval: u32,
pub ssl_enabled: bool,
pub ssl_bind_address: String,
#[serde(serialize_with = "none_as_empty_string")]
Expand Down Expand Up @@ -58,10 +55,11 @@ pub struct Configuration {
pub cleanup_interval: Option<u64>,
pub cleanup_peerless: bool,
pub external_ip: Option<String>,
pub udp_tracker: UdpTrackerConfig,
pub udp_tracker_ipv6: UdpTrackerConfig,
pub http_tracker: HttpTrackerConfig,
pub http_tracker_ipv6: HttpTrackerConfig,
pub announce_interval: u32,
pub peer_timeout: u32,
pub on_reverse_proxy: bool,
pub udp_trackers: Vec<UdpTrackerConfig>,
pub http_trackers: Vec<HttpTrackerConfig>,
pub http_api: HttpApiConfig,
}

Expand Down Expand Up @@ -130,55 +128,47 @@ impl Configuration {

impl Configuration {
pub fn default() -> Configuration {
Configuration {
let mut configuration = Configuration {
log_level: Option::from(String::from("info")),
mode: TrackerMode::PublicMode,
db_path: String::from("data.db"),
persistence: false,
cleanup_interval: Some(600),
cleanup_peerless: true,
external_ip: Some(String::from("0.0.0.0")),
udp_tracker: UdpTrackerConfig {
announce_interval: 120,
peer_timeout: 900,
on_reverse_proxy: false,
udp_trackers: Vec::new(),
http_trackers: Vec::new(),
http_api: HttpApiConfig {
enabled: true,
bind_address: String::from("0.0.0.0:6969"),
announce_interval: 120,
},
udp_tracker_ipv6: UdpTrackerConfig {
bind_address: String::from("127.0.0.1:1212"),
access_tokens: [(String::from("admin"), String::from("MyAccessToken"))].iter().cloned().collect(),
}
};
configuration.udp_trackers.push(
UdpTrackerConfig{
enabled: false,
bind_address: String::from("[::]:6969"),
announce_interval: 120,
},
http_tracker: HttpTrackerConfig {
bind_address: String::from("0.0.0.0:6969")
}
);
configuration.http_trackers.push(
HttpTrackerConfig{
enabled: false,
bind_address: String::from("0.0.0.0:6969"),
on_reverse_proxy: false,
announce_interval: 120,
ssl_enabled: false,
ssl_bind_address: String::from("0.0.0.0:6868"),
ssl_cert_path: None,
ssl_key_path: None
},
http_tracker_ipv6: HttpTrackerConfig {
enabled: false,
bind_address: String::from("[::]:6969"),
on_reverse_proxy: false,
announce_interval: 120,
ssl_enabled: false,
ssl_bind_address: String::from("[::]:6868"),
ssl_cert_path: None,
ssl_key_path: None
},
http_api: HttpApiConfig {
enabled: true,
bind_address: String::from("127.0.0.1:1212"),
access_tokens: [(String::from("admin"), String::from("MyAccessToken"))].iter().cloned().collect(),
},
}
}
);
configuration
}

pub fn verify(&self) -> Result<(), ConfigurationError> {
// UDP is not secure for sending private keys
if (self.mode == TrackerMode::PrivateMode || self.mode == TrackerMode::PrivateListedMode) && self.get_tracker_server() == TrackerServer::UDP {
if self.mode == TrackerMode::PrivateMode || self.mode == TrackerMode::PrivateListedMode {
return Err(ConfigurationError::TrackerModeIncompatible)
}

Expand Down Expand Up @@ -213,12 +203,4 @@ impl Configuration {
fs::write("config.toml", toml_string).expect("Could not write to file!");
Ok(())
}

pub fn get_tracker_server(&self) -> TrackerServer {
if self.http_tracker.enabled {
TrackerServer::HTTP
} else {
TrackerServer::UDP
}
}
}
84 changes: 82 additions & 2 deletions src/http_api_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ struct Torrent<'a> {
peers: Option<Vec<TorrentPeer>>,
}

#[derive(Serialize)]
struct Stats {
torrents: u32,
seeders: u32,
completed: u32,
leechers: u32,
tcp4_connections_handled: u32,
tcp4_announces_handled: u32,
tcp4_scrapes_handled: u32,
tcp6_connections_handled: u32,
tcp6_announces_handled: u32,
tcp6_scrapes_handled: u32,
udp4_connections_handled: u32,
udp4_announces_handled: u32,
udp4_scrapes_handled: u32,
udp6_connections_handled: u32,
udp6_announces_handled: u32,
udp6_scrapes_handled: u32,
}

#[derive(Serialize, Debug)]
#[serde(tag = "status", rename_all = "snake_case")]
enum ActionStatus<'a> {
Expand Down Expand Up @@ -64,13 +84,13 @@ fn authenticate(tokens: HashMap<String, String>) -> impl Filter<Extract = (), Er
pub fn build_server(tracker: Arc<TorrentTracker>) -> Server<impl Filter<Extract = impl Reply> + Clone + Send + Sync + 'static> {
// GET /api/torrents?offset=:u32&limit=:u32
// View torrent list
let t1 = tracker.clone();
let api_torrents = tracker.clone();
let view_torrent_list = filters::method::get()
.and(filters::path::path("torrents"))
.and(filters::path::end())
.and(filters::query::query())
.map(move |limits| {
let tracker = t1.clone();
let tracker = api_torrents.clone();
(limits, tracker)
})
.and_then(|(limits, tracker): (TorrentInfoQuery, Arc<TorrentTracker>)| {
Expand Down Expand Up @@ -99,6 +119,65 @@ pub fn build_server(tracker: Arc<TorrentTracker>) -> Server<impl Filter<Extract
}
});

// GET /api/stats
// View tracker status
let api_stats = tracker.clone();
let view_stats_list = filters::method::get()
.and(filters::path::path("stats"))
.and(filters::path::end())
.map(move || {
let tracker = api_stats.clone();
tracker
})
.and_then(|tracker: Arc<TorrentTracker>| {
async move {
let mut results = Stats{
torrents: 0,
seeders: 0,
completed: 0,
leechers: 0,
tcp4_connections_handled: 0,
tcp4_announces_handled: 0,
tcp4_scrapes_handled: 0,
tcp6_connections_handled: 0,
tcp6_announces_handled: 0,
tcp6_scrapes_handled: 0,
udp4_connections_handled: 0,
udp4_announces_handled: 0,
udp4_scrapes_handled: 0,
udp6_connections_handled: 0,
udp6_announces_handled: 0,
udp6_scrapes_handled: 0
};
let db = tracker.get_torrents().await;
let _: Vec<_> = db
.iter()
.map(|(_info_hash, torrent_entry)| {
let (seeders, completed, leechers) = torrent_entry.get_stats();
results.seeders += seeders;
results.completed += completed;
results.leechers += leechers;
results.torrents += 1;
})
.collect();
let stats = tracker.get_stats().await;
results.tcp4_connections_handled = stats.tcp4_connections_handled as u32;
results.tcp4_announces_handled = stats.tcp4_announces_handled as u32;
results.tcp4_scrapes_handled = stats.tcp4_scrapes_handled as u32;
results.tcp6_connections_handled = stats.tcp6_connections_handled as u32;
results.tcp6_announces_handled = stats.tcp6_announces_handled as u32;
results.tcp6_scrapes_handled = stats.tcp6_scrapes_handled as u32;
results.udp4_connections_handled = stats.udp4_connections_handled as u32;
results.udp4_announces_handled = stats.udp4_announces_handled as u32;
results.udp4_scrapes_handled = stats.udp4_scrapes_handled as u32;
results.udp6_connections_handled = stats.udp6_connections_handled as u32;
results.udp6_announces_handled = stats.udp6_announces_handled as u32;
results.udp6_scrapes_handled = stats.udp6_scrapes_handled as u32;

Result::<_, warp::reject::Rejection>::Ok(reply::json(&results))
}
});

// GET /api/torrent/:info_hash
// View torrent info
let t2 = tracker.clone();
Expand Down Expand Up @@ -219,6 +298,7 @@ pub fn build_server(tracker: Arc<TorrentTracker>) -> Server<impl Filter<Extract
.and(view_torrent_list
.or(delete_torrent)
.or(view_torrent_info)
.or(view_stats_list)
.or(add_torrent)
.or(create_key)
.or(delete_key)
Expand Down
Loading