Skip to content

Commit

Permalink
feat!: [torrust#276] upload torrent returns 400 instead of empty resp…
Browse files Browse the repository at this point in the history
…onse

when the original info-hash of the upload torrent cannot be calculated.

The API should not return empty responses, so "panic" should not be use
for common problems that we should notify to the user.
  • Loading branch information
josecelano committed Sep 20, 2023
1 parent 329485f commit 4cc97c7
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 24 deletions.
12 changes: 7 additions & 5 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use hyper::StatusCode;

use crate::databases::database;
use crate::models::torrent::MetadataError;
use crate::utils::parse_torrent::MetainfoFileDataError;
use crate::utils::parse_torrent::DecodeTorrentFileError;

pub type ServiceResult<V> = Result<V, ServiceError>;

Expand Down Expand Up @@ -216,12 +216,14 @@ impl From<MetadataError> for ServiceError {
}
}

impl From<MetainfoFileDataError> for ServiceError {
fn from(e: MetainfoFileDataError) -> Self {
impl From<DecodeTorrentFileError> for ServiceError {
fn from(e: DecodeTorrentFileError) -> Self {
eprintln!("{e}");
match e {
MetainfoFileDataError::InvalidBencodeData => ServiceError::InvalidTorrentFile,
MetainfoFileDataError::InvalidTorrentPiecesLength => ServiceError::InvalidTorrentTitleLength,
DecodeTorrentFileError::InvalidTorrentPiecesLength => ServiceError::InvalidTorrentTitleLength,
DecodeTorrentFileError::CannotBencodeInfoDict
| DecodeTorrentFileError::InvalidInfoDictionary
| DecodeTorrentFileError::InvalidBencodeData => ServiceError::InvalidTorrentFile,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ impl Index {
add_torrent_req: AddTorrentRequest,
user_id: UserId,
) -> Result<AddTorrentResponse, ServiceError> {
// Authorization: only authenticated users ere allowed to upload torrents
// Guard that the users exists
let _user = self.user_repository.get_compact(&user_id).await?;

let metadata = self.validate_and_build_metadata(&add_torrent_req).await?;
Expand Down
40 changes: 25 additions & 15 deletions src/utils/parse_torrent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ use crate::models::info_hash::InfoHash;
use crate::models::torrent_file::Torrent;

#[derive(Debug, Display, PartialEq, Eq, Error)]
pub enum MetainfoFileDataError {
pub enum DecodeTorrentFileError {
#[display(fmt = "Torrent data could not be decoded from the bencoded format.")]
InvalidBencodeData,

#[display(fmt = "Torrent info dictionary key could not be decoded from the bencoded format.")]
InvalidInfoDictionary,

#[display(fmt = "Torrent has an invalid pieces key length. It should be a multiple of 20.")]
InvalidTorrentPiecesLength,

#[display(fmt = "Cannot bencode the parsed `info` dictionary again to generate the info-hash.")]
CannotBencodeInfoDict,
}

/// It decodes and validate an array of bytes containing a torrent file.
Expand All @@ -31,15 +37,15 @@ pub enum MetainfoFileDataError {
///
/// - The torrent file is not a valid bencoded file.
/// - The pieces key has a length that is not a multiple of 20.
pub fn decode_and_validate_torrent_file(bytes: &[u8]) -> Result<(Torrent, InfoHash), MetainfoFileDataError> {
let original_info_hash = calculate_info_hash(bytes);
pub fn decode_and_validate_torrent_file(bytes: &[u8]) -> Result<(Torrent, InfoHash), DecodeTorrentFileError> {
let original_info_hash = calculate_info_hash(bytes)?;

let torrent = decode_torrent(bytes).map_err(|_| MetainfoFileDataError::InvalidBencodeData)?;
let torrent = decode_torrent(bytes).map_err(|_| DecodeTorrentFileError::InvalidBencodeData)?;

// Make sure that the pieces key has a length that is a multiple of 20
if let Some(pieces) = torrent.info.pieces.as_ref() {
if pieces.as_ref().len() % 20 != 0 {
return Err(MetainfoFileDataError::InvalidTorrentPiecesLength);
return Err(DecodeTorrentFileError::InvalidTorrentPiecesLength);
}
}

Expand Down Expand Up @@ -77,30 +83,34 @@ pub fn encode_torrent(torrent: &Torrent) -> Result<Vec<u8>, Error> {
}

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct MetainfoFile {
struct ParsedInfoDictFromMetainfoFile {
pub info: Value,
}

/// Calculates the `InfoHash` from a the torrent file binary data.
///
/// # Panics
/// # Errors
///
/// This function will return an error if:
///
/// This function will panic if the torrent file is not a valid bencoded file
/// or if the info dictionary cannot be bencoded.
#[must_use]
pub fn calculate_info_hash(bytes: &[u8]) -> InfoHash {
/// - The torrent file is not a valid bencoded torrent file containing an `info`
/// dictionary key.
/// - The original torrent info-hash cannot be bencoded from the parsed `info`
/// dictionary is not a valid bencoded dictionary.
pub fn calculate_info_hash(bytes: &[u8]) -> Result<InfoHash, DecodeTorrentFileError> {
// Extract the info dictionary
let metainfo: MetainfoFile = serde_bencode::from_bytes(bytes).expect("Torrent file cannot be parsed from bencoded format");
let metainfo: ParsedInfoDictFromMetainfoFile =
serde_bencode::from_bytes(bytes).map_err(|_| DecodeTorrentFileError::InvalidInfoDictionary)?;

// Bencode the info dictionary
let info_dict_bytes = serde_bencode::to_bytes(&metainfo.info).expect("Info dictionary cannot by bencoded");
let info_dict_bytes = serde_bencode::to_bytes(&metainfo.info).map_err(|_| DecodeTorrentFileError::CannotBencodeInfoDict)?;

// Calculate the SHA-1 hash of the bencoded info dictionary
let mut hasher = Sha1::new();
hasher.update(&info_dict_bytes);
let result = hasher.finalize();

InfoHash::from_bytes(&result)
Ok(InfoHash::from_bytes(&result))
}

#[cfg(test)]
Expand All @@ -117,7 +127,7 @@ mod tests {
"tests/fixtures/torrents/6c690018c5786dbbb00161f62b0712d69296df97_with_custom_info_dict_key.torrent",
);

let original_info_hash = super::calculate_info_hash(&std::fs::read(torrent_path).unwrap());
let original_info_hash = super::calculate_info_hash(&std::fs::read(torrent_path).unwrap()).unwrap();

assert_eq!(
original_info_hash,
Expand Down
8 changes: 5 additions & 3 deletions tests/e2e/web/api/v1/contexts/torrent/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ mod for_guests {
// Download
let response = client.download_torrent(&test_torrent.file_info_hash()).await;

let downloaded_torrent_info_hash = calculate_info_hash(&response.bytes);
let downloaded_torrent_info_hash =
calculate_info_hash(&response.bytes).expect("failed to calculate info-hash of the downloaded torrent");

assert_eq!(
downloaded_torrent_info_hash.to_hex_string(),
Expand Down Expand Up @@ -598,7 +599,6 @@ mod for_authenticated_users {
use crate::e2e::web::api::v1::contexts::user::steps::new_logged_in_user;

#[tokio::test]
#[should_panic]
async fn contains_a_bencoded_dictionary_with_the_info_key_in_order_to_calculate_the_original_info_hash() {
let mut env = TestEnv::new();
env.start(api::Version::V1).await;
Expand All @@ -614,7 +614,9 @@ mod for_authenticated_users {

let form: UploadTorrentMultipartForm = test_torrent.index_info.into();

let _response = client.upload_torrent(form.into()).await;
let response = client.upload_torrent(form.into()).await;

assert_eq!(response.status, 400);
}

#[tokio::test]
Expand Down

0 comments on commit 4cc97c7

Please sign in to comment.