use torrust_tracker_test_helpers::configuration;
use crate::servers::http::Started;
#[tokio::test]
async fn environment_should_be_started_and_stopped() {
let env = Started::new(&configuration::ephemeral().into()).await;
env.stop().await;
}
mod for_all_config_modes {
use torrust_tracker::servers::http::v1::handlers::health_check::{Report, Status};
use torrust_tracker_test_helpers::configuration;
use tracing::level_filters::LevelFilter;
use crate::common::logging::{tracing_stderr_init, INIT};
use crate::servers::http::client::Client;
use crate::servers::http::Started;
#[tokio::test]
async fn health_check_endpoint_should_return_ok_if_the_http_tracker_is_running() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await;
let response = Client::new(*env.bind_address()).health_check().await;
assert_eq!(response.status(), 200);
assert_eq!(response.headers().get("content-type").unwrap(), "application/json");
assert_eq!(response.json::<Report>().await.unwrap(), Report { status: Status::Ok });
env.stop().await;
}
mod and_running_on_reverse_proxy {
use torrust_tracker_test_helpers::configuration;
use tracing::level_filters::LevelFilter;
use crate::common::logging::{tracing_stderr_init, INIT};
use crate::servers::http::asserts::assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response;
use crate::servers::http::client::Client;
use crate::servers::http::requests::announce::QueryBuilder;
use crate::servers::http::Started;
#[tokio::test]
async fn should_fail_when_the_http_request_does_not_include_the_xff_http_request_header() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await;
let params = QueryBuilder::default().query().params();
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await;
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await;
let params = QueryBuilder::default().query().params();
let response = Client::new(*env.bind_address())
.get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP")
.await;
assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await;
env.stop().await;
}
}
mod receiving_an_announce_request {
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV6};
use std::str::FromStr;
use aquatic_udp_protocol::PeerId;
use local_ip_address::local_ip;
use reqwest::{Response, StatusCode};
use tokio::net::TcpListener;
use torrust_tracker_primitives::info_hash::InfoHash;
use torrust_tracker_primitives::peer::fixture::PeerBuilder;
use torrust_tracker_test_helpers::configuration;
use tracing::level_filters::LevelFilter;
use crate::common::fixtures::invalid_info_hashes;
use crate::common::logging::{tracing_stderr_init, INIT};
use crate::servers::http::asserts::{
assert_announce_response, assert_bad_announce_request_error_response, assert_cannot_parse_query_param_error_response,
assert_cannot_parse_query_params_error_response, assert_compact_announce_response, assert_empty_announce_response,
assert_is_announce_response, assert_missing_query_params_for_announce_request_error_response,
};
use crate::servers::http::client::Client;
use crate::servers::http::requests::announce::{Compact, QueryBuilder};
use crate::servers::http::responses::announce::{Announce, CompactPeer, CompactPeerList, DictionaryPeer};
use crate::servers::http::{responses, Started};
#[tokio::test]
async fn it_should_start_and_stop() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
env.stop().await;
}
#[tokio::test]
async fn should_respond_if_only_the_mandatory_fields_are_provided() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
params.remove_optional_params();
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_is_announce_response(response).await;
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_url_query_component_is_empty() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let response = Client::new(*env.bind_address()).get("announce").await;
assert_missing_query_params_for_announce_request_error_response(response).await;
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_url_query_parameters_are_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let invalid_query_param = "a=b=c";
let response = Client::new(*env.bind_address())
.get(&format!("announce?{invalid_query_param}"))
.await;
assert_cannot_parse_query_param_error_response(response, "invalid param a=b=c").await;
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_a_mandatory_field_is_missing() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
params.info_hash = None;
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "missing param info_hash").await;
let mut params = QueryBuilder::default().query().params();
params.peer_id = None;
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "missing param peer_id").await;
let mut params = QueryBuilder::default().query().params();
params.port = None;
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "missing param port").await;
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_info_hash_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
for invalid_value in &invalid_info_hashes() {
params.set("info_hash", invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_cannot_parse_query_params_error_response(response, "").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_not_fail_when_the_peer_address_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
params.peer_addr = Some("INVALID-IP-ADDRESS".to_string());
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_is_announce_response(response).await;
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_downloaded_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
let invalid_values = ["-1", "1.1", "a"];
for invalid_value in invalid_values {
params.set("downloaded", invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "invalid param value").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_uploaded_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
let invalid_values = ["-1", "1.1", "a"];
for invalid_value in invalid_values {
params.set("uploaded", invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "invalid param value").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_peer_id_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
let invalid_values = [
"0",
"-1",
"1.1",
"a",
"-qB0000000000000000", "-qB000000000000000000", ];
for invalid_value in invalid_values {
params.set("peer_id", invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "invalid param value").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_port_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
let invalid_values = ["-1", "1.1", "a"];
for invalid_value in invalid_values {
params.set("port", invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "invalid param value").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_left_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
let invalid_values = ["-1", "1.1", "a"];
for invalid_value in invalid_values {
params.set("left", invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "invalid param value").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_event_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
let invalid_values = [
"0",
"-1",
"1.1",
"a",
"Started", "Stopped", "Completed", ];
for invalid_value in invalid_values {
params.set("event", invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "invalid param value").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_compact_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
let invalid_values = ["-1", "1.1", "a"];
for invalid_value in invalid_values {
params.set("compact", invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "invalid param value").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_numwant_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral().into()).await;
let mut params = QueryBuilder::default().query().params();
let invalid_values = ["-1", "1.1", "a"];
for invalid_value in invalid_values {
params.set("numwant", invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_bad_announce_request_error_response(response, "invalid param value").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let response = Client::new(*env.bind_address())
.announce(
&QueryBuilder::default()
.with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap())
.query(),
)
.await;
let announce_policy = env.tracker.get_announce_policy();
assert_announce_response(
response,
&Announce {
complete: 1, incomplete: 0,
interval: announce_policy.interval,
min_interval: announce_policy.interval_min,
peers: vec![],
},
)
.await;
env.stop().await;
}
#[tokio::test]
async fn should_return_the_list_of_previously_announced_peers() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let previously_announced_peer = PeerBuilder::default().with_peer_id(&PeerId(*b"-qB00000000000000001")).build();
env.add_torrent_peer(&info_hash, &previously_announced_peer);
let response = Client::new(*env.bind_address())
.announce(
&QueryBuilder::default()
.with_info_hash(&info_hash)
.with_peer_id(&PeerId(*b"-qB00000000000000002"))
.query(),
)
.await;
let announce_policy = env.tracker.get_announce_policy();
assert_announce_response(
response,
&Announce {
complete: 2,
incomplete: 0,
interval: announce_policy.interval,
min_interval: announce_policy.interval_min,
peers: vec![DictionaryPeer::from(previously_announced_peer)],
},
)
.await;
env.stop().await;
}
#[tokio::test]
async fn should_return_the_list_of_previously_announced_peers_including_peers_using_ipv4_and_ipv6() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let peer_using_ipv4 = PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 8080))
.build();
env.add_torrent_peer(&info_hash, &peer_using_ipv4);
let peer_using_ipv6 = PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000002"))
.with_peer_addr(&SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)),
8080,
))
.build();
env.add_torrent_peer(&info_hash, &peer_using_ipv6);
let response = Client::new(*env.bind_address())
.announce(
&QueryBuilder::default()
.with_info_hash(&info_hash)
.with_peer_id(&PeerId(*b"-qB00000000000000003"))
.query(),
)
.await;
let announce_policy = env.tracker.get_announce_policy();
assert_announce_response(
response,
&Announce {
complete: 3,
incomplete: 0,
interval: announce_policy.interval,
min_interval: announce_policy.interval_min,
peers: vec![DictionaryPeer::from(peer_using_ipv4), DictionaryPeer::from(peer_using_ipv6)],
},
)
.await;
env.stop().await;
}
#[tokio::test]
async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_peer_id_even_if_the_ip_is_different() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let peer = PeerBuilder::default().build();
env.add_torrent_peer(&info_hash, &peer);
let announce_query = QueryBuilder::default()
.with_info_hash(&info_hash)
.with_peer_id(&peer.peer_id)
.query();
assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr);
let response = Client::new(*env.bind_address()).announce(&announce_query).await;
assert_empty_announce_response(response).await;
env.stop().await;
}
#[tokio::test]
async fn should_return_the_compact_response() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let previously_announced_peer = PeerBuilder::default().with_peer_id(&PeerId(*b"-qB00000000000000001")).build();
env.add_torrent_peer(&info_hash, &previously_announced_peer);
let response = Client::new(*env.bind_address())
.announce(
&QueryBuilder::default()
.with_info_hash(&info_hash)
.with_peer_id(&PeerId(*b"-qB00000000000000002"))
.with_compact(Compact::Accepted)
.query(),
)
.await;
let expected_response = responses::announce::Compact {
complete: 2,
incomplete: 0,
interval: 120,
min_interval: 120,
peers: CompactPeerList::new([CompactPeer::new(&previously_announced_peer.peer_addr)].to_vec()),
};
assert_compact_announce_response(response, &expected_response).await;
env.stop().await;
}
#[tokio::test]
async fn should_not_return_the_compact_response_by_default() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let previously_announced_peer = PeerBuilder::default().with_peer_id(&PeerId(*b"-qB00000000000000001")).build();
env.add_torrent_peer(&info_hash, &previously_announced_peer);
let response = Client::new(*env.bind_address())
.announce(
&QueryBuilder::default()
.with_info_hash(&info_hash)
.with_peer_id(&PeerId(*b"-qB00000000000000002"))
.without_compact()
.query(),
)
.await;
assert!(!is_a_compact_announce_response(response).await);
env.stop().await;
}
async fn is_a_compact_announce_response(response: Response) -> bool {
let bytes = response.bytes().await.unwrap();
let compact_announce = serde_bencode::from_bytes::<responses::announce::DeserializedCompact>(&bytes);
compact_announce.is_ok()
}
#[tokio::test]
async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
Client::new(*env.bind_address())
.announce(&QueryBuilder::default().query())
.await;
let stats = env.tracker.get_stats().await;
assert_eq!(stats.tcp4_connections_handled, 1);
drop(stats);
env.stop().await;
}
#[tokio::test]
async fn should_increase_the_number_of_tcp6_connections_handled_in_statistics() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
if TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0))
.await
.is_err()
{
return; }
let env = Started::new(&configuration::ephemeral_ipv6().into()).await;
Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap())
.announce(&QueryBuilder::default().query())
.await;
let stats = env.tracker.get_stats().await;
assert_eq!(stats.tcp6_connections_handled, 1);
drop(stats);
env.stop().await;
}
#[tokio::test]
async fn should_not_increase_the_number_of_tcp6_connections_handled_if_the_client_is_not_using_an_ipv6_ip() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
Client::new(*env.bind_address())
.announce(
&QueryBuilder::default()
.with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))
.query(),
)
.await;
let stats = env.tracker.get_stats().await;
assert_eq!(stats.tcp6_connections_handled, 0);
drop(stats);
env.stop().await;
}
#[tokio::test]
async fn should_increase_the_number_of_tcp4_announce_requests_handled_in_statistics() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
Client::new(*env.bind_address())
.announce(&QueryBuilder::default().query())
.await;
let stats = env.tracker.get_stats().await;
assert_eq!(stats.tcp4_announces_handled, 1);
drop(stats);
env.stop().await;
}
#[tokio::test]
async fn should_increase_the_number_of_tcp6_announce_requests_handled_in_statistics() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
if TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0))
.await
.is_err()
{
return; }
let env = Started::new(&configuration::ephemeral_ipv6().into()).await;
Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap())
.announce(&QueryBuilder::default().query())
.await;
let stats = env.tracker.get_stats().await;
assert_eq!(stats.tcp6_announces_handled, 1);
drop(stats);
env.stop().await;
}
#[tokio::test]
async fn should_not_increase_the_number_of_tcp6_announce_requests_handled_if_the_client_is_not_using_an_ipv6_ip() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
Client::new(*env.bind_address())
.announce(
&QueryBuilder::default()
.with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))
.query(),
)
.await;
let stats = env.tracker.get_stats().await;
assert_eq!(stats.tcp6_announces_handled, 0);
drop(stats);
env.stop().await;
}
#[tokio::test]
async fn should_assign_to_the_peer_ip_the_remote_client_ip_instead_of_the_peer_address_in_the_request_param() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let client_ip = local_ip().unwrap();
let announce_query = QueryBuilder::default()
.with_info_hash(&info_hash)
.with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap())
.query();
{
let client = Client::bind(*env.bind_address(), client_ip);
let status = client.announce(&announce_query).await.status();
assert_eq!(status, StatusCode::OK);
}
let peers = env.tracker.get_torrent_peers(&info_hash);
let peer_addr = peers[0].peer_addr;
assert_eq!(peer_addr.ip(), client_ip);
assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
env.stop().await;
}
#[tokio::test]
async fn when_the_client_ip_is_a_loopback_ipv4_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration(
) {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env =
Started::new(&configuration::ephemeral_with_external_ip(IpAddr::from_str("2.137.87.41").unwrap()).into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap();
let client_ip = loopback_ip;
let announce_query = QueryBuilder::default()
.with_info_hash(&info_hash)
.with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap())
.query();
{
let client = Client::bind(*env.bind_address(), client_ip);
let status = client.announce(&announce_query).await.status();
assert_eq!(status, StatusCode::OK);
}
let peers = env.tracker.get_torrent_peers(&info_hash);
let peer_addr = peers[0].peer_addr;
assert_eq!(peer_addr.ip(), env.tracker.get_maybe_external_ip().unwrap());
assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
env.stop().await;
}
#[tokio::test]
async fn when_the_client_ip_is_a_loopback_ipv6_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration(
) {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(
&configuration::ephemeral_with_external_ip(IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap())
.into(),
)
.await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap();
let client_ip = loopback_ip;
let announce_query = QueryBuilder::default()
.with_info_hash(&info_hash)
.with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap())
.query();
{
let client = Client::bind(*env.bind_address(), client_ip);
let status = client.announce(&announce_query).await.status();
assert_eq!(status, StatusCode::OK);
}
let peers = env.tracker.get_torrent_peers(&info_hash);
let peer_addr = peers[0].peer_addr;
assert_eq!(peer_addr.ip(), env.tracker.get_maybe_external_ip().unwrap());
assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap());
env.stop().await;
}
#[tokio::test]
async fn when_the_tracker_is_behind_a_reverse_proxy_it_should_assign_to_the_peer_ip_the_ip_in_the_x_forwarded_for_http_header(
) {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_with_reverse_proxy().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query();
{
let client = Client::new(*env.bind_address());
let status = client
.announce_with_header(
&announce_query,
"X-Forwarded-For",
"203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178",
)
.await
.status();
assert_eq!(status, StatusCode::OK);
}
let peers = env.tracker.get_torrent_peers(&info_hash);
let peer_addr = peers[0].peer_addr;
assert_eq!(peer_addr.ip(), IpAddr::from_str("150.172.238.178").unwrap());
env.stop().await;
}
}
mod receiving_an_scrape_request {
use std::net::{IpAddr, Ipv6Addr, SocketAddrV6};
use std::str::FromStr;
use aquatic_udp_protocol::PeerId;
use tokio::net::TcpListener;
use torrust_tracker_primitives::info_hash::InfoHash;
use torrust_tracker_primitives::peer::fixture::PeerBuilder;
use torrust_tracker_test_helpers::configuration;
use tracing::level_filters::LevelFilter;
use crate::common::fixtures::invalid_info_hashes;
use crate::common::logging::{tracing_stderr_init, INIT};
use crate::servers::http::asserts::{
assert_cannot_parse_query_params_error_response, assert_missing_query_params_for_scrape_request_error_response,
assert_scrape_response,
};
use crate::servers::http::client::Client;
use crate::servers::http::requests::scrape::QueryBuilder;
use crate::servers::http::responses::scrape::{self, File, ResponseBuilder};
use crate::servers::http::{requests, Started};
#[tokio::test]
#[allow(dead_code)]
async fn should_fail_when_the_request_is_empty() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let response = Client::new(*env.bind_address()).get("scrape").await;
assert_missing_query_params_for_scrape_request_error_response(response).await;
env.stop().await;
}
#[tokio::test]
async fn should_fail_when_the_info_hash_param_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let mut params = QueryBuilder::default().query().params();
for invalid_value in &invalid_info_hashes() {
params.set_one_info_hash_param(invalid_value);
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
assert_cannot_parse_query_params_error_response(response, "").await;
}
env.stop().await;
}
#[tokio::test]
async fn should_return_the_file_with_the_incomplete_peer_when_there_is_one_peer_with_bytes_pending_to_download() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
env.add_torrent_peer(
&info_hash,
&PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_bytes_pending_to_download(1)
.build(),
);
let response = Client::new(*env.bind_address())
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
let expected_scrape_response = ResponseBuilder::default()
.add_file(
info_hash.bytes(),
File {
complete: 0,
downloaded: 0,
incomplete: 1,
},
)
.build();
assert_scrape_response(response, &expected_scrape_response).await;
env.stop().await;
}
#[tokio::test]
async fn should_return_the_file_with_the_complete_peer_when_there_is_one_peer_with_no_bytes_pending_to_download() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
env.add_torrent_peer(
&info_hash,
&PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_no_bytes_pending_to_download()
.build(),
);
let response = Client::new(*env.bind_address())
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
let expected_scrape_response = ResponseBuilder::default()
.add_file(
info_hash.bytes(),
File {
complete: 1,
downloaded: 0,
incomplete: 0,
},
)
.build();
assert_scrape_response(response, &expected_scrape_response).await;
env.stop().await;
}
#[tokio::test]
async fn should_return_a_file_with_zeroed_values_when_there_are_no_peers() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let response = Client::new(*env.bind_address())
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
assert_scrape_response(response, &scrape::Response::with_one_file(info_hash.bytes(), File::zeroed())).await;
env.stop().await;
}
#[tokio::test]
async fn should_accept_multiple_infohashes() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash1 = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let info_hash2 = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap();
let response = Client::new(*env.bind_address())
.scrape(
&requests::scrape::QueryBuilder::default()
.add_info_hash(&info_hash1)
.add_info_hash(&info_hash2)
.query(),
)
.await;
let expected_scrape_response = ResponseBuilder::default()
.add_file(info_hash1.bytes(), File::zeroed())
.add_file(info_hash2.bytes(), File::zeroed())
.build();
assert_scrape_response(response, &expected_scrape_response).await;
env.stop().await;
}
#[tokio::test]
async fn should_increase_the_number_ot_tcp4_scrape_requests_handled_in_statistics() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_public().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
Client::new(*env.bind_address())
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
let stats = env.tracker.get_stats().await;
assert_eq!(stats.tcp4_scrapes_handled, 1);
drop(stats);
env.stop().await;
}
#[tokio::test]
async fn should_increase_the_number_ot_tcp6_scrape_requests_handled_in_statistics() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
if TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0))
.await
.is_err()
{
return; }
let env = Started::new(&configuration::ephemeral_ipv6().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
Client::bind(*env.bind_address(), IpAddr::from_str("::1").unwrap())
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
let stats = env.tracker.get_stats().await;
assert_eq!(stats.tcp6_scrapes_handled, 1);
drop(stats);
env.stop().await;
}
}
}
mod configured_as_whitelisted {
mod and_receiving_an_announce_request {
use std::str::FromStr;
use torrust_tracker_primitives::info_hash::InfoHash;
use torrust_tracker_test_helpers::configuration;
use tracing::level_filters::LevelFilter;
use crate::common::logging::{tracing_stderr_init, INIT};
use crate::servers::http::asserts::{assert_is_announce_response, assert_torrent_not_in_whitelist_error_response};
use crate::servers::http::client::Client;
use crate::servers::http::requests::announce::QueryBuilder;
use crate::servers::http::Started;
#[tokio::test]
async fn should_fail_if_the_torrent_is_not_in_the_whitelist() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_listed().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let response = Client::new(*env.bind_address())
.announce(&QueryBuilder::default().with_info_hash(&info_hash).query())
.await;
assert_torrent_not_in_whitelist_error_response(response).await;
env.stop().await;
}
#[tokio::test]
async fn should_allow_announcing_a_whitelisted_torrent() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_listed().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
env.tracker
.add_torrent_to_whitelist(&info_hash)
.await
.expect("should add the torrent to the whitelist");
let response = Client::new(*env.bind_address())
.announce(&QueryBuilder::default().with_info_hash(&info_hash).query())
.await;
assert_is_announce_response(response).await;
env.stop().await;
}
}
mod receiving_an_scrape_request {
use std::str::FromStr;
use aquatic_udp_protocol::PeerId;
use torrust_tracker_primitives::info_hash::InfoHash;
use torrust_tracker_primitives::peer::fixture::PeerBuilder;
use torrust_tracker_test_helpers::configuration;
use tracing::level_filters::LevelFilter;
use crate::common::logging::{tracing_stderr_init, INIT};
use crate::servers::http::asserts::assert_scrape_response;
use crate::servers::http::client::Client;
use crate::servers::http::responses::scrape::{File, ResponseBuilder};
use crate::servers::http::{requests, Started};
#[tokio::test]
async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_listed().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
env.add_torrent_peer(
&info_hash,
&PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_bytes_pending_to_download(1)
.build(),
);
let response = Client::new(*env.bind_address())
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build();
assert_scrape_response(response, &expected_scrape_response).await;
env.stop().await;
}
#[tokio::test]
async fn should_return_the_file_stats_when_the_requested_file_is_whitelisted() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_listed().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
env.add_torrent_peer(
&info_hash,
&PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_bytes_pending_to_download(1)
.build(),
);
env.tracker
.add_torrent_to_whitelist(&info_hash)
.await
.expect("should add the torrent to the whitelist");
let response = Client::new(*env.bind_address())
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
let expected_scrape_response = ResponseBuilder::default()
.add_file(
info_hash.bytes(),
File {
complete: 0,
downloaded: 0,
incomplete: 1,
},
)
.build();
assert_scrape_response(response, &expected_scrape_response).await;
env.stop().await;
}
}
}
mod configured_as_private {
mod and_receiving_an_announce_request {
use std::str::FromStr;
use std::time::Duration;
use torrust_tracker::core::auth::Key;
use torrust_tracker_primitives::info_hash::InfoHash;
use torrust_tracker_test_helpers::configuration;
use tracing::level_filters::LevelFilter;
use crate::common::logging::{tracing_stderr_init, INIT};
use crate::servers::http::asserts::{assert_authentication_error_response, assert_is_announce_response};
use crate::servers::http::client::Client;
use crate::servers::http::requests::announce::QueryBuilder;
use crate::servers::http::Started;
#[tokio::test]
async fn should_respond_to_authenticated_peers() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_private().into()).await;
let expiring_key = env.tracker.generate_auth_key(Some(Duration::from_secs(60))).await.unwrap();
let response = Client::authenticated(*env.bind_address(), expiring_key.key())
.announce(&QueryBuilder::default().query())
.await;
assert_is_announce_response(response).await;
env.stop().await;
}
#[tokio::test]
async fn should_fail_if_the_peer_has_not_provided_the_authentication_key() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_private().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
let response = Client::new(*env.bind_address())
.announce(&QueryBuilder::default().with_info_hash(&info_hash).query())
.await;
assert_authentication_error_response(response).await;
env.stop().await;
}
#[tokio::test]
async fn should_fail_if_the_key_query_param_cannot_be_parsed() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_private().into()).await;
let invalid_key = "INVALID_KEY";
let response = Client::new(*env.bind_address())
.get(&format!(
"announce/{invalid_key}?info_hash=%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port=17548&left=0&event=completed&compact=0"
))
.await;
assert_authentication_error_response(response).await;
}
#[tokio::test]
async fn should_fail_if_the_peer_cannot_be_authenticated_with_the_provided_key() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_private().into()).await;
let unregistered_key = Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap();
let response = Client::authenticated(*env.bind_address(), unregistered_key)
.announce(&QueryBuilder::default().query())
.await;
assert_authentication_error_response(response).await;
env.stop().await;
}
}
mod receiving_an_scrape_request {
use std::str::FromStr;
use std::time::Duration;
use aquatic_udp_protocol::PeerId;
use torrust_tracker::core::auth::Key;
use torrust_tracker_primitives::info_hash::InfoHash;
use torrust_tracker_primitives::peer::fixture::PeerBuilder;
use torrust_tracker_test_helpers::configuration;
use tracing::level_filters::LevelFilter;
use crate::common::logging::{tracing_stderr_init, INIT};
use crate::servers::http::asserts::{assert_authentication_error_response, assert_scrape_response};
use crate::servers::http::client::Client;
use crate::servers::http::responses::scrape::{File, ResponseBuilder};
use crate::servers::http::{requests, Started};
#[tokio::test]
async fn should_fail_if_the_key_query_param_cannot_be_parsed() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_private().into()).await;
let invalid_key = "INVALID_KEY";
let response = Client::new(*env.bind_address())
.get(&format!(
"scrape/{invalid_key}?info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"
))
.await;
assert_authentication_error_response(response).await;
}
#[tokio::test]
async fn should_return_the_zeroed_file_when_the_client_is_not_authenticated() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_private().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
env.add_torrent_peer(
&info_hash,
&PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_bytes_pending_to_download(1)
.build(),
);
let response = Client::new(*env.bind_address())
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build();
assert_scrape_response(response, &expected_scrape_response).await;
env.stop().await;
}
#[tokio::test]
async fn should_return_the_real_file_stats_when_the_client_is_authenticated() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_private().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
env.add_torrent_peer(
&info_hash,
&PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_bytes_pending_to_download(1)
.build(),
);
let expiring_key = env.tracker.generate_auth_key(Some(Duration::from_secs(60))).await.unwrap();
let response = Client::authenticated(*env.bind_address(), expiring_key.key())
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
let expected_scrape_response = ResponseBuilder::default()
.add_file(
info_hash.bytes(),
File {
complete: 0,
downloaded: 0,
incomplete: 1,
},
)
.build();
assert_scrape_response(response, &expected_scrape_response).await;
env.stop().await;
}
#[tokio::test]
async fn should_return_the_zeroed_file_when_the_authentication_key_provided_by_the_client_is_invalid() {
INIT.call_once(|| {
tracing_stderr_init(LevelFilter::ERROR);
});
let env = Started::new(&configuration::ephemeral_private().into()).await;
let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap();
env.add_torrent_peer(
&info_hash,
&PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_bytes_pending_to_download(1)
.build(),
);
let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap();
let response = Client::authenticated(*env.bind_address(), false_key)
.scrape(
&requests::scrape::QueryBuilder::default()
.with_one_info_hash(&info_hash)
.query(),
)
.await;
let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build();
assert_scrape_response(response, &expected_scrape_response).await;
env.stop().await;
}
}
}
mod configured_as_private_and_whitelisted {
mod and_receiving_an_announce_request {}
mod receiving_an_scrape_request {}
}