diff --git a/share/default/config/index.development.sqlite3.toml b/share/default/config/index.development.sqlite3.toml index 11a3f48b..c0b04253 100644 --- a/share/default/config/index.development.sqlite3.toml +++ b/share/default/config/index.development.sqlite3.toml @@ -4,7 +4,7 @@ log_level = "info" name = "Torrust" [tracker] -api_url = "http://localhost:1212" +api_url = "http://localhost:1212/" mode = "Public" token = "MyAccessToken" token_valid_seconds = 7257600 diff --git a/src/config/mod.rs b/src/config/mod.rs index b4284713..9a2bc633 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -15,6 +15,7 @@ use tokio::sync::RwLock; use torrust_index_located_error::LocatedError; use url::Url; +use self::v1::tracker::ApiToken; use crate::web::api::server::DynError; pub type Settings = v1::Settings; @@ -55,7 +56,7 @@ pub const ENV_VAR_AUTH_SECRET_KEY: &str = "TORRUST_INDEX_AUTH_SECRET_KEY"; pub struct Info { config_toml: Option, config_toml_path: String, - tracker_api_token: Option, + tracker_api_token: Option, auth_secret_key: Option, } @@ -88,7 +89,10 @@ impl Info { default_config_toml_path }; - let tracker_api_token = env::var(env_var_tracker_api_admin_token).ok(); + let tracker_api_token = env::var(env_var_tracker_api_admin_token) + .ok() + .map(|token| ApiToken::new(&token)); + let auth_secret_key = env::var(env_var_auth_secret_key).ok(); Ok(Self { @@ -325,21 +329,18 @@ impl Configuration { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ConfigurationPublic { website_name: String, - tracker_url: String, + tracker_url: Url, tracker_mode: TrackerMode, email_on_signup: EmailOnSignup, } -fn parse_url(url_str: &str) -> Result { - Url::parse(url_str) -} - #[cfg(test)] mod tests { use url::Url; use crate::config::v1::auth::SecretKey; + use crate::config::v1::tracker::ApiToken; use crate::config::{Configuration, ConfigurationPublic, Info, Settings}; #[cfg(test)] @@ -350,7 +351,7 @@ mod tests { [tracker] url = "udp://localhost:6969" mode = "Public" - api_url = "http://localhost:1212" + api_url = "http://localhost:1212/" token = "MyAccessToken" token_valid_seconds = 7257600 @@ -475,7 +476,7 @@ mod tests { let info = Info { config_toml: Some(default_config_toml()), config_toml_path: String::new(), - tracker_api_token: Some("OVERRIDDEN API TOKEN".to_string()), + tracker_api_token: Some(ApiToken::new("OVERRIDDEN API TOKEN")), auth_secret_key: None, }; @@ -483,7 +484,7 @@ mod tests { assert_eq!( configuration.get_all().await.tracker.token, - "OVERRIDDEN API TOKEN".to_string() + ApiToken::new("OVERRIDDEN API TOKEN") ); } @@ -504,7 +505,7 @@ mod tests { let settings = Configuration::load_settings(&info).expect("Could not load configuration from file"); - assert_eq!(settings.tracker.token, "OVERRIDDEN API TOKEN".to_string()); + assert_eq!(settings.tracker.token, ApiToken::new("OVERRIDDEN API TOKEN")); Ok(()) }); @@ -550,24 +551,9 @@ mod tests { }); } - mod syntax_checks { - // todo: use rich types in configuration structs for basic syntax checks. - - use crate::config::validator::Validator; - use crate::config::Configuration; - - #[tokio::test] - async fn tracker_url_should_be_a_valid_url() { - let configuration = Configuration::default(); - - let mut settings_lock = configuration.settings.write().await; - settings_lock.tracker.url = "INVALID URL".to_string(); - - assert!(settings_lock.validate().is_err()); - } - } - mod semantic_validation { + use url::Url; + use crate::config::validator::Validator; use crate::config::{Configuration, TrackerMode}; @@ -577,7 +563,7 @@ mod tests { let mut settings_lock = configuration.settings.write().await; settings_lock.tracker.mode = TrackerMode::Private; - settings_lock.tracker.url = "udp://localhost:6969".to_string(); + settings_lock.tracker.url = Url::parse("udp://localhost:6969").unwrap(); assert!(settings_lock.validate().is_err()); } diff --git a/src/config/v1/mod.rs b/src/config/v1/mod.rs index 8deea44d..fea3e2ae 100644 --- a/src/config/v1/mod.rs +++ b/src/config/v1/mod.rs @@ -16,7 +16,7 @@ use self::database::Database; use self::image_cache::ImageCache; use self::mail::Mail; use self::net::Network; -use self::tracker::Tracker; +use self::tracker::{ApiToken, Tracker}; use self::tracker_statistics_importer::TrackerStatisticsImporter; use self::website::Website; use super::validator::{ValidationError, Validator}; @@ -48,7 +48,7 @@ pub struct Settings { } impl Settings { - pub fn override_tracker_api_token(&mut self, tracker_api_token: &str) { + pub fn override_tracker_api_token(&mut self, tracker_api_token: &ApiToken) { self.tracker.override_tracker_api_token(tracker_api_token); } @@ -57,7 +57,7 @@ impl Settings { } pub fn remove_secrets(&mut self) { - "***".clone_into(&mut self.tracker.token); + self.tracker.token = ApiToken::new("***"); if let Some(_password) = self.database.connect_url.password() { let _ = self.database.connect_url.set_password(Some("***")); } diff --git a/src/config/v1/tracker.rs b/src/config/v1/tracker.rs index fd95a82e..e7089133 100644 --- a/src/config/v1/tracker.rs +++ b/src/config/v1/tracker.rs @@ -1,29 +1,31 @@ +use std::fmt; + use serde::{Deserialize, Serialize}; -use torrust_index_located_error::Located; +use url::Url; use super::{ValidationError, Validator}; -use crate::config::{parse_url, TrackerMode}; +use crate::config::TrackerMode; /// Configuration for the associated tracker. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Tracker { /// Connection string for the tracker. For example: `udp://TRACKER_IP:6969`. - pub url: String, + pub url: Url, /// The mode of the tracker. For example: `Public`. /// See `TrackerMode` in [`torrust-tracker-primitives`](https://docs.rs/torrust-tracker-primitives) /// crate for more information. pub mode: TrackerMode, - /// The url of the tracker API. For example: `http://localhost:1212`. - pub api_url: String, + /// The url of the tracker API. For example: `http://localhost:1212/`. + pub api_url: Url, /// The token used to authenticate with the tracker API. - pub token: String, + pub token: ApiToken, /// The amount of seconds the token is valid. pub token_valid_seconds: u64, } impl Tracker { - pub fn override_tracker_api_token(&mut self, tracker_api_token: &str) { - self.token = tracker_api_token.to_string(); + pub fn override_tracker_api_token(&mut self, tracker_api_token: &ApiToken) { + self.token = tracker_api_token.clone(); } } @@ -32,15 +34,6 @@ impl Validator for Tracker { let tracker_mode = self.mode.clone(); let tracker_url = self.url.clone(); - let tracker_url = match parse_url(&tracker_url) { - Ok(url) => url, - Err(err) => { - return Err(ValidationError::InvalidTrackerUrl { - source: Located(err).into(), - }) - } - }; - if tracker_mode.is_close() && (tracker_url.scheme() != "http" && tracker_url.scheme() != "https") { return Err(ValidationError::UdpTrackersInPrivateModeNotSupported); } @@ -52,11 +45,48 @@ impl Validator for Tracker { impl Default for Tracker { fn default() -> Self { Self { - url: "udp://localhost:6969".to_string(), + url: Url::parse("udp://localhost:6969").unwrap(), mode: TrackerMode::default(), - api_url: "http://localhost:1212".to_string(), - token: "MyAccessToken".to_string(), + api_url: Url::parse("http://localhost:1212/").unwrap(), + token: ApiToken::new("MyAccessToken"), token_valid_seconds: 7_257_600, } } } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ApiToken(String); + +impl ApiToken { + /// # Panics + /// + /// Will panic if the tracker API token if empty. + #[must_use] + pub fn new(key: &str) -> Self { + assert!(!key.is_empty(), "tracker API token cannot be empty"); + + Self(key.to_owned()) + } + + #[must_use] + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl fmt::Display for ApiToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(test)] +mod tests { + use super::ApiToken; + + #[test] + #[should_panic(expected = "tracker API token cannot be empty")] + fn secret_key_can_not_be_empty() { + drop(ApiToken::new("")); + } +} diff --git a/src/config/validator.rs b/src/config/validator.rs index d3ef047a..3578461d 100644 --- a/src/config/validator.rs +++ b/src/config/validator.rs @@ -1,15 +1,9 @@ //! Trait to validate the whole settings of sections of the settings. use thiserror::Error; -use torrust_index_located_error::LocatedError; -use url::ParseError; /// Errors that can occur validating the configuration. #[derive(Error, Debug)] pub enum ValidationError { - /// Unable to load the configuration from the configuration file. - #[error("Invalid tracker URL: {source}")] - InvalidTrackerUrl { source: LocatedError<'static, ParseError> }, - #[error("UDP private trackers are not supported. URL schemes for private tracker URLs must be HTTP ot HTTPS")] UdpTrackersInPrivateModeNotSupported, } diff --git a/src/console/commands/tracker_statistics_importer/app.rs b/src/console/commands/tracker_statistics_importer/app.rs index eccbbc4c..edb43304 100644 --- a/src/console/commands/tracker_statistics_importer/app.rs +++ b/src/console/commands/tracker_statistics_importer/app.rs @@ -100,7 +100,7 @@ pub async fn import() { let tracker_url = settings.tracker.url.clone(); - eprintln!("Tracker url: {}", tracker_url.green()); + eprintln!("Tracker url: {}", tracker_url.to_string().green()); let database = Arc::new( database::connect(settings.database.connect_url.as_ref()) diff --git a/src/databases/database.rs b/src/databases/database.rs index a19970be..ce3843b0 100644 --- a/src/databases/database.rs +++ b/src/databases/database.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use chrono::{DateTime, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; +use url::Url; use crate::databases::mysql::Mysql; use crate::databases::sqlite::Sqlite; @@ -336,7 +337,7 @@ pub trait Database: Sync + Send { async fn get_tags_for_torrent_id(&self, torrent_id: i64) -> Result, Error>; /// Update the seeders and leechers info for a torrent with `torrent_id`, `tracker_url`, `seeders` and `leechers`. - async fn update_tracker_info(&self, torrent_id: i64, tracker_url: &str, seeders: i64, leechers: i64) -> Result<(), Error>; + async fn update_tracker_info(&self, torrent_id: i64, tracker_url: &Url, seeders: i64, leechers: i64) -> Result<(), Error>; /// Delete a torrent with `torrent_id`. async fn delete_torrent(&self, torrent_id: i64) -> Result<(), Error>; diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs index d28f1ae7..beec1a80 100644 --- a/src/databases/mysql.rs +++ b/src/databases/mysql.rs @@ -5,6 +5,7 @@ use async_trait::async_trait; use chrono::{DateTime, NaiveDateTime, Utc}; use sqlx::mysql::{MySqlConnectOptions, MySqlPoolOptions}; use sqlx::{query, query_as, Acquire, ConnectOptions, MySqlPool}; +use url::Url; use super::database::TABLES_TO_TRUNCATE; use crate::databases::database; @@ -1072,13 +1073,13 @@ impl Database for Mysql { async fn update_tracker_info( &self, torrent_id: i64, - tracker_url: &str, + tracker_url: &Url, seeders: i64, leechers: i64, ) -> Result<(), database::Error> { query("REPLACE INTO torrust_torrent_tracker_stats (torrent_id, tracker_url, seeders, leechers, updated_at) VALUES (?, ?, ?, ?, ?)") .bind(torrent_id) - .bind(tracker_url) + .bind(tracker_url.to_string()) .bind(seeders) .bind(leechers) .bind(datetime_now()) diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs index 421292d6..a70beac6 100644 --- a/src/databases/sqlite.rs +++ b/src/databases/sqlite.rs @@ -5,6 +5,7 @@ use async_trait::async_trait; use chrono::{DateTime, NaiveDateTime, Utc}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use sqlx::{query, query_as, Acquire, ConnectOptions, SqlitePool}; +use url::Url; use super::database::TABLES_TO_TRUNCATE; use crate::databases::database; @@ -1064,13 +1065,13 @@ impl Database for Sqlite { async fn update_tracker_info( &self, torrent_id: i64, - tracker_url: &str, + tracker_url: &Url, seeders: i64, leechers: i64, ) -> Result<(), database::Error> { query("REPLACE INTO torrust_torrent_tracker_stats (torrent_id, tracker_url, seeders, leechers, updated_at) VALUES ($1, $2, $3, $4, $5)") .bind(torrent_id) - .bind(tracker_url) + .bind(tracker_url.to_string()) .bind(seeders) .bind(leechers) .bind(datetime_now()) diff --git a/src/errors.rs b/src/errors.rs index 71ff3ab3..9723b4f7 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -161,7 +161,7 @@ pub enum ServiceError { #[display(fmt = "Tracker response error. The operation could not be performed.")] TrackerResponseError, - #[display(fmt = "Tracker unknown response. Unexpected response from tracker. For example, if it can be parsed.")] + #[display(fmt = "Tracker unknown response. Unexpected response from tracker. For example, if it can't be parsed.")] TrackerUnknownResponse, #[display(fmt = "Torrent not found in tracker.")] @@ -253,8 +253,8 @@ impl From for ServiceError { fn from(e: TrackerAPIError) -> Self { eprintln!("{e}"); match e { - TrackerAPIError::TrackerOffline => ServiceError::TrackerOffline, - TrackerAPIError::InternalServerError => ServiceError::TrackerResponseError, + TrackerAPIError::TrackerOffline { error: _ } => ServiceError::TrackerOffline, + TrackerAPIError::InternalServerError | TrackerAPIError::NotFound => ServiceError::TrackerResponseError, TrackerAPIError::TorrentNotFound => ServiceError::TorrentNotFoundInTracker, TrackerAPIError::UnexpectedResponseStatus | TrackerAPIError::MissingResponseBody diff --git a/src/lib.rs b/src/lib.rs index b386549d..c3148e86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,7 +59,7 @@ //! [tracker] //! url = "udp://localhost:6969" //! mode = "Public" -//! api_url = "http://localhost:1212" +//! api_url = "http://localhost:1212/" //! token = "MyAccessToken" //! token_valid_seconds = 7257600 //! ``` @@ -172,7 +172,7 @@ //! [tracker] //! url = "udp://localhost:6969" //! mode = "Public" -//! api_url = "http://localhost:1212" +//! api_url = "http://localhost:1212/" //! token = "MyAccessToken" //! token_valid_seconds = 7257600 //! diff --git a/src/models/response.rs b/src/models/response.rs index 020eb9be..ea3ef7f3 100644 --- a/src/models/response.rs +++ b/src/models/response.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use url::Url; use super::category::Category; use super::torrent::TorrentId; @@ -107,12 +108,12 @@ impl TorrentResponse { } /// It adds the tracker URL in the first position of the tracker list. - pub fn include_url_as_main_tracker(&mut self, tracker_url: &str) { + pub fn include_url_as_main_tracker(&mut self, tracker_url: &Url) { // Remove any existing instances of tracker_url - self.trackers.retain(|tracker| tracker != tracker_url); + self.trackers.retain(|tracker| *tracker != tracker_url.to_string()); // Insert tracker_url at the first position - self.trackers.insert(0, tracker_url.to_owned()); + self.trackers.insert(0, tracker_url.to_owned().to_string()); } } diff --git a/src/models/torrent_file.rs b/src/models/torrent_file.rs index 1bbd7157..ac28a6dc 100644 --- a/src/models/torrent_file.rs +++ b/src/models/torrent_file.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use serde_bencode::ser; use serde_bytes::ByteBuf; use sha1::{Digest, Sha1}; +use url::Url; use super::info_hash::InfoHash; use crate::utils::hex::{from_bytes, into_bytes}; @@ -127,14 +128,14 @@ impl Torrent { /// /// It will be the URL in the `announce` field and also the first URL in the /// `announce_list`. - pub fn include_url_as_main_tracker(&mut self, tracker_url: &str) { + pub fn include_url_as_main_tracker(&mut self, tracker_url: &Url) { self.set_announce_to(tracker_url); self.add_url_to_front_of_announce_list(tracker_url); } /// Sets the announce url to the tracker url. - pub fn set_announce_to(&mut self, tracker_url: &str) { - self.announce = Some(tracker_url.to_owned()); + pub fn set_announce_to(&mut self, tracker_url: &Url) { + self.announce = Some(tracker_url.to_owned().to_string()); } /// Adds a new tracker URL to the front of the `announce_list`, removes duplicates, @@ -146,15 +147,15 @@ impl Torrent { /// a strict requirement of the `BitTorrent` protocol; it's more of a /// convention followed by some torrent creators for redundancy and to /// ensure better availability of trackers. - pub fn add_url_to_front_of_announce_list(&mut self, tracker_url: &str) { + pub fn add_url_to_front_of_announce_list(&mut self, tracker_url: &Url) { if let Some(list) = &mut self.announce_list { // Remove the tracker URL from existing lists for inner_list in list.iter_mut() { - inner_list.retain(|url| url != tracker_url); + inner_list.retain(|url| *url != tracker_url.to_string()); } // Prepend a new vector containing the tracker_url - let vec = vec![tracker_url.to_owned()]; + let vec = vec![tracker_url.to_owned().to_string()]; list.insert(0, vec); // Remove any empty inner lists diff --git a/src/services/torrent.rs b/src/services/torrent.rs index c3c7e7f0..606f8550 100644 --- a/src/services/torrent.rs +++ b/src/services/torrent.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use log::debug; use serde_derive::{Deserialize, Serialize}; +use url::Url; use super::category::DbCategoryRepository; use super::user::DbUserRepository; @@ -432,7 +433,7 @@ impl Index { Ok(torrent_response) } - async fn get_tracker_url(&self) -> String { + async fn get_tracker_url(&self) -> Url { let settings = self.configuration.settings.read().await; settings.tracker.url.clone() } diff --git a/src/tracker/api.rs b/src/tracker/api.rs index c81a745e..3978d5a3 100644 --- a/src/tracker/api.rs +++ b/src/tracker/api.rs @@ -1,16 +1,17 @@ use std::time::Duration; use reqwest::{Error, Response}; +use url::Url; pub struct ConnectionInfo { /// The URL of the tracker API. Eg: . - pub url: String, + pub url: Url, /// The token used to authenticate with the tracker API. pub token: String, } impl ConnectionInfo { #[must_use] - pub fn new(url: String, token: String) -> Self { + pub fn new(url: Url, token: String) -> Self { Self { url, token } } } @@ -29,7 +30,7 @@ impl Client { /// /// Will fails if it can't build a HTTP client with a timeout. pub fn new(connection_info: ConnectionInfo) -> Result { - let base_url = format!("{}/api/v1", connection_info.url); + let base_url = format!("{}api/v1", connection_info.url); let client = reqwest::Client::builder().timeout(Duration::from_secs(5)).build()?; let token_param = [(TOKEN_PARAM_NAME.to_string(), connection_info.token.to_string())]; diff --git a/src/tracker/service.rs b/src/tracker/service.rs index 3036ce89..5de57d11 100644 --- a/src/tracker/service.rs +++ b/src/tracker/service.rs @@ -4,6 +4,7 @@ use derive_more::{Display, Error}; use hyper::StatusCode; use log::{debug, error}; use serde::{Deserialize, Serialize}; +use url::Url; use super::api::{Client, ConnectionInfo}; use crate::config::Configuration; @@ -14,8 +15,8 @@ use crate::models::user::UserId; #[derive(Debug, Display, PartialEq, Eq, Error)] #[allow(dead_code)] pub enum TrackerAPIError { - #[display(fmt = "Error with tracker connection.")] - TrackerOffline, + #[display(fmt = "Error with tracker request: {error}.")] + TrackerOffline { error: String }, #[display(fmt = "Invalid token for tracker API. Check the tracker token in settings.")] InvalidToken, @@ -23,6 +24,9 @@ pub enum TrackerAPIError { #[display(fmt = "Tracker returned an internal server error.")] InternalServerError, + #[display(fmt = "Tracker returned a not found error.")] + NotFound, + #[display(fmt = "Tracker returned an unexpected response status.")] UnexpectedResponseStatus, @@ -77,7 +81,7 @@ pub struct Service { database: Arc>, api_client: Client, token_valid_seconds: u64, - tracker_url: String, + tracker_url: Url, } impl Service { @@ -88,7 +92,7 @@ impl Service { let settings = cfg.settings.read().await; let api_client = Client::new(ConnectionInfo::new( settings.tracker.api_url.clone(), - settings.tracker.token.clone(), + settings.tracker.token.clone().to_string(), )) .expect("a reqwest client should be provided"); let token_valid_seconds = settings.tracker.token_valid_seconds; @@ -140,7 +144,7 @@ impl Service { } } } - Err(_) => Err(TrackerAPIError::TrackerOffline), + Err(err) => Err(TrackerAPIError::TrackerOffline { error: err.to_string() }), } } @@ -182,7 +186,7 @@ impl Service { } } } - Err(_) => Err(TrackerAPIError::TrackerOffline), + Err(err) => Err(TrackerAPIError::TrackerOffline { error: err.to_string() }), } } @@ -197,7 +201,7 @@ impl Service { /// /// Will return an error if the HTTP request to get generated a new /// user tracker key failed. - pub async fn get_personal_announce_url(&self, user_id: UserId) -> Result { + pub async fn get_personal_announce_url(&self, user_id: UserId) -> Result { debug!(target: "tracker-service", "get personal announce url for user: {user_id}"); let tracker_key = self.database.get_user_tracker_key(user_id).await; @@ -206,7 +210,7 @@ impl Service { Some(tracker_key) => Ok(self.announce_url_with_key(&tracker_key)), None => match self.retrieve_new_tracker_key(user_id).await { Ok(new_tracker_key) => Ok(self.announce_url_with_key(&new_tracker_key)), - Err(_) => Err(TrackerAPIError::TrackerOffline), + Err(err) => Err(TrackerAPIError::TrackerOffline { error: err.to_string() }), }, } } @@ -263,7 +267,7 @@ impl Service { } } } - Err(_) => Err(TrackerAPIError::TrackerOffline), + Err(err) => Err(TrackerAPIError::TrackerOffline { error: err.to_string() }), } } @@ -283,6 +287,7 @@ impl Service { match maybe_response { Ok(response) => { let status: StatusCode = map_status_code(response.status()); + let url = response.url().clone(); let body = response.text().await.map_err(|_| { error!(target: "tracker-service", "response without body"); @@ -305,13 +310,17 @@ impl Service { Err(TrackerAPIError::InternalServerError) } } + StatusCode::NOT_FOUND => { + error!(target: "tracker-service", "get torrents info 404 response: url {url}"); + Err(TrackerAPIError::NotFound) + } _ => { error!(target: "tracker-service", "get torrents info unhandled response: status {status}, body: {body}"); Err(TrackerAPIError::UnexpectedResponseStatus) } } } - Err(_) => Err(TrackerAPIError::TrackerOffline), + Err(err) => Err(TrackerAPIError::TrackerOffline { error: err.to_string() }), } } @@ -360,14 +369,14 @@ impl Service { } } } - Err(_) => Err(TrackerAPIError::TrackerOffline), + Err(err) => Err(TrackerAPIError::TrackerOffline { error: err.to_string() }), } } /// It builds the announce url appending the user tracker key. /// Eg: - fn announce_url_with_key(&self, tracker_key: &TrackerKey) -> String { - format!("{}/{}", self.tracker_url, tracker_key.key) + fn announce_url_with_key(&self, tracker_key: &TrackerKey) -> Url { + Url::parse(&format!("{}/{}", self.tracker_url, tracker_key.key)).unwrap() } fn invalid_token_body() -> String { diff --git a/src/tracker/statistics_importer.rs b/src/tracker/statistics_importer.rs index b9842855..87512a42 100644 --- a/src/tracker/statistics_importer.rs +++ b/src/tracker/statistics_importer.rs @@ -4,6 +4,7 @@ use std::time::Instant; use chrono::{DateTime, Utc}; use log::{debug, error, info}; use text_colorizer::Colorize; +use url::Url; use super::service::{Service, TorrentInfo, TrackerAPIError}; use crate::config::Configuration; @@ -14,7 +15,7 @@ const LOG_TARGET: &str = "Tracker Stats Importer"; pub struct StatisticsImporter { database: Arc>, tracker_service: Arc, - tracker_url: String, + tracker_url: Url, } impl StatisticsImporter { @@ -41,7 +42,7 @@ impl StatisticsImporter { return Ok(()); } - info!(target: LOG_TARGET, "Importing {} torrents statistics from tracker {} ...", torrents.len().to_string().yellow(), self.tracker_url.yellow()); + info!(target: LOG_TARGET, "Importing {} torrents statistics from tracker {} ...", torrents.len().to_string().yellow(), self.tracker_url.to_string().yellow()); // Start the timer before the loop let start_time = Instant::now(); @@ -91,7 +92,7 @@ impl StatisticsImporter { return Ok(()); } - info!(target: LOG_TARGET, "Importing {} torrents statistics from tracker {} ...", torrents.len().to_string().yellow(), self.tracker_url.yellow()); + info!(target: LOG_TARGET, "Importing {} torrents statistics from tracker {} ...", torrents.len().to_string().yellow(), self.tracker_url.to_string().yellow()); // Import stats for all torrents in one request diff --git a/src/web/api/client/v1/contexts/settings/mod.rs b/src/web/api/client/v1/contexts/settings/mod.rs index 174a92ef..d762a98f 100644 --- a/src/web/api/client/v1/contexts/settings/mod.rs +++ b/src/web/api/client/v1/contexts/settings/mod.rs @@ -1,7 +1,9 @@ pub mod responses; use serde::{Deserialize, Serialize}; +use url::Url; +use crate::config::v1::tracker::ApiToken; use crate::config::{ Api as DomainApi, Auth as DomainAuth, Database as DomainDatabase, ImageCache as DomainImageCache, Mail as DomainMail, Network as DomainNetwork, Settings as DomainSettings, Tracker as DomainTracker, @@ -28,10 +30,10 @@ pub struct Website { #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub struct Tracker { - pub url: String, + pub url: Url, pub mode: String, - pub api_url: String, - pub token: String, + pub api_url: Url, + pub token: ApiToken, pub token_valid_seconds: u64, } diff --git a/src/web/api/server/v1/contexts/settings/mod.rs b/src/web/api/server/v1/contexts/settings/mod.rs index e5978890..fe9aeab3 100644 --- a/src/web/api/server/v1/contexts/settings/mod.rs +++ b/src/web/api/server/v1/contexts/settings/mod.rs @@ -36,7 +36,7 @@ //! "tracker": { //! "url": "udp://localhost:6969", //! "mode": "Public", -//! "api_url": "http://localhost:1212", +//! "api_url": "http://localhost:1212/", //! "token": "MyAccessToken", //! "token_valid_seconds": 7257600 //! }, @@ -102,7 +102,7 @@ //! --header "Content-Type: application/json" \ //! --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjp7InVzZXJfaWQiOjEsInVzZXJuYW1lIjoiaW5kZXhhZG1pbiIsImFkbWluaXN0cmF0b3IiOnRydWV9LCJleHAiOjE2ODYyMTU3ODh9.4k8ty27DiWwOk4WVcYEhIrAndhpXMRWnLZ3i_HlJnvI" \ //! --request POST \ -//! --data '{"website":{"name":"Torrust"},"tracker":{"url":"udp://localhost:6969","mode":"Public","api_url":"http://localhost:1212","token":"MyAccessToken","token_valid_seconds":7257600},"net":{"port":3001,"base_url":null},"auth":{"email_on_signup":"Optional","min_password_length":6,"max_password_length":64,"secret_key":"MaxVerstappenWC2021"},"database":{"connect_url":"sqlite://./storage/database/data.db?mode=rwc"},"mail":{"email_verification_enabled":false,"from":"example@email.com","reply_to":"noreply@email.com","username":"","password":"","server":"","port":25},"image_cache":{"max_request_timeout_ms":1000,"capacity":128000000,"entry_size_limit":4000000,"user_quota_period_seconds":3600,"user_quota_bytes":64000000},"api":{"default_torrent_page_size":10,"max_torrent_page_size":30},"tracker_statistics_importer":{"torrent_info_update_interval":3600}}' \ +//! --data '{"website":{"name":"Torrust"},"tracker":{"url":"udp://localhost:6969","mode":"Public","api_url":"http://localhost:1212/","token":"MyAccessToken","token_valid_seconds":7257600},"net":{"port":3001,"base_url":null},"auth":{"email_on_signup":"Optional","min_password_length":6,"max_password_length":64,"secret_key":"MaxVerstappenWC2021"},"database":{"connect_url":"sqlite://./storage/database/data.db?mode=rwc"},"mail":{"email_verification_enabled":false,"from":"example@email.com","reply_to":"noreply@email.com","username":"","password":"","server":"","port":25},"image_cache":{"max_request_timeout_ms":1000,"capacity":128000000,"entry_size_limit":4000000,"user_quota_period_seconds":3600,"user_quota_bytes":64000000},"api":{"default_torrent_page_size":10,"max_torrent_page_size":30},"tracker_statistics_importer":{"torrent_info_update_interval":3600}}' \ //! "http://127.0.0.1:3001/v1/settings" //! ``` //! diff --git a/tests/common/client.rs b/tests/common/client.rs index 9312ac6f..687b5044 100644 --- a/tests/common/client.rs +++ b/tests/common/client.rs @@ -21,15 +21,26 @@ impl Client { // todo: forms in POST requests can be passed by reference. fn base_path() -> String { - "/v1".to_string() + "v1".to_string() + } + + /// Remove last '/' char in the address if present. + /// + /// For example: to . + fn base_url(bind_address: &str) -> String { + let mut url = bind_address.to_owned(); + if url.ends_with('/') { + url.pop(); + } + url } pub fn unauthenticated(bind_address: &str) -> Self { - Self::new(ConnectionInfo::anonymous(bind_address, &Self::base_path())) + Self::new(ConnectionInfo::anonymous(&Self::base_url(bind_address), &Self::base_path())) } pub fn authenticated(bind_address: &str, token: &str) -> Self { - Self::new(ConnectionInfo::new(bind_address, &Self::base_path(), token)) + Self::new(ConnectionInfo::new(&Self::base_url(bind_address), &Self::base_path(), token)) } pub fn new(connection_info: ConnectionInfo) -> Self { @@ -340,9 +351,13 @@ impl Http { } fn base_url(&self, path: &str) -> String { - format!( - "http://{}{}{path}", + let url = format!( + "http://{}/{}{path}", // DevSkim: ignore DS137138 &self.connection_info.bind_address, &self.connection_info.base_path - ) + ); + + println!("URL: {url}"); + + url } } diff --git a/tests/common/contexts/settings/mod.rs b/tests/common/contexts/settings/mod.rs index 0eca1bb6..b72ca7d5 100644 --- a/tests/common/contexts/settings/mod.rs +++ b/tests/common/contexts/settings/mod.rs @@ -1,11 +1,13 @@ pub mod responses; use serde::{Deserialize, Serialize}; +use torrust_index::config::v1::tracker::ApiToken; use torrust_index::config::{ Api as DomainApi, Auth as DomainAuth, Database as DomainDatabase, ImageCache as DomainImageCache, Mail as DomainMail, Network as DomainNetwork, Settings as DomainSettings, Tracker as DomainTracker, TrackerStatisticsImporter as DomainTrackerStatisticsImporter, Website as DomainWebsite, }; +use url::Url; #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub struct Settings { @@ -27,10 +29,10 @@ pub struct Website { #[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] pub struct Tracker { - pub url: String, + pub url: Url, pub mode: String, - pub api_url: String, - pub token: String, + pub api_url: Url, + pub token: ApiToken, pub token_valid_seconds: u64, } diff --git a/tests/common/contexts/settings/responses.rs b/tests/common/contexts/settings/responses.rs index 096ef1f4..782a0703 100644 --- a/tests/common/contexts/settings/responses.rs +++ b/tests/common/contexts/settings/responses.rs @@ -1,4 +1,5 @@ use serde::Deserialize; +use url::Url; use super::Settings; @@ -15,7 +16,7 @@ pub struct PublicSettingsResponse { #[derive(Deserialize, PartialEq, Debug)] pub struct Public { pub website_name: String, - pub tracker_url: String, + pub tracker_url: Url, pub tracker_mode: String, pub email_on_signup: String, } diff --git a/tests/e2e/environment.rs b/tests/e2e/environment.rs index c81bf70a..f78c7c1b 100644 --- a/tests/e2e/environment.rs +++ b/tests/e2e/environment.rs @@ -1,5 +1,6 @@ use std::env; +use torrust_index::config::v1::tracker::ApiToken; use torrust_index::web::api::Version; use url::Url; @@ -98,9 +99,12 @@ impl TestEnv { settings.database.connect_url = connect_url.to_string(); } - "***".clone_into(&mut settings.tracker.token); + settings.tracker.token = ApiToken::new("***"); + "***".clone_into(&mut settings.mail.password); + "***".clone_into(&mut settings.auth.secret_key); + Some(settings) } None => None, diff --git a/tests/e2e/web/api/v1/contexts/torrent/contract.rs b/tests/e2e/web/api/v1/contexts/torrent/contract.rs index 03ef94fb..bc6f0369 100644 --- a/tests/e2e/web/api/v1/contexts/torrent/contract.rs +++ b/tests/e2e/web/api/v1/contexts/torrent/contract.rs @@ -172,7 +172,7 @@ mod for_guests { let torrent_details_response: TorrentDetailsResponse = serde_json::from_str(&response.body).unwrap(); - let tracker_url = env.server_settings().unwrap().tracker.url; + let tracker_url = env.server_settings().unwrap().tracker.url.to_string(); let encoded_tracker_url = urlencoding::encode(&tracker_url); let expected_torrent = TorrentDetails { @@ -194,9 +194,9 @@ mod for_guests { path: vec![test_torrent.file_info.files[0].clone()], // Using one file torrent for testing: content_size = first file size length: test_torrent.file_info.content_size, - md5sum: None, + md5sum: None, // DevSkim: ignore DS126858 }], - trackers: vec![tracker_url.clone()], + trackers: vec![tracker_url.clone().to_string()], magnet_link: format!( // cspell:disable-next-line "magnet:?xt=urn:btih:{}&dn={}&tr={}", diff --git a/tests/e2e/web/api/v1/contexts/torrent/steps.rs b/tests/e2e/web/api/v1/contexts/torrent/steps.rs index 60f37f9b..27b21c7d 100644 --- a/tests/e2e/web/api/v1/contexts/torrent/steps.rs +++ b/tests/e2e/web/api/v1/contexts/torrent/steps.rs @@ -28,7 +28,7 @@ pub async fn upload_torrent(uploader: &LoggedInUserData, torrent: &TorrentIndexI let res = serde_json::from_str::(&response.body); if res.is_err() { - println!("Error deserializing response: {res:?}"); + println!("Error deserializing response: {res:?}. Body: {0}", response.body); } TorrentListedInIndex::from(torrent.clone(), res.unwrap().data.torrent_id)