Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracker Checker: check UDP trackers #664

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ url = "2.5.0"
tempfile = "3.9.0"
clap = { version = "4.4.18", features = ["derive", "env"]}
anyhow = "1.0.79"
hex-literal = "0.4.1"

[dev-dependencies]
criterion = { version = "0.5.1", features = ["async_tokio"] }
Expand Down
24 changes: 24 additions & 0 deletions src/console/clients/checker/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use std::sync::Arc;

use anyhow::{Context, Result};
use clap::Parser;
use log::{debug, LevelFilter};

use super::config::Configuration;
use super::console::Console;
Expand All @@ -39,6 +40,8 @@ struct Args {
///
/// Will return an error if the configuration was not provided.
pub async fn run() -> Result<Vec<CheckResult>> {
setup_logging(LevelFilter::Info);

let args = Args::parse();

let config = setup_config(args)?;
Expand All @@ -53,6 +56,27 @@ pub async fn run() -> Result<Vec<CheckResult>> {
Ok(service.run_checks().await)
}

fn setup_logging(level: LevelFilter) {
if let Err(_err) = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{} [{}][{}] {}",
chrono::Local::now().format("%+"),
record.target(),
record.level(),
message
));
})
.level(level)
.chain(std::io::stdout())
.apply()
{
panic!("Failed to initialize logging.")
}

debug!("logging initialized.");
}

fn setup_config(args: Args) -> Result<Configuration> {
match (args.config_path, args.config_content) {
(Some(config_path), _) => load_config_from_file(&config_path),
Expand Down
51 changes: 51 additions & 0 deletions src/console/clients/checker/checks/health.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use std::time::Duration;

use colored::Colorize;
use reqwest::{Client as HttpClient, Url, Url as ServiceUrl};

use crate::console::clients::checker::console::Console;
use crate::console::clients::checker::printer::Printer;
use crate::console::clients::checker::service::{CheckError, CheckResult};

pub async fn run(health_checks: &Vec<ServiceUrl>, console: &Console, check_results: &mut Vec<CheckResult>) {
console.println("Health checks ...");

for health_check_url in health_checks {
match run_health_check(health_check_url.clone(), console).await {
Ok(()) => check_results.push(Ok(())),
Err(err) => check_results.push(Err(err)),
}
}
}

async fn run_health_check(url: Url, console: &Console) -> Result<(), CheckError> {
let client = HttpClient::builder().timeout(Duration::from_secs(5)).build().unwrap();

let colored_url = url.to_string().yellow();

match client.get(url.clone()).send().await {
Ok(response) => {
if response.status().is_success() {
console.println(&format!("{} - Health API at {} is OK", "✓".green(), colored_url));
Ok(())
} else {
console.eprintln(&format!(
"{} - Health API at {} is failing: {:?}",
"✗".red(),
colored_url,
response
));
Err(CheckError::HealthCheckError { url })
}
}
Err(err) => {
console.eprintln(&format!(
"{} - Health API at {} is failing: {:?}",
"✗".red(),
colored_url,
err
));
Err(CheckError::HealthCheckError { url })
}
}
}
95 changes: 95 additions & 0 deletions src/console/clients/checker/checks/http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use std::str::FromStr;

use colored::Colorize;
use log::debug;
use reqwest::Url as ServiceUrl;
use url::Url;

use crate::console::clients::checker::console::Console;
use crate::console::clients::checker::printer::Printer;
use crate::console::clients::checker::service::{CheckError, CheckResult};
use crate::shared::bit_torrent::info_hash::InfoHash;
use crate::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder;
use crate::shared::bit_torrent::tracker::http::client::responses::announce::Announce;
use crate::shared::bit_torrent::tracker::http::client::responses::scrape;
use crate::shared::bit_torrent::tracker::http::client::{requests, Client};

pub async fn run(http_trackers: &Vec<ServiceUrl>, console: &Console, check_results: &mut Vec<CheckResult>) {
console.println("HTTP trackers ...");

for http_tracker in http_trackers {
let colored_tracker_url = http_tracker.to_string().yellow();

match check_http_announce(http_tracker).await {
Ok(()) => {
check_results.push(Ok(()));
console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_tracker_url));
}
Err(err) => {
check_results.push(Err(err));
console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url));
}
}

match check_http_scrape(http_tracker).await {
Ok(()) => {
check_results.push(Ok(()));
console.println(&format!("{} - Scrape at {} is OK", "✓".green(), colored_tracker_url));
}
Err(err) => {
check_results.push(Err(err));
console.println(&format!("{} - Scrape at {} is failing", "✗".red(), colored_tracker_url));
}
}
}
}

async fn check_http_announce(tracker_url: &Url) -> Result<(), CheckError> {
let info_hash_str = "9c38422213e30bff212b30c360d26f9a02136422".to_string(); // # DevSkim: ignore DS173237
let info_hash = InfoHash::from_str(&info_hash_str).expect("a valid info-hash is required");

// todo: HTTP request could panic.For example, if the server is not accessible.
// We should change the client to catch that error and return a `CheckError`.
// Otherwise the checking process will stop. The idea is to process all checks
// and return a final report.
let response = Client::new(tracker_url.clone())
.announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query())
.await;

if let Ok(body) = response.bytes().await {
if let Ok(_announce_response) = serde_bencode::from_bytes::<Announce>(&body) {
Ok(())
} else {
debug!("announce body {:#?}", body);
Err(CheckError::HttpError {
url: tracker_url.clone(),
})
}
} else {
Err(CheckError::HttpError {
url: tracker_url.clone(),
})
}
}

async fn check_http_scrape(url: &Url) -> Result<(), CheckError> {
let info_hashes: Vec<String> = vec!["9c38422213e30bff212b30c360d26f9a02136422".to_string()]; // # DevSkim: ignore DS173237
let query = requests::scrape::Query::try_from(info_hashes).expect("a valid array of info-hashes is required");

// todo: HTTP request could panic.For example, if the server is not accessible.
// We should change the client to catch that error and return a `CheckError`.
// Otherwise the checking process will stop. The idea is to process all checks
// and return a final report.
let response = Client::new(url.clone()).scrape(&query).await;

if let Ok(body) = response.bytes().await {
if let Ok(_scrape_response) = scrape::Response::try_from_bencoded(&body) {
Ok(())
} else {
debug!("scrape body {:#?}", body);
Err(CheckError::HttpError { url: url.clone() })
}
} else {
Err(CheckError::HttpError { url: url.clone() })
}
}
3 changes: 3 additions & 0 deletions src/console/clients/checker/checks/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod health;
pub mod http;
pub mod udp;
87 changes: 87 additions & 0 deletions src/console/clients/checker/checks/udp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::net::SocketAddr;

use aquatic_udp_protocol::{Port, TransactionId};
use colored::Colorize;
use hex_literal::hex;
use log::debug;

use crate::console::clients::checker::console::Console;
use crate::console::clients::checker::printer::Printer;
use crate::console::clients::checker::service::{CheckError, CheckResult};
use crate::console::clients::udp::checker;
use crate::shared::bit_torrent::info_hash::InfoHash;

const ASSIGNED_BY_OS: u16 = 0;
const RANDOM_TRANSACTION_ID: i32 = -888_840_697;

pub async fn run(udp_trackers: &Vec<SocketAddr>, console: &Console, check_results: &mut Vec<CheckResult>) {
console.println("UDP trackers ...");

for udp_tracker in udp_trackers {
debug!("UDP tracker: {:?}", udp_tracker);

let colored_tracker_url = udp_tracker.to_string().yellow();

let transaction_id = TransactionId(RANDOM_TRANSACTION_ID);

let mut client = checker::Client::default();

debug!("Bind and connect");

let Ok(bound_to) = client.bind_and_connect(ASSIGNED_BY_OS, udp_tracker).await else {
check_results.push(Err(CheckError::UdpError {
socket_addr: *udp_tracker,
}));
console.println(&format!("{} - Can't connect to socket {}", "✗".red(), colored_tracker_url));
break;
};

debug!("Send connection request");

let Ok(connection_id) = client.send_connection_request(transaction_id).await else {
check_results.push(Err(CheckError::UdpError {
socket_addr: *udp_tracker,
}));
console.println(&format!(
"{} - Can't make tracker connection request to {}",
"✗".red(),
colored_tracker_url
));
break;
};

let info_hash = InfoHash(hex!("9c38422213e30bff212b30c360d26f9a02136422")); // # DevSkim: ignore DS173237

debug!("Send announce request");

if (client
.send_announce_request(connection_id, transaction_id, info_hash, Port(bound_to.port()))
.await)
.is_ok()
{
check_results.push(Ok(()));
console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_tracker_url));
} else {
let err = CheckError::UdpError {
socket_addr: *udp_tracker,
};
check_results.push(Err(err));
console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url));
}

debug!("Send scrape request");

let info_hashes = vec![InfoHash(hex!("9c38422213e30bff212b30c360d26f9a02136422"))]; // # DevSkim: ignore DS173237

if (client.send_scrape_request(connection_id, transaction_id, info_hashes).await).is_ok() {
check_results.push(Ok(()));
console.println(&format!("{} - Announce at {} is OK", "✓".green(), colored_tracker_url));
} else {
let err = CheckError::UdpError {
socket_addr: *udp_tracker,
};
check_results.push(Err(err));
console.println(&format!("{} - Announce at {} is failing", "✗".red(), colored_tracker_url));
}
}
}
1 change: 1 addition & 0 deletions src/console/clients/checker/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod app;
pub mod checks;
pub mod config;
pub mod console;
pub mod logger;
Expand Down
Loading