Skip to content

Commit

Permalink
test: [#278] allow using non canonical info-hash to download a torrent
Browse files Browse the repository at this point in the history
  • Loading branch information
josecelano committed Sep 13, 2023
1 parent 05b67c7 commit 2a73f10
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 4 deletions.
46 changes: 42 additions & 4 deletions src/web/api/v1/contexts/torrent/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::sync::Arc;
use axum::extract::{self, Multipart, Path, Query, State};
use axum::response::{IntoResponse, Redirect, Response};
use axum::Json;
use log::debug;
use serde::Deserialize;
use uuid::Uuid;

Expand All @@ -23,6 +24,7 @@ use crate::utils::parse_torrent;
use crate::web::api::v1::auth::get_optional_logged_in_user;
use crate::web::api::v1::extractors::bearer_token::Extract;
use crate::web::api::v1::responses::OkResponseData;
use crate::web::api::v1::routes::API_VERSION_URL_PREFIX;

/// Upload a new torrent file to the Index
///
Expand Down Expand Up @@ -78,7 +80,10 @@ pub async fn download_torrent_handler(
return errors::Request::InvalidInfoHashParam.into_response();
};

if let Some(redirect_response) = guard_that_canonical_info_hash_is_used_or_redirect(&app_data, &info_hash).await {
debug!("Downloading torrent: {:?}", info_hash.to_hex_string());

if let Some(redirect_response) = redirect_to_download_url_using_canonical_info_hash_if_needed(&app_data, &info_hash).await {
debug!("Redirecting to URL with canonical info-hash");
redirect_response
} else {
let opt_user_id = match get_optional_logged_in_user(maybe_bearer_token, app_data.clone()).await {
Expand All @@ -99,6 +104,32 @@ pub async fn download_torrent_handler(
}
}

async fn redirect_to_download_url_using_canonical_info_hash_if_needed(
app_data: &Arc<AppData>,
info_hash: &InfoHash,
) -> Option<Response> {
match app_data
.torrent_info_hash_repository
.find_canonical_info_hash_for(info_hash)
.await
{
Ok(Some(canonical_info_hash)) => {
if canonical_info_hash != *info_hash {
return Some(
Redirect::temporary(&format!(
"/{API_VERSION_URL_PREFIX}/torrent/download/{}",
canonical_info_hash.to_hex_string()
))
.into_response(),
);
}
None
}
Ok(None) => None,
Err(error) => Some(error.into_response()),
}
}

/// It returns a list of torrents matching the search criteria.
///
/// Eg: `/torrents?categories=music,other,movie&search=bunny&sort=size_DESC`
Expand Down Expand Up @@ -132,7 +163,7 @@ pub async fn get_torrent_info_handler(
return errors::Request::InvalidInfoHashParam.into_response();
};

if let Some(redirect_response) = guard_that_canonical_info_hash_is_used_or_redirect(&app_data, &info_hash).await {
if let Some(redirect_response) = redirect_to_details_url_using_canonical_info_hash_if_needed(&app_data, &info_hash).await {
redirect_response
} else {
let opt_user_id = match get_optional_logged_in_user(maybe_bearer_token, app_data.clone()).await {
Expand All @@ -147,7 +178,10 @@ pub async fn get_torrent_info_handler(
}
}

async fn guard_that_canonical_info_hash_is_used_or_redirect(app_data: &Arc<AppData>, info_hash: &InfoHash) -> Option<Response> {
async fn redirect_to_details_url_using_canonical_info_hash_if_needed(
app_data: &Arc<AppData>,
info_hash: &InfoHash,
) -> Option<Response> {
match app_data
.torrent_info_hash_repository
.find_canonical_info_hash_for(info_hash)
Expand All @@ -156,7 +190,11 @@ async fn guard_that_canonical_info_hash_is_used_or_redirect(app_data: &Arc<AppDa
Ok(Some(canonical_info_hash)) => {
if canonical_info_hash != *info_hash {
return Some(
Redirect::temporary(&format!("/v1/torrent/{}", canonical_info_hash.to_hex_string())).into_response(),
Redirect::temporary(&format!(
"/{API_VERSION_URL_PREFIX}/torrent/{}",
canonical_info_hash.to_hex_string()
))
.into_response(),
);
}
None
Expand Down
4 changes: 4 additions & 0 deletions tests/common/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ impl Http {
.await
.unwrap(),
};
// todo: If the response is a JSON, it returns the JSON body in a byte
// array. This is not the expected behavior.
// - Rename BinaryResponse to BinaryTorrentResponse
// - Return an error if the response is not a bittorrent file
BinaryResponse::from(response).await
}

Expand Down
46 changes: 46 additions & 0 deletions tests/e2e/web/api/v1/contexts/torrent/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Get torrent info:

mod for_guests {

use torrust_index_backend::utils::parse_torrent::decode_torrent;
use torrust_index_backend::web::api;
use uuid::Uuid;

Expand Down Expand Up @@ -234,13 +235,15 @@ mod for_guests {
let title = format!("title-{id}");
let file_contents = "data".to_string();

// Upload the first torrent
let mut first_torrent = TestTorrent::with_custom_info_dict_field(id, &file_contents, "custom 01");
first_torrent.index_info.title = title.clone();

let first_torrent_canonical_info_hash = upload_test_torrent(&client, &first_torrent)
.await
.expect("first torrent should be uploaded");

// Upload the second torrent with the same canonical info-hash
let mut second_torrent = TestTorrent::with_custom_info_dict_field(id, &file_contents, "custom 02");
second_torrent.index_info.title = format!("{title}-clone");

Expand Down Expand Up @@ -350,6 +353,49 @@ mod for_guests {
}
}

#[tokio::test]
async fn it_should_allow_guests_to_download_a_torrent_using_a_non_canonical_info_hash() {
let mut env = TestEnv::new();
env.start(api::Version::V1).await;

if !env.provides_a_tracker() {
println!("test skipped. It requires a tracker to be running.");
return;
}

let uploader = new_logged_in_user(&env).await;
let client = Client::authenticated(&env.server_socket_addr().unwrap(), &uploader.token);

// Sample data needed to build two torrents with the same canonical info-hash.
// Those torrents belong to the same Canonical Infohash Group.
let id = Uuid::new_v4();
let title = format!("title-{id}");
let file_contents = "data".to_string();

// Upload the first torrent
let mut first_torrent = TestTorrent::with_custom_info_dict_field(id, &file_contents, "custom 01");
first_torrent.index_info.title = title.clone();

let first_torrent_canonical_info_hash = upload_test_torrent(&client, &first_torrent)
.await
.expect("first torrent should be uploaded");

// Upload the second torrent with the same canonical info-hash
let mut second_torrent = TestTorrent::with_custom_info_dict_field(id, &file_contents, "custom 02");
second_torrent.index_info.title = format!("{title}-clone");

let _result = upload_test_torrent(&client, &second_torrent).await;

// Download the torrent using the non-canonical info-hash (second torrent info-hash)
let response = client.download_torrent(&second_torrent.file_info_hash()).await;

let torrent = decode_torrent(&response.bytes).expect("could not decode downloaded torrent");

// The returned torrent info-hash should be the same as the first torrent
assert_eq!(response.status, 200);
assert_eq!(torrent.info_hash_hex(), first_torrent_canonical_info_hash.to_hex_string());
}

#[tokio::test]
async fn it_should_return_a_not_found_response_trying_to_get_the_torrent_info_for_a_non_existing_torrent() {
let mut env = TestEnv::new();
Expand Down

0 comments on commit 2a73f10

Please sign in to comment.