Skip to content

Commit

Permalink
feat(api): [torrust#143] axum api. POST /api/whitelist/:info_hash end…
Browse files Browse the repository at this point in the history
…point
  • Loading branch information
josecelano committed Jan 11, 2023
1 parent e1ed929 commit 5c5fcbd
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 10 deletions.
41 changes: 40 additions & 1 deletion src/apis/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use std::str::FromStr;
use std::sync::Arc;

use axum::extract::{Path, Query, State};
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Json, Response};
use serde::{de, Deserialize, Deserializer};
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_json::json;

use crate::api::resource::stats::Stats;
Expand All @@ -14,6 +15,31 @@ use crate::tracker::services::statistics::get_metrics;
use crate::tracker::services::torrent::{get_torrent_info, get_torrents, Pagination};
use crate::tracker::Tracker;

#[derive(Serialize, Debug)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ActionStatus<'a> {
Ok,
Err { reason: std::borrow::Cow<'a, str> },
}

fn response_ok() -> Response {
(
StatusCode::OK,
[(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
format!("{:?}", ActionStatus::Ok),
)
.into_response()
}

fn response_err(reason: String) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
[(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
format!("Unhandled rejection: {:?}", ActionStatus::Err { reason: reason.into() }),
)
.into_response()
}

pub async fn get_stats_handler(State(tracker): State<Arc<Tracker>>) -> Json<Stats> {
Json(Stats::from(get_metrics(tracker.clone()).await))
}
Expand Down Expand Up @@ -50,6 +76,19 @@ pub async fn get_torrents_handler(
))
}

/// # Panics
///
/// Will panic if it can't parse the infohash in the request
pub async fn add_torrent_to_whitelist_handler(State(tracker): State<Arc<Tracker>>, Path(info_hash): Path<String>) -> Response {
match tracker
.add_torrent_to_whitelist(&InfoHash::from_str(&info_hash).unwrap())
.await
{
Ok(..) => response_ok(),
Err(..) => response_err("failed to whitelist torrent".to_string()),
}
}

/// Serde deserialization decorator to map empty Strings to None,
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
Expand Down
18 changes: 16 additions & 2 deletions src/apis/server.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::net::SocketAddr;
use std::sync::Arc;

use axum::routing::get;
use axum::routing::{get, post};
use axum::{middleware, Router};
use axum_server::tls_rustls::RustlsConfig;
use axum_server::Handle;
Expand All @@ -10,14 +10,21 @@ use log::info;
use warp::hyper;

use super::middlewares::auth::auth;
use super::routes::{get_stats_handler, get_torrent_handler, get_torrents_handler};
use super::routes::{add_torrent_to_whitelist_handler, get_stats_handler, get_torrent_handler, get_torrents_handler};
use crate::tracker;

pub fn start(socket_addr: SocketAddr, tracker: &Arc<tracker::Tracker>) -> impl Future<Output = hyper::Result<()>> {
let app = Router::new()
// Stats
.route("/stats", get(get_stats_handler).with_state(tracker.clone()))
// Torrents
.route("/torrent/:info_hash", get(get_torrent_handler).with_state(tracker.clone()))
.route("/torrents", get(get_torrents_handler).with_state(tracker.clone()))
// Whitelisted torrents
.route(
"/whitelist/:info_hash",
post(add_torrent_to_whitelist_handler).with_state(tracker.clone()),
)
.layer(middleware::from_fn_with_state(tracker.config.clone(), auth));

let server = axum::Server::bind(&socket_addr).serve(app.into_make_service());
Expand All @@ -34,9 +41,16 @@ pub fn start_tls(
tracker: &Arc<tracker::Tracker>,
) -> impl Future<Output = Result<(), std::io::Error>> {
let app = Router::new()
// Stats
.route("/stats", get(get_stats_handler).with_state(tracker.clone()))
// Torrents
.route("/torrent/:info_hash", get(get_torrent_handler).with_state(tracker.clone()))
.route("/torrents", get(get_torrents_handler).with_state(tracker.clone()))
// Whitelisted torrents
.route(
"/whitelist/:info_hash",
post(add_torrent_to_whitelist_handler).with_state(tracker.clone()),
)
.layer(middleware::from_fn_with_state(tracker.config.clone(), auth));

let handle = Handle::new();
Expand Down
94 changes: 87 additions & 7 deletions tests/tracker_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,36 +371,36 @@ mod tracker_api {
}

#[tokio::test]
async fn should_return_an_error_when_the_torrent_cannot_be_removed_from_the_whitelist() {
async fn should_allow_removing_a_torrent_from_the_whitelist() {
let api_server = start_default_api(&Version::Warp).await;

let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
let info_hash = InfoHash::from_str(&hash).unwrap();
api_server.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();

force_database_error(&api_server.tracker);

let response = Client::new(api_server.get_connection_info(), &Version::Warp)
.remove_torrent_from_whitelist(&hash)
.await;

assert_failed_to_remove_torrent_from_whitelist(response).await;
assert_eq!(response.status(), 200);
assert!(!api_server.tracker.is_info_hash_whitelisted(&info_hash).await);
}

#[tokio::test]
async fn should_allow_removing_a_torrent_from_the_whitelist() {
async fn should_return_an_error_when_the_torrent_cannot_be_removed_from_the_whitelist() {
let api_server = start_default_api(&Version::Warp).await;

let hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();
let info_hash = InfoHash::from_str(&hash).unwrap();
api_server.tracker.add_torrent_to_whitelist(&info_hash).await.unwrap();

force_database_error(&api_server.tracker);

let response = Client::new(api_server.get_connection_info(), &Version::Warp)
.remove_torrent_from_whitelist(&hash)
.await;

assert_eq!(response.status(), 200);
assert!(!api_server.tracker.is_info_hash_whitelisted(&info_hash).await);
assert_failed_to_remove_torrent_from_whitelist(response).await;
}

#[tokio::test]
Expand Down Expand Up @@ -943,4 +943,84 @@ mod tracker_apis {
assert_unauthorized(response).await;
}
}

mod for_whitelisted_torrent_resources {
use std::str::FromStr;

use torrust_tracker::protocol::info_hash::InfoHash;

use crate::api::asserts::{assert_failed_to_whitelist_torrent, assert_token_not_valid, assert_unauthorized};
use crate::api::client::Client;
use crate::api::connection_info::{connection_with_invalid_token, connection_with_no_token};
use crate::api::server::start_default_api;
use crate::api::{force_database_error, Version};

#[tokio::test]
async fn should_allow_whitelisting_a_torrent() {
let api_server = start_default_api(&Version::Axum).await;

let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();

let res = Client::new(api_server.get_connection_info(), &Version::Axum)
.whitelist_a_torrent(&info_hash)
.await;

assert_eq!(res.status(), 200);
assert!(
api_server
.tracker
.is_info_hash_whitelisted(&InfoHash::from_str(&info_hash).unwrap())
.await
);
}

#[tokio::test]
async fn should_allow_whitelisting_a_torrent_that_has_been_already_whitelisted() {
let api_server = start_default_api(&Version::Axum).await;

let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();

let api_client = Client::new(api_server.get_connection_info(), &Version::Axum);

let res = api_client.whitelist_a_torrent(&info_hash).await;
assert_eq!(res.status(), 200);

let res = api_client.whitelist_a_torrent(&info_hash).await;
assert_eq!(res.status(), 200);
}

#[tokio::test]
async fn should_not_allow_whitelisting_a_torrent_for_unauthenticated_users() {
let api_server = start_default_api(&Version::Axum).await;

let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();

let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address()), &Version::Axum)
.whitelist_a_torrent(&info_hash)
.await;

assert_token_not_valid(response).await;

let response = Client::new(connection_with_no_token(&api_server.get_bind_address()), &Version::Axum)
.whitelist_a_torrent(&info_hash)
.await;

assert_unauthorized(response).await;
}

#[tokio::test]
async fn should_return_an_error_when_the_torrent_cannot_be_whitelisted() {
let api_server = start_default_api(&Version::Axum).await;

let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned();

force_database_error(&api_server.tracker);

let response = Client::new(api_server.get_connection_info(), &Version::Axum)
.whitelist_a_torrent(&info_hash)
.await;

assert_failed_to_whitelist_torrent(response).await;
}
}
}

0 comments on commit 5c5fcbd

Please sign in to comment.