|
| 1 | +use std::cmp::min; |
| 2 | +use std::collections::{HashMap, HashSet}; |
| 3 | +use std::sync::Arc; |
| 4 | +use std::time::Duration; |
| 5 | + |
| 6 | +use serde::Deserialize; |
| 7 | +use warp::{filters, reply, Filter}; |
| 8 | + |
| 9 | +use super::resource::auth_key::AuthKey; |
| 10 | +use super::resource::peer; |
| 11 | +use super::resource::stats::Stats; |
| 12 | +use super::resource::torrent::{ListItem, Torrent}; |
| 13 | +use super::{ActionStatus, TorrentInfoQuery}; |
| 14 | +use crate::protocol::info_hash::InfoHash; |
| 15 | +use crate::tracker; |
| 16 | + |
| 17 | +fn authenticate(tokens: HashMap<String, String>) -> impl Filter<Extract = (), Error = warp::reject::Rejection> + Clone { |
| 18 | + #[derive(Deserialize)] |
| 19 | + struct AuthToken { |
| 20 | + token: Option<String>, |
| 21 | + } |
| 22 | + |
| 23 | + let tokens: HashSet<String> = tokens.into_values().collect(); |
| 24 | + |
| 25 | + let tokens = Arc::new(tokens); |
| 26 | + warp::filters::any::any() |
| 27 | + .map(move || tokens.clone()) |
| 28 | + .and(filters::query::query::<AuthToken>()) |
| 29 | + .and_then(|tokens: Arc<HashSet<String>>, token: AuthToken| async move { |
| 30 | + match token.token { |
| 31 | + Some(token) => { |
| 32 | + if !tokens.contains(&token) { |
| 33 | + return Err(warp::reject::custom(ActionStatus::Err { |
| 34 | + reason: "token not valid".into(), |
| 35 | + })); |
| 36 | + } |
| 37 | + |
| 38 | + Ok(()) |
| 39 | + } |
| 40 | + None => Err(warp::reject::custom(ActionStatus::Err { |
| 41 | + reason: "unauthorized".into(), |
| 42 | + })), |
| 43 | + } |
| 44 | + }) |
| 45 | + .untuple_one() |
| 46 | +} |
| 47 | + |
| 48 | +#[allow(clippy::too_many_lines)] |
| 49 | +#[must_use] |
| 50 | +pub fn routes(tracker: &Arc<tracker::Tracker>) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone { |
| 51 | + // GET /api/torrents?offset=:u32&limit=:u32 |
| 52 | + // View torrent list |
| 53 | + let api_torrents = tracker.clone(); |
| 54 | + let view_torrent_list = filters::method::get() |
| 55 | + .and(filters::path::path("torrents")) |
| 56 | + .and(filters::path::end()) |
| 57 | + .and(filters::query::query()) |
| 58 | + .map(move |limits| { |
| 59 | + let tracker = api_torrents.clone(); |
| 60 | + (limits, tracker) |
| 61 | + }) |
| 62 | + .and_then(|(limits, tracker): (TorrentInfoQuery, Arc<tracker::Tracker>)| async move { |
| 63 | + let offset = limits.offset.unwrap_or(0); |
| 64 | + let limit = min(limits.limit.unwrap_or(1000), 4000); |
| 65 | + |
| 66 | + let db = tracker.get_torrents().await; |
| 67 | + let results: Vec<_> = db |
| 68 | + .iter() |
| 69 | + .map(|(info_hash, torrent_entry)| { |
| 70 | + let (seeders, completed, leechers) = torrent_entry.get_stats(); |
| 71 | + ListItem { |
| 72 | + info_hash: info_hash.to_string(), |
| 73 | + seeders, |
| 74 | + completed, |
| 75 | + leechers, |
| 76 | + peers: None, |
| 77 | + } |
| 78 | + }) |
| 79 | + .skip(offset as usize) |
| 80 | + .take(limit as usize) |
| 81 | + .collect(); |
| 82 | + |
| 83 | + Result::<_, warp::reject::Rejection>::Ok(reply::json(&results)) |
| 84 | + }); |
| 85 | + |
| 86 | + // GET /api/stats |
| 87 | + // View tracker status |
| 88 | + let api_stats = tracker.clone(); |
| 89 | + let view_stats_list = filters::method::get() |
| 90 | + .and(filters::path::path("stats")) |
| 91 | + .and(filters::path::end()) |
| 92 | + .map(move || api_stats.clone()) |
| 93 | + .and_then(|tracker: Arc<tracker::Tracker>| async move { |
| 94 | + let mut results = Stats { |
| 95 | + torrents: 0, |
| 96 | + seeders: 0, |
| 97 | + completed: 0, |
| 98 | + leechers: 0, |
| 99 | + tcp4_connections_handled: 0, |
| 100 | + tcp4_announces_handled: 0, |
| 101 | + tcp4_scrapes_handled: 0, |
| 102 | + tcp6_connections_handled: 0, |
| 103 | + tcp6_announces_handled: 0, |
| 104 | + tcp6_scrapes_handled: 0, |
| 105 | + udp4_connections_handled: 0, |
| 106 | + udp4_announces_handled: 0, |
| 107 | + udp4_scrapes_handled: 0, |
| 108 | + udp6_connections_handled: 0, |
| 109 | + udp6_announces_handled: 0, |
| 110 | + udp6_scrapes_handled: 0, |
| 111 | + }; |
| 112 | + |
| 113 | + let db = tracker.get_torrents().await; |
| 114 | + |
| 115 | + db.values().for_each(|torrent_entry| { |
| 116 | + let (seeders, completed, leechers) = torrent_entry.get_stats(); |
| 117 | + results.seeders += seeders; |
| 118 | + results.completed += completed; |
| 119 | + results.leechers += leechers; |
| 120 | + results.torrents += 1; |
| 121 | + }); |
| 122 | + |
| 123 | + let stats = tracker.get_stats().await; |
| 124 | + |
| 125 | + #[allow(clippy::cast_possible_truncation)] |
| 126 | + { |
| 127 | + results.tcp4_connections_handled = stats.tcp4_connections_handled as u32; |
| 128 | + results.tcp4_announces_handled = stats.tcp4_announces_handled as u32; |
| 129 | + results.tcp4_scrapes_handled = stats.tcp4_scrapes_handled as u32; |
| 130 | + results.tcp6_connections_handled = stats.tcp6_connections_handled as u32; |
| 131 | + results.tcp6_announces_handled = stats.tcp6_announces_handled as u32; |
| 132 | + results.tcp6_scrapes_handled = stats.tcp6_scrapes_handled as u32; |
| 133 | + results.udp4_connections_handled = stats.udp4_connections_handled as u32; |
| 134 | + results.udp4_announces_handled = stats.udp4_announces_handled as u32; |
| 135 | + results.udp4_scrapes_handled = stats.udp4_scrapes_handled as u32; |
| 136 | + results.udp6_connections_handled = stats.udp6_connections_handled as u32; |
| 137 | + results.udp6_announces_handled = stats.udp6_announces_handled as u32; |
| 138 | + results.udp6_scrapes_handled = stats.udp6_scrapes_handled as u32; |
| 139 | + } |
| 140 | + |
| 141 | + Result::<_, warp::reject::Rejection>::Ok(reply::json(&results)) |
| 142 | + }); |
| 143 | + |
| 144 | + // GET /api/torrent/:info_hash |
| 145 | + // View torrent info |
| 146 | + let t2 = tracker.clone(); |
| 147 | + let view_torrent_info = filters::method::get() |
| 148 | + .and(filters::path::path("torrent")) |
| 149 | + .and(filters::path::param()) |
| 150 | + .and(filters::path::end()) |
| 151 | + .map(move |info_hash: InfoHash| { |
| 152 | + let tracker = t2.clone(); |
| 153 | + (info_hash, tracker) |
| 154 | + }) |
| 155 | + .and_then(|(info_hash, tracker): (InfoHash, Arc<tracker::Tracker>)| async move { |
| 156 | + let db = tracker.get_torrents().await; |
| 157 | + let torrent_entry_option = db.get(&info_hash); |
| 158 | + |
| 159 | + let torrent_entry = match torrent_entry_option { |
| 160 | + Some(torrent_entry) => torrent_entry, |
| 161 | + None => { |
| 162 | + return Result::<_, warp::reject::Rejection>::Ok(reply::json(&"torrent not known")); |
| 163 | + } |
| 164 | + }; |
| 165 | + let (seeders, completed, leechers) = torrent_entry.get_stats(); |
| 166 | + |
| 167 | + let peers = torrent_entry.get_peers(None); |
| 168 | + |
| 169 | + let peer_resources = peers.iter().map(|peer| peer::Peer::from(**peer)).collect(); |
| 170 | + |
| 171 | + Ok(reply::json(&Torrent { |
| 172 | + info_hash: info_hash.to_string(), |
| 173 | + seeders, |
| 174 | + completed, |
| 175 | + leechers, |
| 176 | + peers: Some(peer_resources), |
| 177 | + })) |
| 178 | + }); |
| 179 | + |
| 180 | + // DELETE /api/whitelist/:info_hash |
| 181 | + // Delete info hash from whitelist |
| 182 | + let t3 = tracker.clone(); |
| 183 | + let delete_torrent = filters::method::delete() |
| 184 | + .and(filters::path::path("whitelist")) |
| 185 | + .and(filters::path::param()) |
| 186 | + .and(filters::path::end()) |
| 187 | + .map(move |info_hash: InfoHash| { |
| 188 | + let tracker = t3.clone(); |
| 189 | + (info_hash, tracker) |
| 190 | + }) |
| 191 | + .and_then(|(info_hash, tracker): (InfoHash, Arc<tracker::Tracker>)| async move { |
| 192 | + match tracker.remove_torrent_from_whitelist(&info_hash).await { |
| 193 | + Ok(_) => Ok(warp::reply::json(&ActionStatus::Ok)), |
| 194 | + Err(_) => Err(warp::reject::custom(ActionStatus::Err { |
| 195 | + reason: "failed to remove torrent from whitelist".into(), |
| 196 | + })), |
| 197 | + } |
| 198 | + }); |
| 199 | + |
| 200 | + // POST /api/whitelist/:info_hash |
| 201 | + // Add info hash to whitelist |
| 202 | + let t4 = tracker.clone(); |
| 203 | + let add_torrent = filters::method::post() |
| 204 | + .and(filters::path::path("whitelist")) |
| 205 | + .and(filters::path::param()) |
| 206 | + .and(filters::path::end()) |
| 207 | + .map(move |info_hash: InfoHash| { |
| 208 | + let tracker = t4.clone(); |
| 209 | + (info_hash, tracker) |
| 210 | + }) |
| 211 | + .and_then(|(info_hash, tracker): (InfoHash, Arc<tracker::Tracker>)| async move { |
| 212 | + match tracker.add_torrent_to_whitelist(&info_hash).await { |
| 213 | + Ok(..) => Ok(warp::reply::json(&ActionStatus::Ok)), |
| 214 | + Err(..) => Err(warp::reject::custom(ActionStatus::Err { |
| 215 | + reason: "failed to whitelist torrent".into(), |
| 216 | + })), |
| 217 | + } |
| 218 | + }); |
| 219 | + |
| 220 | + // POST /api/key/:seconds_valid |
| 221 | + // Generate new key |
| 222 | + let t5 = tracker.clone(); |
| 223 | + let create_key = filters::method::post() |
| 224 | + .and(filters::path::path("key")) |
| 225 | + .and(filters::path::param()) |
| 226 | + .and(filters::path::end()) |
| 227 | + .map(move |seconds_valid: u64| { |
| 228 | + let tracker = t5.clone(); |
| 229 | + (seconds_valid, tracker) |
| 230 | + }) |
| 231 | + .and_then(|(seconds_valid, tracker): (u64, Arc<tracker::Tracker>)| async move { |
| 232 | + match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await { |
| 233 | + Ok(auth_key) => Ok(warp::reply::json(&AuthKey::from(auth_key))), |
| 234 | + Err(..) => Err(warp::reject::custom(ActionStatus::Err { |
| 235 | + reason: "failed to generate key".into(), |
| 236 | + })), |
| 237 | + } |
| 238 | + }); |
| 239 | + |
| 240 | + // DELETE /api/key/:key |
| 241 | + // Delete key |
| 242 | + let t6 = tracker.clone(); |
| 243 | + let delete_key = filters::method::delete() |
| 244 | + .and(filters::path::path("key")) |
| 245 | + .and(filters::path::param()) |
| 246 | + .and(filters::path::end()) |
| 247 | + .map(move |key: String| { |
| 248 | + let tracker = t6.clone(); |
| 249 | + (key, tracker) |
| 250 | + }) |
| 251 | + .and_then(|(key, tracker): (String, Arc<tracker::Tracker>)| async move { |
| 252 | + match tracker.remove_auth_key(&key).await { |
| 253 | + Ok(_) => Ok(warp::reply::json(&ActionStatus::Ok)), |
| 254 | + Err(_) => Err(warp::reject::custom(ActionStatus::Err { |
| 255 | + reason: "failed to delete key".into(), |
| 256 | + })), |
| 257 | + } |
| 258 | + }); |
| 259 | + |
| 260 | + // GET /api/whitelist/reload |
| 261 | + // Reload whitelist |
| 262 | + let t7 = tracker.clone(); |
| 263 | + let reload_whitelist = filters::method::get() |
| 264 | + .and(filters::path::path("whitelist")) |
| 265 | + .and(filters::path::path("reload")) |
| 266 | + .and(filters::path::end()) |
| 267 | + .map(move || t7.clone()) |
| 268 | + .and_then(|tracker: Arc<tracker::Tracker>| async move { |
| 269 | + match tracker.load_whitelist().await { |
| 270 | + Ok(_) => Ok(warp::reply::json(&ActionStatus::Ok)), |
| 271 | + Err(_) => Err(warp::reject::custom(ActionStatus::Err { |
| 272 | + reason: "failed to reload whitelist".into(), |
| 273 | + })), |
| 274 | + } |
| 275 | + }); |
| 276 | + |
| 277 | + // GET /api/keys/reload |
| 278 | + // Reload whitelist |
| 279 | + let t8 = tracker.clone(); |
| 280 | + let reload_keys = filters::method::get() |
| 281 | + .and(filters::path::path("keys")) |
| 282 | + .and(filters::path::path("reload")) |
| 283 | + .and(filters::path::end()) |
| 284 | + .map(move || t8.clone()) |
| 285 | + .and_then(|tracker: Arc<tracker::Tracker>| async move { |
| 286 | + match tracker.load_keys().await { |
| 287 | + Ok(_) => Ok(warp::reply::json(&ActionStatus::Ok)), |
| 288 | + Err(_) => Err(warp::reject::custom(ActionStatus::Err { |
| 289 | + reason: "failed to reload keys".into(), |
| 290 | + })), |
| 291 | + } |
| 292 | + }); |
| 293 | + |
| 294 | + let api_routes = filters::path::path("api").and( |
| 295 | + view_torrent_list |
| 296 | + .or(delete_torrent) |
| 297 | + .or(view_torrent_info) |
| 298 | + .or(view_stats_list) |
| 299 | + .or(add_torrent) |
| 300 | + .or(create_key) |
| 301 | + .or(delete_key) |
| 302 | + .or(reload_whitelist) |
| 303 | + .or(reload_keys), |
| 304 | + ); |
| 305 | + |
| 306 | + api_routes.and(authenticate(tracker.config.http_api.access_tokens.clone())) |
| 307 | +} |
0 commit comments