torrust_index/
errors.rs

1use std::borrow::Cow;
2use std::error;
3
4use derive_more::{Display, Error};
5use hyper::StatusCode;
6
7use crate::databases::database;
8use crate::models::torrent::MetadataError;
9use crate::tracker::service::TrackerAPIError;
10use crate::utils::parse_torrent::DecodeTorrentFileError;
11
12pub type ServiceResult<V> = Result<V, ServiceError>;
13
14#[derive(Debug, Display, PartialEq, Eq, Error)]
15#[allow(dead_code)]
16pub enum ServiceError {
17    #[display(fmt = "internal server error")]
18    InternalServerError,
19
20    #[display(fmt = "This server is is closed for registration. Contact admin if this is unexpected")]
21    ClosedForRegistration,
22
23    #[display(fmt = "Email is required")] //405j
24    EmailMissing,
25    #[display(fmt = "Please enter a valid email address")] //405j
26    EmailInvalid,
27
28    #[display(fmt = "The value you entered for URL is not a URL")] //405j
29    NotAUrl,
30
31    #[display(fmt = "Invalid username/email or password")]
32    WrongPasswordOrUsername,
33    #[display(fmt = "Invalid password")]
34    InvalidPassword,
35    #[display(fmt = "Username not found")]
36    UsernameNotFound,
37    #[display(fmt = "User not found")]
38    UserNotFound,
39
40    #[display(fmt = "Account not found")]
41    AccountNotFound,
42
43    /// when the value passed contains profanity
44    #[display(fmt = "Can't allow profanity in usernames")]
45    ProfanityError,
46    /// when the value passed contains blacklisted words
47    /// see [blacklist](https://github.com/shuttlecraft/The-Big-Username-Blacklist)
48    #[display(fmt = "Username contains blacklisted words")]
49    BlacklistError,
50    /// when the value passed contains characters not present
51    /// in [UsernameCaseMapped](https://tools.ietf.org/html/rfc8265#page-7)
52    /// profile
53    #[display(fmt = "username_case_mapped violation")]
54    UsernameCaseMappedError,
55
56    #[display(fmt = "Password too short")]
57    PasswordTooShort,
58    #[display(fmt = "Password too long")]
59    PasswordTooLong,
60    #[display(fmt = "Passwords don't match")]
61    PasswordsDontMatch,
62
63    /// when the a username is already taken
64    #[display(fmt = "Username not available")]
65    UsernameTaken,
66
67    #[display(fmt = "Invalid username. Usernames must consist of 1-20 alphanumeric characters, dashes, or underscore")]
68    UsernameInvalid,
69
70    /// email is already taken
71    #[display(fmt = "Email not available")]
72    EmailTaken,
73
74    #[display(fmt = "Please verify your email before logging in")]
75    EmailNotVerified,
76
77    /// when the a token name is already taken
78    /// token not found
79    #[display(fmt = "Token not found. Please sign in.")]
80    TokenNotFound,
81
82    /// token expired
83    #[display(fmt = "Token expired. Please sign in again.")]
84    TokenExpired,
85
86    #[display(fmt = "Token invalid.")]
87    /// token invalid
88    TokenInvalid,
89
90    #[display(fmt = "Uploaded torrent is not valid.")]
91    InvalidTorrentFile,
92
93    #[display(fmt = "Uploaded torrent has an invalid pieces key.")]
94    InvalidTorrentPiecesLength,
95
96    #[display(fmt = "Only .torrent files can be uploaded.")]
97    InvalidFileType,
98
99    #[display(fmt = "Torrent title is too short.")]
100    InvalidTorrentTitleLength,
101
102    #[display(fmt = "Some mandatory metadata fields are missing.")]
103    MissingMandatoryMetadataFields,
104
105    #[display(fmt = "Selected category does not exist.")]
106    InvalidCategory,
107
108    #[display(fmt = "Selected tag does not exist.")]
109    InvalidTag,
110
111    #[display(fmt = "Unauthorized action.")]
112    UnauthorizedAction,
113
114    #[display(
115        fmt = "Unauthorized actions for guest users. Try logging in to check if you have permission to perform the action"
116    )]
117    UnauthorizedActionForGuests,
118
119    #[display(fmt = "This torrent already exists in our database.")]
120    InfoHashAlreadyExists,
121
122    #[display(fmt = "A torrent with the same canonical infohash already exists in our database.")]
123    CanonicalInfoHashAlreadyExists,
124
125    #[display(fmt = "A torrent with the same original infohash already exists in our database.")]
126    OriginalInfoHashAlreadyExists,
127
128    #[display(fmt = "This torrent title has already been used.")]
129    TorrentTitleAlreadyExists,
130
131    #[display(fmt = "Could not whitelist torrent.")]
132    WhitelistingError,
133
134    #[display(fmt = "Failed to send verification email.")]
135    FailedToSendVerificationEmail,
136
137    #[display(fmt = "Category already exists.")]
138    CategoryAlreadyExists,
139
140    #[display(fmt = "Category name cannot be empty.")]
141    CategoryNameEmpty,
142
143    #[display(fmt = "Tag already exists.")]
144    TagAlreadyExists,
145
146    #[display(fmt = "Tag name cannot be empty.")]
147    TagNameEmpty,
148
149    #[display(fmt = "Torrent not found.")]
150    TorrentNotFound,
151
152    #[display(fmt = "Category not found.")]
153    CategoryNotFound,
154
155    #[display(fmt = "Tag not found.")]
156    TagNotFound,
157
158    #[display(fmt = "Database error.")]
159    DatabaseError,
160
161    #[display(fmt = "Authentication error, please sign in")]
162    LoggedInUserNotFound,
163
164    // Begin tracker errors
165    #[display(fmt = "Sorry, we have an error with our tracker connection.")]
166    TrackerOffline,
167
168    #[display(fmt = "Tracker response error. The operation could not be performed.")]
169    TrackerResponseError,
170
171    #[display(fmt = "Tracker unknown response. Unexpected response from tracker. For example, if it can't be parsed.")]
172    TrackerUnknownResponse,
173
174    #[display(fmt = "Torrent not found in tracker.")]
175    TorrentNotFoundInTracker,
176
177    #[display(fmt = "Invalid tracker API token.")]
178    InvalidTrackerToken,
179    // End tracker errors
180}
181
182impl From<sqlx::Error> for ServiceError {
183    fn from(e: sqlx::Error) -> Self {
184        eprintln!("{e:?}");
185
186        if let Some(err) = e.as_database_error() {
187            return if err.code() == Some(Cow::from("2067")) {
188                if err.message().contains("torrust_torrents.info_hash") {
189                    println!("info_hash already exists {}", err.message());
190                    ServiceError::InfoHashAlreadyExists
191                } else {
192                    ServiceError::InternalServerError
193                }
194            } else {
195                ServiceError::TorrentNotFound
196            };
197        }
198
199        ServiceError::InternalServerError
200    }
201}
202
203impl From<database::Error> for ServiceError {
204    fn from(e: database::Error) -> Self {
205        map_database_error_to_service_error(&e)
206    }
207}
208
209impl From<argon2::password_hash::Error> for ServiceError {
210    fn from(e: argon2::password_hash::Error) -> Self {
211        eprintln!("{e}");
212        ServiceError::InternalServerError
213    }
214}
215
216impl From<std::io::Error> for ServiceError {
217    fn from(e: std::io::Error) -> Self {
218        eprintln!("{e}");
219        ServiceError::InternalServerError
220    }
221}
222
223impl From<Box<dyn error::Error>> for ServiceError {
224    fn from(e: Box<dyn error::Error>) -> Self {
225        eprintln!("{e}");
226        ServiceError::InternalServerError
227    }
228}
229
230impl From<serde_json::Error> for ServiceError {
231    fn from(e: serde_json::Error) -> Self {
232        eprintln!("{e}");
233        ServiceError::InternalServerError
234    }
235}
236
237impl From<MetadataError> for ServiceError {
238    fn from(e: MetadataError) -> Self {
239        eprintln!("{e}");
240        match e {
241            MetadataError::MissingTorrentTitle => ServiceError::MissingMandatoryMetadataFields,
242            MetadataError::InvalidTorrentTitleLength => ServiceError::InvalidTorrentTitleLength,
243        }
244    }
245}
246
247impl From<DecodeTorrentFileError> for ServiceError {
248    fn from(e: DecodeTorrentFileError) -> Self {
249        eprintln!("{e}");
250        match e {
251            DecodeTorrentFileError::InvalidTorrentPiecesLength => ServiceError::InvalidTorrentTitleLength,
252            DecodeTorrentFileError::CannotBencodeInfoDict
253            | DecodeTorrentFileError::InvalidInfoDictionary
254            | DecodeTorrentFileError::InvalidBencodeData => ServiceError::InvalidTorrentFile,
255        }
256    }
257}
258
259impl From<TrackerAPIError> for ServiceError {
260    fn from(e: TrackerAPIError) -> Self {
261        eprintln!("{e}");
262        match e {
263            TrackerAPIError::TrackerOffline { error: _ } => ServiceError::TrackerOffline,
264            TrackerAPIError::InternalServerError | TrackerAPIError::NotFound => ServiceError::TrackerResponseError,
265            TrackerAPIError::TorrentNotFound => ServiceError::TorrentNotFoundInTracker,
266            TrackerAPIError::UnexpectedResponseStatus
267            | TrackerAPIError::MissingResponseBody
268            | TrackerAPIError::FailedToParseTrackerResponse { body: _ } => ServiceError::TrackerUnknownResponse,
269            TrackerAPIError::CannotSaveUserKey => ServiceError::DatabaseError,
270            TrackerAPIError::InvalidToken => ServiceError::InvalidTrackerToken,
271        }
272    }
273}
274
275#[must_use]
276pub fn http_status_code_for_service_error(error: &ServiceError) -> StatusCode {
277    #[allow(clippy::match_same_arms)]
278    match error {
279        ServiceError::ClosedForRegistration => StatusCode::FORBIDDEN,
280        ServiceError::EmailInvalid => StatusCode::BAD_REQUEST,
281        ServiceError::NotAUrl => StatusCode::BAD_REQUEST,
282        ServiceError::WrongPasswordOrUsername => StatusCode::FORBIDDEN,
283        ServiceError::InvalidPassword => StatusCode::FORBIDDEN,
284        ServiceError::UsernameNotFound => StatusCode::NOT_FOUND,
285        ServiceError::UserNotFound => StatusCode::NOT_FOUND,
286        ServiceError::AccountNotFound => StatusCode::NOT_FOUND,
287        ServiceError::ProfanityError => StatusCode::BAD_REQUEST,
288        ServiceError::BlacklistError => StatusCode::BAD_REQUEST,
289        ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST,
290        ServiceError::PasswordTooShort => StatusCode::BAD_REQUEST,
291        ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST,
292        ServiceError::PasswordsDontMatch => StatusCode::BAD_REQUEST,
293        ServiceError::UsernameTaken => StatusCode::BAD_REQUEST,
294        ServiceError::UsernameInvalid => StatusCode::BAD_REQUEST,
295        ServiceError::EmailTaken => StatusCode::BAD_REQUEST,
296        ServiceError::EmailNotVerified => StatusCode::FORBIDDEN,
297        ServiceError::TokenNotFound => StatusCode::UNAUTHORIZED,
298        ServiceError::TokenExpired => StatusCode::UNAUTHORIZED,
299        ServiceError::TokenInvalid => StatusCode::UNAUTHORIZED,
300        ServiceError::TorrentNotFound => StatusCode::NOT_FOUND,
301        ServiceError::InvalidTorrentFile => StatusCode::BAD_REQUEST,
302        ServiceError::InvalidTorrentPiecesLength => StatusCode::BAD_REQUEST,
303        ServiceError::InvalidFileType => StatusCode::BAD_REQUEST,
304        ServiceError::InvalidTorrentTitleLength => StatusCode::BAD_REQUEST,
305        ServiceError::MissingMandatoryMetadataFields => StatusCode::BAD_REQUEST,
306        ServiceError::InvalidCategory => StatusCode::BAD_REQUEST,
307        ServiceError::InvalidTag => StatusCode::BAD_REQUEST,
308        ServiceError::UnauthorizedAction => StatusCode::FORBIDDEN,
309        ServiceError::UnauthorizedActionForGuests => StatusCode::UNAUTHORIZED,
310        ServiceError::InfoHashAlreadyExists => StatusCode::BAD_REQUEST,
311        ServiceError::CanonicalInfoHashAlreadyExists => StatusCode::CONFLICT,
312        ServiceError::OriginalInfoHashAlreadyExists => StatusCode::CONFLICT,
313        ServiceError::TorrentTitleAlreadyExists => StatusCode::BAD_REQUEST,
314        ServiceError::TrackerOffline => StatusCode::SERVICE_UNAVAILABLE,
315        ServiceError::CategoryNameEmpty => StatusCode::BAD_REQUEST,
316        ServiceError::CategoryAlreadyExists => StatusCode::BAD_REQUEST,
317        ServiceError::TagNameEmpty => StatusCode::BAD_REQUEST,
318        ServiceError::TagAlreadyExists => StatusCode::BAD_REQUEST,
319        ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
320        ServiceError::EmailMissing => StatusCode::NOT_FOUND,
321        ServiceError::FailedToSendVerificationEmail => StatusCode::INTERNAL_SERVER_ERROR,
322        ServiceError::WhitelistingError => StatusCode::INTERNAL_SERVER_ERROR,
323        ServiceError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
324        ServiceError::CategoryNotFound => StatusCode::NOT_FOUND,
325        ServiceError::TagNotFound => StatusCode::NOT_FOUND,
326        ServiceError::TrackerResponseError => StatusCode::INTERNAL_SERVER_ERROR,
327        ServiceError::TrackerUnknownResponse => StatusCode::INTERNAL_SERVER_ERROR,
328        ServiceError::TorrentNotFoundInTracker => StatusCode::NOT_FOUND,
329        ServiceError::InvalidTrackerToken => StatusCode::INTERNAL_SERVER_ERROR,
330        ServiceError::LoggedInUserNotFound => StatusCode::UNAUTHORIZED,
331    }
332}
333
334#[must_use]
335pub fn map_database_error_to_service_error(error: &database::Error) -> ServiceError {
336    #[allow(clippy::match_same_arms)]
337    match error {
338        database::Error::Error => ServiceError::InternalServerError,
339        database::Error::ErrorWithText(_) => ServiceError::InternalServerError,
340        database::Error::UsernameTaken => ServiceError::UsernameTaken,
341        database::Error::EmailTaken => ServiceError::EmailTaken,
342        database::Error::UserNotFound => ServiceError::UserNotFound,
343        database::Error::CategoryNotFound => ServiceError::InvalidCategory,
344        database::Error::TagAlreadyExists => ServiceError::TagAlreadyExists,
345        database::Error::TagNotFound => ServiceError::InvalidTag,
346        database::Error::TorrentNotFound => ServiceError::TorrentNotFound,
347        database::Error::TorrentAlreadyExists => ServiceError::InfoHashAlreadyExists,
348        database::Error::TorrentTitleAlreadyExists => ServiceError::TorrentTitleAlreadyExists,
349        database::Error::UnrecognizedDatabaseDriver => ServiceError::InternalServerError,
350        database::Error::TorrentInfoHashNotFound => ServiceError::TorrentNotFound,
351    }
352}