Skip to content

Commit b23d64b

Browse files
committed
feat: add ssl support for the API
New config options have been added to support HTTPs conenctionto the API: ``` [http_api] ssl_enabled = false ssl_cert_path = "./storage/ssl_certificates/localhost.crt" ssl_key_path = "./storage/ssl_certificates/localhost.key" ```
1 parent 5af28a2 commit b23d64b

File tree

9 files changed

+393
-344
lines changed

9 files changed

+393
-344
lines changed

src/api/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,20 @@
11
pub mod resource;
2+
pub mod routes;
23
pub mod server;
4+
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Deserialize, Debug)]
8+
pub struct TorrentInfoQuery {
9+
offset: Option<u32>,
10+
limit: Option<u32>,
11+
}
12+
13+
#[derive(Serialize, Debug)]
14+
#[serde(tag = "status", rename_all = "snake_case")]
15+
enum ActionStatus<'a> {
16+
Ok,
17+
Err { reason: std::borrow::Cow<'a, str> },
18+
}
19+
20+
impl warp::reject::Reject for ActionStatus<'static> {}

src/api/routes.rs

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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

Comments
 (0)