Skip to content

Commit 2cf80bc

Browse files
committed
Merge #259: Improve Bad Request error message for uploading
ce50f26 feat: [#257] improve bad request error message for uploading (Jose Celano) Pull request description: Add a custom error for each case so that the Bad Request response constains a descriptive error message. Top commit has no ACKs. Tree-SHA512: 70659460ad69a694da21e66416bb51d2a42b743f6ed16a0d15fb460197b98b71921de740b707a50e73ce9e0d7239f22e92cd252b97a87af0bf3975d4043dd757
2 parents 022e2c5 + ce50f26 commit 2cf80bc

File tree

8 files changed

+154
-80
lines changed

8 files changed

+154
-80
lines changed

src/errors.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ pub enum ServiceError {
9797
#[display(fmt = "Torrent title is too short.")]
9898
InvalidTorrentTitleLength,
9999

100-
#[display(fmt = "Bad request.")]
101-
BadRequest,
100+
#[display(fmt = "Some mandatory metadata fields are missing.")]
101+
MissingMandatoryMetadataFields,
102102

103103
#[display(fmt = "Selected category does not exist.")]
104104
InvalidCategory,
@@ -223,7 +223,7 @@ pub fn http_status_code_for_service_error(error: &ServiceError) -> StatusCode {
223223
ServiceError::InvalidTorrentPiecesLength => StatusCode::BAD_REQUEST,
224224
ServiceError::InvalidFileType => StatusCode::BAD_REQUEST,
225225
ServiceError::InvalidTorrentTitleLength => StatusCode::BAD_REQUEST,
226-
ServiceError::BadRequest => StatusCode::BAD_REQUEST,
226+
ServiceError::MissingMandatoryMetadataFields => StatusCode::BAD_REQUEST,
227227
ServiceError::InvalidCategory => StatusCode::BAD_REQUEST,
228228
ServiceError::InvalidTag => StatusCode::BAD_REQUEST,
229229
ServiceError::Unauthorized => StatusCode::FORBIDDEN,

src/models/torrent.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize};
22

33
use super::torrent_tag::TagId;
44
use crate::errors::ServiceError;
5-
use crate::models::torrent_file::Torrent;
65

76
#[allow(clippy::module_name_repetitions)]
87
pub type TorrentId = i64;
@@ -23,13 +22,6 @@ pub struct TorrentListing {
2322
pub leechers: i64,
2423
}
2524

26-
#[allow(clippy::module_name_repetitions)]
27-
#[derive(Debug)]
28-
pub struct AddTorrentRequest {
29-
pub metadata: Metadata,
30-
pub torrent: Torrent,
31-
}
32-
3325
#[derive(Debug, Deserialize)]
3426
pub struct Metadata {
3527
pub title: String,
@@ -43,10 +35,10 @@ impl Metadata {
4335
///
4436
/// # Errors
4537
///
46-
/// This function will return an `BadRequest` error if the `title` or the `category` is empty.
38+
/// This function will return an error if the any of the mandatory metadata fields are missing.
4739
pub fn verify(&self) -> Result<(), ServiceError> {
4840
if self.title.is_empty() || self.category.is_empty() {
49-
Err(ServiceError::BadRequest)
41+
Err(ServiceError::MissingMandatoryMetadataFields)
5042
} else {
5143
Ok(())
5244
}

src/models/torrent_file.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ impl Torrent {
229229

230230
#[must_use]
231231
pub fn info_hash(&self) -> String {
232+
// todo: return an InfoHash struct
232233
from_bytes(&self.calculate_info_hash_as_bytes()).to_lowercase()
233234
}
234235

src/services/torrent.rs

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ use crate::errors::ServiceError;
1111
use crate::models::category::CategoryId;
1212
use crate::models::info_hash::InfoHash;
1313
use crate::models::response::{DeletedTorrentResponse, TorrentResponse, TorrentsResponse};
14-
use crate::models::torrent::{AddTorrentRequest, TorrentId, TorrentListing};
14+
use crate::models::torrent::{Metadata, TorrentId, TorrentListing};
1515
use crate::models::torrent_file::{DbTorrentInfo, Torrent, TorrentFile};
1616
use crate::models::torrent_tag::{TagId, TorrentTag};
1717
use crate::models::user::UserId;
1818
use crate::tracker::statistics_importer::StatisticsImporter;
19+
use crate::utils::parse_torrent;
1920
use crate::{tracker, AsCSV};
2021

2122
const MIN_TORRENT_TITLE_LENGTH: usize = 3;
@@ -34,6 +35,14 @@ pub struct Index {
3435
torrent_listing_generator: Arc<DbTorrentListingGenerator>,
3536
}
3637

38+
pub struct AddTorrentRequest {
39+
pub title: String,
40+
pub description: String,
41+
pub category: String,
42+
pub tags: Vec<TagId>,
43+
pub torrent_buffer: Vec<u8>,
44+
}
45+
3746
/// User request to generate a torrent listing.
3847
#[derive(Debug, Deserialize)]
3948
pub struct ListingRequest {
@@ -101,46 +110,75 @@ impl Index {
101110
/// * Unable to insert the torrent into the database.
102111
/// * Unable to add the torrent to the whitelist.
103112
/// * Torrent title is too short.
104-
pub async fn add_torrent(&self, mut torrent_request: AddTorrentRequest, user_id: UserId) -> Result<TorrentId, ServiceError> {
113+
///
114+
/// # Panics
115+
///
116+
/// This function will panic if:
117+
///
118+
/// * Unable to parse the torrent info-hash.
119+
pub async fn add_torrent(
120+
&self,
121+
add_torrent_form: AddTorrentRequest,
122+
user_id: UserId,
123+
) -> Result<(TorrentId, InfoHash), ServiceError> {
124+
let metadata = Metadata {
125+
title: add_torrent_form.title,
126+
description: add_torrent_form.description,
127+
category: add_torrent_form.category,
128+
tags: add_torrent_form.tags,
129+
};
130+
131+
metadata.verify()?;
132+
133+
let mut torrent =
134+
parse_torrent::decode_torrent(&add_torrent_form.torrent_buffer).map_err(|_| ServiceError::InvalidTorrentFile)?;
135+
136+
// Make sure that the pieces key has a length that is a multiple of 20
137+
if let Some(pieces) = torrent.info.pieces.as_ref() {
138+
if pieces.as_ref().len() % 20 != 0 {
139+
return Err(ServiceError::InvalidTorrentPiecesLength);
140+
}
141+
}
142+
105143
let _user = self.user_repository.get_compact(&user_id).await?;
106144

107-
torrent_request.torrent.set_announce_urls(&self.configuration).await;
145+
torrent.set_announce_urls(&self.configuration).await;
108146

109-
if torrent_request.metadata.title.len() < MIN_TORRENT_TITLE_LENGTH {
147+
if metadata.title.len() < MIN_TORRENT_TITLE_LENGTH {
110148
return Err(ServiceError::InvalidTorrentTitleLength);
111149
}
112150

113151
let category = self
114152
.category_repository
115-
.get_by_name(&torrent_request.metadata.category)
153+
.get_by_name(&metadata.category)
116154
.await
117155
.map_err(|_| ServiceError::InvalidCategory)?;
118156

119-
let torrent_id = self.torrent_repository.add(&torrent_request, user_id, category).await?;
157+
let torrent_id = self.torrent_repository.add(&torrent, &metadata, user_id, category).await?;
158+
let info_hash: InfoHash = torrent
159+
.info_hash()
160+
.parse()
161+
.expect("the parsed torrent should have a valid info hash");
120162

121163
drop(
122164
self.tracker_statistics_importer
123-
.import_torrent_statistics(torrent_id, &torrent_request.torrent.info_hash())
165+
.import_torrent_statistics(torrent_id, &torrent.info_hash())
124166
.await,
125167
);
126168

127169
// We always whitelist the torrent on the tracker because even if the tracker mode is `public`
128170
// it could be changed to `private` later on.
129-
if let Err(e) = self
130-
.tracker_service
131-
.whitelist_info_hash(torrent_request.torrent.info_hash())
132-
.await
133-
{
171+
if let Err(e) = self.tracker_service.whitelist_info_hash(torrent.info_hash()).await {
134172
// If the torrent can't be whitelisted somehow, remove the torrent from database
135173
drop(self.torrent_repository.delete(&torrent_id).await);
136174
return Err(e);
137175
}
138176

139177
self.torrent_tag_repository
140-
.link_torrent_to_tags(&torrent_id, &torrent_request.metadata.tags)
178+
.link_torrent_to_tags(&torrent_id, &metadata.tags)
141179
.await?;
142180

143-
Ok(torrent_id)
181+
Ok((torrent_id, info_hash))
144182
}
145183

146184
/// Gets a torrent from the Index.
@@ -436,18 +474,13 @@ impl DbTorrentRepository {
436474
/// This function will return an error there is a database error.
437475
pub async fn add(
438476
&self,
439-
torrent_request: &AddTorrentRequest,
477+
torrent: &Torrent,
478+
metadata: &Metadata,
440479
user_id: UserId,
441480
category: Category,
442481
) -> Result<TorrentId, Error> {
443482
self.database
444-
.insert_torrent_and_get_id(
445-
&torrent_request.torrent,
446-
user_id,
447-
category.category_id,
448-
&torrent_request.metadata.title,
449-
&torrent_request.metadata.description,
450-
)
483+
.insert_torrent_and_get_id(torrent, user_id, category.category_id, &metadata.title, &metadata.description)
451484
.await
452485
}
453486

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use axum::response::{IntoResponse, Response};
2+
use derive_more::{Display, Error};
3+
use hyper::StatusCode;
4+
5+
use crate::web::api::v1::responses::{json_error_response, ErrorResponseData};
6+
7+
#[derive(Debug, Display, PartialEq, Eq, Error)]
8+
pub enum Request {
9+
#[display(fmt = "torrent title bytes are nota valid UTF8 string.")]
10+
TitleIsNotValidUtf8,
11+
12+
#[display(fmt = "torrent description bytes are nota valid UTF8 string.")]
13+
DescriptionIsNotValidUtf8,
14+
15+
#[display(fmt = "torrent category bytes are nota valid UTF8 string.")]
16+
CategoryIsNotValidUtf8,
17+
18+
#[display(fmt = "torrent tags arrays bytes are nota valid UTF8 string array.")]
19+
TagsArrayIsNotValidUtf8,
20+
21+
#[display(fmt = "torrent tags string is not a valid JSON.")]
22+
TagsArrayIsNotValidJson,
23+
24+
#[display(fmt = "upload torrent request header `content-type` should be `application/x-bittorrent`.")]
25+
InvalidFileType,
26+
27+
#[display(fmt = "cannot write uploaded torrent bytes (binary file) into memory.")]
28+
CannotWriteChunkFromUploadedBinary,
29+
30+
#[display(fmt = "cannot read a chunk of bytes from the uploaded torrent file. Review the request body size limit.")]
31+
CannotReadChunkFromUploadedBinary,
32+
33+
#[display(fmt = "provided path param for Info-hash is not valid.")]
34+
InvalidInfoHashParam,
35+
}
36+
37+
impl IntoResponse for Request {
38+
fn into_response(self) -> Response {
39+
json_error_response(
40+
http_status_code_for_handler_error(&self),
41+
&ErrorResponseData { error: self.to_string() },
42+
)
43+
}
44+
}
45+
46+
#[must_use]
47+
pub fn http_status_code_for_handler_error(error: &Request) -> StatusCode {
48+
#[allow(clippy::match_same_arms)]
49+
match error {
50+
Request::TitleIsNotValidUtf8 => StatusCode::BAD_REQUEST,
51+
Request::DescriptionIsNotValidUtf8 => StatusCode::BAD_REQUEST,
52+
Request::CategoryIsNotValidUtf8 => StatusCode::BAD_REQUEST,
53+
Request::TagsArrayIsNotValidUtf8 => StatusCode::BAD_REQUEST,
54+
Request::TagsArrayIsNotValidJson => StatusCode::BAD_REQUEST,
55+
Request::InvalidFileType => StatusCode::BAD_REQUEST,
56+
Request::InvalidInfoHashParam => StatusCode::BAD_REQUEST,
57+
// Internal errors processing the request
58+
Request::CannotWriteChunkFromUploadedBinary => StatusCode::INTERNAL_SERVER_ERROR,
59+
Request::CannotReadChunkFromUploadedBinary => StatusCode::INTERNAL_SERVER_ERROR,
60+
}
61+
}

0 commit comments

Comments
 (0)