From 23916a60a2e6881ec4336f1f995e57e2fea8c54d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 22 Nov 2022 19:13:43 +0000 Subject: [PATCH 1/3] fix: [#108] revert change in auth key generation endpoint The response for the enpoint POST /api/key/:seconds_valid should be: ```json { "key": "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM", "valid_until": 1674804892 } ``` instead of: ```json { "key": "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM", "valid_until": { "secs": 1674804892, "nanos": 423855037 } } ``` It was propagated to the API after changing the internal struct `AuthKey` from: ```rust pub struct AuthKey { pub key: String, pub valid_until: Option, } ``` to: ```rust pub struct AuthKey { pub key: String, pub valid_until: Option, } ``` --- src/api/mod.rs | 1 + src/api/resources/auth_key_resource.rs | 57 ++++++++++++++++++++++++++ src/api/resources/mod.rs | 9 ++++ src/api/server.rs | 4 +- 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/api/resources/auth_key_resource.rs create mode 100644 src/api/resources/mod.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index 74f47ad34..e08417133 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1 +1,2 @@ pub mod server; +pub mod resources; diff --git a/src/api/resources/auth_key_resource.rs b/src/api/resources/auth_key_resource.rs new file mode 100644 index 000000000..4f74266f6 --- /dev/null +++ b/src/api/resources/auth_key_resource.rs @@ -0,0 +1,57 @@ +use serde::{Deserialize, Serialize}; + +use crate::key::AuthKey; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct AuthKeyResource { + pub key: String, + pub valid_until: Option, +} + +impl AuthKeyResource { + pub fn from_auth_key(auth_key: &AuthKey) -> Self { + Self { + key: auth_key.key.clone(), + valid_until: auth_key.valid_until.map(|duration| duration.as_secs()), + } + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::AuthKeyResource; + use crate::key::AuthKey; + use crate::protocol::clock::{DefaultClock, TimeNow}; + + #[test] + fn it_should_be_instantiated_from_an_auth_key() { + let expire_time = DefaultClock::add(&Duration::new(60, 0)).unwrap(); + + let auth_key_resource = AuthKey { + key: "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM".to_string(), // cspell:disable-line + valid_until: Some(expire_time), + }; + + assert_eq!( + AuthKeyResource::from_auth_key(&auth_key_resource), + AuthKeyResource { + key: "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM".to_string(), // cspell:disable-line + valid_until: Some(expire_time.as_secs()) + } + ) + } + + #[test] + fn it_should_be_converted_to_json() { + assert_eq!( + serde_json::to_string(&AuthKeyResource { + key: "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM".to_string(), // cspell:disable-line + valid_until: Some(60) + }) + .unwrap(), + "{\"key\":\"IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM\",\"valid_until\":60}" // cspell:disable-line + ); + } +} diff --git a/src/api/resources/mod.rs b/src/api/resources/mod.rs new file mode 100644 index 000000000..f7d24ee86 --- /dev/null +++ b/src/api/resources/mod.rs @@ -0,0 +1,9 @@ +//! These are the Rest API resources. +//! +//! WIP. Not all endpoints have their resource structs. +//! +//! - [x] AuthKeys +//! - [ ] ... +//! - [ ] ... +//! - [ ] ... +pub mod auth_key_resource; diff --git a/src/api/server.rs b/src/api/server.rs index 5a604aa0c..89d3bb38d 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -11,6 +11,8 @@ use crate::peer::TorrentPeer; use crate::protocol::common::*; use crate::tracker::TorrentTracker; +use super::resources::auth_key_resource::AuthKeyResource; + #[derive(Deserialize, Debug)] struct TorrentInfoQuery { offset: Option, @@ -267,7 +269,7 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl warp }) .and_then(|(seconds_valid, tracker): (u64, Arc)| async move { match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await { - Ok(auth_key) => Ok(warp::reply::json(&auth_key)), + Ok(auth_key) => Ok(warp::reply::json(&AuthKeyResource::from_auth_key(&auth_key))), Err(..) => Err(warp::reject::custom(ActionStatus::Err { reason: "failed to generate key".into(), })), From ede046082660a0bf69c1518e329413aac1959634 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 22 Nov 2022 19:17:09 +0000 Subject: [PATCH 2/3] feat: [#108] add dev dependency reqwest Added for API end to end tests. --- Cargo.lock | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce66efa09..e3a6d9c09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -528,6 +528,15 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -941,9 +950,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ "bytes", "futures-channel", @@ -963,6 +972,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.51" @@ -1035,6 +1057,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ipnet" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" + [[package]] name = "itertools" version = "0.10.5" @@ -1857,6 +1885,43 @@ dependencies = [ "winapi", ] +[[package]] +name = "reqwest" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -2542,6 +2607,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.23.4" @@ -2621,6 +2696,7 @@ dependencies = [ "r2d2_mysql", "r2d2_sqlite", "rand", + "reqwest", "serde", "serde_bencode", "serde_json", @@ -2887,6 +2963,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.83" @@ -3076,6 +3164,15 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "wyz" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index b2b256a2c..80e9009f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,3 +62,4 @@ uuid = { version = "1", features = ["v4"] } [dev-dependencies] mockall = "0.11" +reqwest = { version = "0.11.13", features = ["json"] } From 409f82af4cb76936da389b37deca05d8321710fa Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 22 Nov 2022 19:18:55 +0000 Subject: [PATCH 3/3] test: [#108] add e2e test for auth key generation API endpoint --- .gitignore | 1 + src/api/mod.rs | 2 +- src/api/resources/auth_key_resource.rs | 56 +++++++++--- src/api/resources/mod.rs | 4 +- src/api/server.rs | 5 +- tests/api.rs | 119 +++++++++++++++++++++++++ tests/common/mod.rs | 8 ++ tests/udp.rs | 19 ++-- 8 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 tests/api.rs create mode 100644 tests/common/mod.rs diff --git a/.gitignore b/.gitignore index e2956b2d6..ba9ceeb53 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /config.toml /data.db /.vscode/launch.json + diff --git a/src/api/mod.rs b/src/api/mod.rs index e08417133..46ad24218 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,2 +1,2 @@ -pub mod server; pub mod resources; +pub mod server; diff --git a/src/api/resources/auth_key_resource.rs b/src/api/resources/auth_key_resource.rs index 4f74266f6..c38b7cc18 100644 --- a/src/api/resources/auth_key_resource.rs +++ b/src/api/resources/auth_key_resource.rs @@ -1,6 +1,9 @@ +use std::convert::From; + use serde::{Deserialize, Serialize}; use crate::key::AuthKey; +use crate::protocol::clock::DurationSinceUnixEpoch; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct AuthKeyResource { @@ -8,11 +11,22 @@ pub struct AuthKeyResource { pub valid_until: Option, } -impl AuthKeyResource { - pub fn from_auth_key(auth_key: &AuthKey) -> Self { - Self { - key: auth_key.key.clone(), - valid_until: auth_key.valid_until.map(|duration| duration.as_secs()), +impl From for AuthKey { + fn from(auth_key_resource: AuthKeyResource) -> Self { + AuthKey { + key: auth_key_resource.key, + valid_until: auth_key_resource + .valid_until + .map(|valid_until| DurationSinceUnixEpoch::new(valid_until, 0)), + } + } +} + +impl From for AuthKeyResource { + fn from(auth_key: AuthKey) -> Self { + AuthKeyResource { + key: auth_key.key, + valid_until: auth_key.valid_until.map(|valid_until| valid_until.as_secs()), } } } @@ -26,25 +40,43 @@ mod tests { use crate::protocol::clock::{DefaultClock, TimeNow}; #[test] - fn it_should_be_instantiated_from_an_auth_key() { - let expire_time = DefaultClock::add(&Duration::new(60, 0)).unwrap(); + fn it_should_be_convertible_into_an_auth_key() { + let duration_in_secs = 60; + + let auth_key_resource = AuthKeyResource { + key: "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM".to_string(), // cspell:disable-line + valid_until: Some(duration_in_secs), + }; + + assert_eq!( + AuthKey::from(auth_key_resource), + AuthKey { + key: "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM".to_string(), // cspell:disable-line + valid_until: Some(DefaultClock::add(&Duration::new(duration_in_secs, 0)).unwrap()) + } + ) + } + + #[test] + fn it_should_be_convertible_from_an_auth_key() { + let duration_in_secs = 60; - let auth_key_resource = AuthKey { + let auth_key = AuthKey { key: "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM".to_string(), // cspell:disable-line - valid_until: Some(expire_time), + valid_until: Some(DefaultClock::add(&Duration::new(duration_in_secs, 0)).unwrap()), }; assert_eq!( - AuthKeyResource::from_auth_key(&auth_key_resource), + AuthKeyResource::from(auth_key), AuthKeyResource { key: "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM".to_string(), // cspell:disable-line - valid_until: Some(expire_time.as_secs()) + valid_until: Some(duration_in_secs) } ) } #[test] - fn it_should_be_converted_to_json() { + fn it_should_be_convertible_into_json() { assert_eq!( serde_json::to_string(&AuthKeyResource { key: "IaWDneuFNZi8IB4MPA3qW1CD0M30EZSM".to_string(), // cspell:disable-line diff --git a/src/api/resources/mod.rs b/src/api/resources/mod.rs index f7d24ee86..4b4f2214c 100644 --- a/src/api/resources/mod.rs +++ b/src/api/resources/mod.rs @@ -1,7 +1,7 @@ //! These are the Rest API resources. -//! +//! //! WIP. Not all endpoints have their resource structs. -//! +//! //! - [x] AuthKeys //! - [ ] ... //! - [ ] ... diff --git a/src/api/server.rs b/src/api/server.rs index 89d3bb38d..9f215710e 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -7,12 +7,11 @@ use std::time::Duration; use serde::{Deserialize, Serialize}; use warp::{filters, reply, serve, Filter}; +use super::resources::auth_key_resource::AuthKeyResource; use crate::peer::TorrentPeer; use crate::protocol::common::*; use crate::tracker::TorrentTracker; -use super::resources::auth_key_resource::AuthKeyResource; - #[derive(Deserialize, Debug)] struct TorrentInfoQuery { offset: Option, @@ -269,7 +268,7 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl warp }) .and_then(|(seconds_valid, tracker): (u64, Arc)| async move { match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await { - Ok(auth_key) => Ok(warp::reply::json(&AuthKeyResource::from_auth_key(&auth_key))), + Ok(auth_key) => Ok(warp::reply::json(&AuthKeyResource::from(auth_key))), Err(..) => Err(warp::reject::custom(ActionStatus::Err { reason: "failed to generate key".into(), })), diff --git a/tests/api.rs b/tests/api.rs new file mode 100644 index 000000000..38966a81b --- /dev/null +++ b/tests/api.rs @@ -0,0 +1,119 @@ +/// Integration tests for the tracker API +/// +/// cargo test tracker_api -- --nocapture +extern crate rand; + +mod common; + +mod tracker_api { + use core::panic; + use std::env; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + + use tokio::task::JoinHandle; + use tokio::time::{sleep, Duration}; + use torrust_tracker::api::resources::auth_key_resource::AuthKeyResource; + use torrust_tracker::jobs::tracker_api; + use torrust_tracker::tracker::key::AuthKey; + use torrust_tracker::tracker::statistics::StatsTracker; + use torrust_tracker::tracker::TorrentTracker; + use torrust_tracker::{ephemeral_instance_keys, logging, static_time, Configuration}; + + use crate::common::ephemeral_random_port; + + #[tokio::test] + async fn should_generate_a_new_auth_key() { + let configuration = tracker_configuration(); + let api_server = new_running_api_server(configuration.clone()).await; + + let bind_address = api_server.bind_address.unwrap().clone(); + let seconds_valid = 60; + let api_token = configuration.http_api.access_tokens.get_key_value("admin").unwrap().1.clone(); + + let url = format!("http://{}/api/key/{}?token={}", &bind_address, &seconds_valid, &api_token); + + let auth_key: AuthKeyResource = reqwest::Client::new().post(url).send().await.unwrap().json().await.unwrap(); + + // Verify the key with the tracker + assert!(api_server + .tracker + .unwrap() + .verify_auth_key(&AuthKey::from(auth_key)) + .await + .is_ok()); + } + + fn tracker_configuration() -> Arc { + let mut config = Configuration::default(); + config.log_level = Some("off".to_owned()); + + config.http_api.bind_address = format!("127.0.0.1:{}", ephemeral_random_port()); + + // Temp database + let temp_directory = env::temp_dir(); + let temp_file = temp_directory.join("data.db"); + config.db_path = temp_file.to_str().unwrap().to_owned(); + + Arc::new(config) + } + + async fn new_running_api_server(configuration: Arc) -> ApiServer { + let mut api_server = ApiServer::new(); + api_server.start(configuration).await; + api_server + } + + pub struct ApiServer { + pub started: AtomicBool, + pub job: Option>, + pub bind_address: Option, + pub tracker: Option>, + } + + impl ApiServer { + pub fn new() -> Self { + Self { + started: AtomicBool::new(false), + job: None, + bind_address: None, + tracker: None, + } + } + + pub async fn start(&mut self, configuration: Arc) { + if !self.started.load(Ordering::Relaxed) { + self.bind_address = Some(configuration.http_api.bind_address.clone()); + + // Set the time of Torrust app starting + lazy_static::initialize(&static_time::TIME_AT_APP_START); + + // Initialize the Ephemeral Instance Random Seed + lazy_static::initialize(&ephemeral_instance_keys::RANDOM_SEED); + + // Initialize stats tracker + let (stats_event_sender, stats_repository) = StatsTracker::new_active_instance(); + + // Initialize Torrust tracker + let tracker = match TorrentTracker::new(configuration.clone(), Some(stats_event_sender), stats_repository) { + Ok(tracker) => Arc::new(tracker), + Err(error) => { + panic!("{}", error) + } + }; + self.tracker = Some(tracker.clone()); + + // Initialize logging + logging::setup_logging(&configuration); + + // Start the HTTP API job + self.job = Some(tracker_api::start_job(&configuration, tracker.clone())); + + self.started.store(true, Ordering::Relaxed); + + // Wait to give time to the API server to be ready to accept requests + sleep(Duration::from_millis(100)).await; + } + } + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 000000000..5fd484cf5 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,8 @@ +use rand::{thread_rng, Rng}; + +pub fn ephemeral_random_port() -> u16 { + // todo: this may produce random test failures because two tests can try to bind the same port. + // We could create a pool of available ports (with read/write lock) + let mut rng = thread_rng(); + rng.gen_range(49152..65535) +} diff --git a/tests/udp.rs b/tests/udp.rs index c88dc9885..ab96259c5 100644 --- a/tests/udp.rs +++ b/tests/udp.rs @@ -3,6 +3,8 @@ /// cargo test udp_tracker_server -- --nocapture extern crate rand; +mod common; + mod udp_tracker_server { use core::panic; use std::io::Cursor; @@ -14,14 +16,15 @@ mod udp_tracker_server { AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, InfoHash, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Request, Response, ScrapeRequest, TransactionId, }; - use rand::{thread_rng, Rng}; use tokio::net::UdpSocket; use tokio::task::JoinHandle; use torrust_tracker::jobs::udp_tracker; use torrust_tracker::tracker::statistics::StatsTracker; use torrust_tracker::tracker::TorrentTracker; use torrust_tracker::udp::MAX_PACKET_SIZE; - use torrust_tracker::{logging, static_time, Configuration}; + use torrust_tracker::{ephemeral_instance_keys, logging, static_time, Configuration}; + + use crate::common::ephemeral_random_port; fn tracker_configuration() -> Arc { let mut config = Configuration::default(); @@ -50,6 +53,9 @@ mod udp_tracker_server { // Set the time of Torrust app starting lazy_static::initialize(&static_time::TIME_AT_APP_START); + // Initialize the Ephemeral Instance Random Seed + lazy_static::initialize(&ephemeral_instance_keys::RANDOM_SEED); + // Initialize stats tracker let (stats_event_sender, stats_repository) = StatsTracker::new_active_instance(); @@ -162,15 +168,6 @@ mod udp_tracker_server { [0; MAX_PACKET_SIZE] } - /// Generates a random ephemeral port for a client source address - fn ephemeral_random_port() -> u16 { - // todo: this may produce random test failures because two tests can try to bind the same port. - // We could either use the same client for all tests (slower) or - // create a pool of available ports (with read/write lock) - let mut rng = thread_rng(); - rng.gen_range(49152..65535) - } - /// Generates the source address for the UDP client fn source_address(port: u16) -> String { format!("127.0.0.1:{}", port)