use chrono::Utc;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, RwLock};
use crate::modules::types::{
ConnectionQuality, ServerCapabilities, ServerProvider, SpeedTestResult, TestConfig, TestServer,
};
use crate::modules::ui::UI;
const PARALLEL_CONNECTIONS: usize = 50;
const SERVER_SELECTION_COUNT: usize = 3;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeoLocation {
pub country: String,
pub city: String,
pub latitude: f64,
pub longitude: f64,
pub isp: Option<String>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ServerPerformance {
pub server: TestServer,
pub latency_ms: f64,
pub jitter_ms: f64,
pub packet_loss: f64,
pub download_score: f64,
pub upload_score: f64,
pub overall_score: f64,
}
pub struct SpeedTest {
config: TestConfig,
client: Client,
ui: UI,
geo_location: Arc<RwLock<Option<GeoLocation>>>,
server_pool: Arc<RwLock<Vec<TestServer>>>,
}
impl SpeedTest {
pub fn new(config: TestConfig) -> Result<Self, Box<dyn std::error::Error>> {
let client = Client::builder()
.timeout(Duration::from_secs(30))
.pool_max_idle_per_host(100)
.pool_idle_timeout(Duration::from_secs(120))
.tcp_keepalive(Duration::from_secs(10))
.http2_keep_alive_interval(Duration::from_secs(10))
.http2_adaptive_window(true)
.http2_initial_stream_window_size(1024 * 1024) .http2_initial_connection_window_size(2 * 1024 * 1024) .danger_accept_invalid_certs(false)
.build()?;
let ui = UI::new(config.clone());
Ok(Self {
config,
client,
ui,
geo_location: Arc::new(RwLock::new(None)),
server_pool: Arc::new(RwLock::new(Vec::new())),
})
}
pub async fn run_full_test(&self) -> Result<SpeedTestResult, Box<dyn std::error::Error>> {
let start = Instant::now();
let geo = self.detect_location().await?;
*self.geo_location.write().await = Some(geo.clone());
self.build_server_pool(&geo).await?;
let best_servers = self.select_best_servers().await?;
if !self.config.json_output {
println!(
"{} {} ({}, {:.0} km)",
"✓ Selected:".bright_green().bold(),
best_servers[0].name,
best_servers[0].location,
best_servers[0].distance_km.unwrap_or(0.0)
);
}
let ping_ms = self.measure_latency(&best_servers[0]).await?;
let download_mbps = self.progressive_download_test(&best_servers).await?;
let upload_mbps = self.progressive_upload_test(&best_servers).await?;
let (jitter_ms, packet_loss) = self.measure_jitter_and_loss(&best_servers[0]).await?;
let quality = ConnectionQuality::from_speed_and_ping(download_mbps, upload_mbps, ping_ms);
let test_duration = start.elapsed().as_secs_f64();
let result = SpeedTestResult {
timestamp: Utc::now(),
download_mbps,
upload_mbps,
ping_ms,
jitter_ms,
packet_loss_percent: packet_loss,
server_location: best_servers[0].location.clone(),
server_ip: self.resolve_server_ip(&best_servers[0].url).await,
client_ip: self.get_client_ip().await,
quality,
test_duration_seconds: test_duration,
isp: geo.isp.clone(),
};
if !self.config.json_output {
self.display_results(&result)?;
}
Ok(result)
}
async fn detect_location(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
if !self.config.json_output {
println!("{}", "🌍 Detecting your location...".bright_cyan());
}
match self.try_ipapi_co().await {
Ok(geo) => {
if !self.config.json_output {
println!(
"{} {}, {} (via ipapi.co)",
"📍 Location:".bright_green(),
geo.city,
geo.country
);
if let Some(isp) = &geo.isp {
println!("{} {}", "🔌 ISP:".bright_blue(), isp);
}
}
return Ok(geo);
}
Err(e) => {
if std::env::var("NETRUNNER_DEBUG").is_ok() {
eprintln!("[TRACE] ipapi.co geolocation failed: {}", e);
}
}
}
match self.try_ip_api_com().await {
Ok(geo) => {
if !self.config.json_output {
println!(
"{} {}, {} (via ip-api.com)",
"📍 Location:".bright_green(),
geo.city,
geo.country
);
if let Some(isp) = &geo.isp {
println!("{} {}", "🔌 ISP:".bright_blue(), isp);
}
}
return Ok(geo);
}
Err(e) => {
if std::env::var("NETRUNNER_DEBUG").is_ok() {
eprintln!("[TRACE] ip-api.com geolocation failed: {}", e);
}
}
}
match self.try_ipinfo_io().await {
Ok(geo) => {
if !self.config.json_output {
println!(
"{} {}, {} (via ipinfo.io)",
"📍 Location:".bright_green(),
geo.city,
geo.country
);
if let Some(isp) = &geo.isp {
println!("{} {}", "🔌 ISP:".bright_blue(), isp);
}
}
return Ok(geo);
}
Err(e) => {
if std::env::var("NETRUNNER_DEBUG").is_ok() {
eprintln!("[TRACE] ipinfo.io geolocation failed: {}", e);
}
}
}
match self.try_freegeoip_app().await {
Ok(geo) => {
if !self.config.json_output {
println!(
"{} {}, {} (via freegeoip.app)",
"📍 Location:".bright_green(),
geo.city,
geo.country
);
if let Some(isp) = &geo.isp {
println!("{} {}", "🔌 ISP:".bright_blue(), isp);
}
}
return Ok(geo);
}
Err(e) => {
if std::env::var("NETRUNNER_DEBUG").is_ok() {
eprintln!("[TRACE] freegeoip.app geolocation failed: {}", e);
}
}
}
match self.try_ipwhois_app().await {
Ok(geo) => {
if !self.config.json_output {
println!(
"{} {}, {} (via ipwhois.app)",
"📍 Location:".bright_green(),
geo.city,
geo.country
);
if let Some(isp) = &geo.isp {
println!("{} {}", "🔌 ISP:".bright_blue(), isp);
}
}
return Ok(geo);
}
Err(e) => {
if std::env::var("NETRUNNER_DEBUG").is_ok() {
eprintln!("[TRACE] ipwhois.app geolocation failed: {}", e);
}
}
}
if !self.config.json_output {
println!(
"{} Using default location (USA Central) - all geolocation services failed",
"⚠".bright_yellow()
);
}
Ok(GeoLocation {
country: "United States".to_string(),
city: "Kansas City".to_string(),
latitude: 39.0997,
longitude: -94.5786,
isp: None,
})
}
async fn try_ipapi_co(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
let response = self
.client
.get("https://ipapi.co/json/")
.timeout(Duration::from_secs(5))
.send()
.await?;
if !response.status().is_success() {
return Err(format!("HTTP error: {}", response.status()).into());
}
let json: serde_json::Value = response.json().await?;
if json.get("error").is_some() {
return Err(format!(
"API error: {}",
json["reason"].as_str().unwrap_or("Unknown")
)
.into());
}
let country = json["country_name"]
.as_str()
.filter(|s| !s.is_empty() && *s != "Unknown")
.ok_or("Invalid country")?
.to_string();
let city = json["city"]
.as_str()
.filter(|s| !s.is_empty() && *s != "Unknown")
.ok_or("Invalid city")?
.to_string();
let latitude = json["latitude"].as_f64().ok_or("Invalid latitude")?;
let longitude = json["longitude"].as_f64().ok_or("Invalid longitude")?;
if latitude == 0.0 && longitude == 0.0 {
return Err("Invalid coordinates".into());
}
Ok(GeoLocation {
country,
city,
latitude,
longitude,
isp: json["org"].as_str().map(String::from),
})
}
async fn try_ip_api_com(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
let response = self
.client
.get("http://ip-api.com/json/?fields=status,message,country,city,lat,lon,isp")
.timeout(Duration::from_secs(5))
.send()
.await?;
if !response.status().is_success() {
return Err(format!("HTTP error: {}", response.status()).into());
}
let json: serde_json::Value = response.json().await?;
if json["status"].as_str() != Some("success") {
return Err(format!(
"API error: {}",
json["message"].as_str().unwrap_or("Unknown")
)
.into());
}
let country = json["country"]
.as_str()
.filter(|s| !s.is_empty() && *s != "Unknown")
.ok_or("Invalid country")?
.to_string();
let city = json["city"]
.as_str()
.filter(|s| !s.is_empty() && *s != "Unknown")
.ok_or("Invalid city")?
.to_string();
let latitude = json["lat"].as_f64().ok_or("Invalid latitude")?;
let longitude = json["lon"].as_f64().ok_or("Invalid longitude")?;
if latitude == 0.0 && longitude == 0.0 {
return Err("Invalid coordinates".into());
}
Ok(GeoLocation {
country,
city,
latitude,
longitude,
isp: json["isp"].as_str().map(String::from),
})
}
async fn try_ipinfo_io(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
let response = self
.client
.get("https://ipinfo.io/json")
.timeout(Duration::from_secs(5))
.send()
.await?;
if !response.status().is_success() {
return Err(format!("HTTP error: {}", response.status()).into());
}
let json: serde_json::Value = response.json().await?;
let country = json["country"]
.as_str()
.filter(|s| !s.is_empty())
.ok_or("Invalid country")?
.to_string();
let city = json["city"]
.as_str()
.filter(|s| !s.is_empty())
.ok_or("Invalid city")?
.to_string();
let loc = json["loc"].as_str().ok_or("Invalid location")?;
let coords: Vec<&str> = loc.split(',').collect();
if coords.len() != 2 {
return Err("Invalid coordinates format".into());
}
let latitude: f64 = coords[0].parse().map_err(|_| "Invalid latitude")?;
let longitude: f64 = coords[1].parse().map_err(|_| "Invalid longitude")?;
if latitude == 0.0 && longitude == 0.0 {
return Err("Invalid coordinates".into());
}
Ok(GeoLocation {
country,
city,
latitude,
longitude,
isp: json["org"].as_str().map(String::from),
})
}
async fn try_freegeoip_app(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
let response = self
.client
.get("https://freegeoip.app/json/")
.timeout(Duration::from_secs(5))
.send()
.await?;
if !response.status().is_success() {
return Err(format!("HTTP error: {}", response.status()).into());
}
let json: serde_json::Value = response.json().await?;
let country = json["country_name"]
.as_str()
.filter(|s| !s.is_empty())
.ok_or("Invalid country")?
.to_string();
let city = json["city"]
.as_str()
.filter(|s| !s.is_empty())
.ok_or("Invalid city")?
.to_string();
let latitude = json["latitude"].as_f64().ok_or("Invalid latitude")?;
let longitude = json["longitude"].as_f64().ok_or("Invalid longitude")?;
if latitude == 0.0 && longitude == 0.0 {
return Err("Invalid coordinates".into());
}
Ok(GeoLocation {
country,
city,
latitude,
longitude,
isp: None,
})
}
async fn try_ipwhois_app(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
let response = self
.client
.get("https://ipwho.is/")
.timeout(Duration::from_secs(5))
.send()
.await?;
if !response.status().is_success() {
return Err(format!("HTTP error: {}", response.status()).into());
}
let json: serde_json::Value = response.json().await?;
if !json["success"].as_bool().unwrap_or(false) {
return Err(format!(
"API error: {}",
json["message"].as_str().unwrap_or("Unknown")
)
.into());
}
let country = json["country"]
.as_str()
.filter(|s| !s.is_empty())
.ok_or("Invalid country")?
.to_string();
let city = json["city"]
.as_str()
.filter(|s| !s.is_empty())
.ok_or("Invalid city")?
.to_string();
let latitude = json["latitude"].as_f64().ok_or("Invalid latitude")?;
let longitude = json["longitude"].as_f64().ok_or("Invalid longitude")?;
if latitude == 0.0 && longitude == 0.0 {
return Err("Invalid coordinates".into());
}
Ok(GeoLocation {
country,
city,
latitude,
longitude,
isp: json["connection"]["isp"].as_str().map(String::from),
})
}
async fn build_server_pool(&self, geo: &GeoLocation) -> Result<(), Box<dyn std::error::Error>> {
if !self.config.json_output {
println!("{}", "🔍 Building server pool...".bright_cyan());
}
let mut servers = Vec::new();
servers.extend(self.discover_nearby_servers(geo).await);
servers.extend(self.get_global_cdn_servers());
for server in &mut servers {
if server.distance_km.is_none() {
server.distance_km = Some(self.estimate_distance(geo, server));
}
}
servers.sort_by(|a, b| {
a.distance_km
.unwrap_or(f64::MAX)
.partial_cmp(&b.distance_km.unwrap_or(f64::MAX))
.unwrap_or(std::cmp::Ordering::Equal)
});
servers.truncate(20);
let server_count = servers.len();
*self.server_pool.write().await = servers;
if !self.config.json_output {
println!("{} {} servers in pool", "✓".bright_green(), server_count);
}
Ok(())
}
fn get_global_cdn_servers(&self) -> Vec<TestServer> {
vec![
TestServer {
name: "Cloudflare Global".to_string(),
url: "https://speed.cloudflare.com".to_string(),
location: "Global CDN".to_string(),
distance_km: Some(5000.0), latency_ms: None,
provider: ServerProvider::Cloudflare,
capabilities: ServerCapabilities {
supports_download: true,
supports_upload: true,
supports_latency: true,
max_test_size_mb: 2000,
geographic_weight: 0.5, },
quality_score: None,
country_code: None,
city: None,
is_backup: true,
},
TestServer {
name: "Google Global".to_string(),
url: "https://www.google.com".to_string(),
location: "Global CDN".to_string(),
distance_km: Some(5000.0),
latency_ms: None,
provider: ServerProvider::Google,
capabilities: ServerCapabilities {
supports_download: true,
supports_upload: false,
supports_latency: true,
max_test_size_mb: 100,
geographic_weight: 0.4,
},
quality_score: None,
country_code: None,
city: None,
is_backup: true,
},
]
}
async fn discover_nearby_servers(&self, geo: &GeoLocation) -> Vec<TestServer> {
let mut servers = Vec::new();
if !self.config.json_output {
println!(
"{}",
"🔍 Discovering nearby speed test servers...".bright_cyan()
);
}
if let Ok(speedtest_servers) = self.fetch_speedtest_net_servers(geo).await {
servers.extend(speedtest_servers);
}
servers.extend(self.get_continent_servers(geo));
servers.extend(self.get_country_servers(geo));
if !self.config.json_output {
println!(
"{} {} nearby servers",
"✓ Found".bright_green(),
servers.len()
);
}
servers
}
async fn fetch_speedtest_net_servers(
&self,
geo: &GeoLocation,
) -> Result<Vec<TestServer>, Box<dyn std::error::Error>> {
let url = "https://www.speedtest.net/api/js/servers?engine=js&limit=10";
if let Ok(response) = self.client.get(url).send().await {
if let Ok(text) = response.text().await {
if let Ok(servers) = self.parse_speedtest_servers(&text, geo) {
return Ok(servers);
}
}
}
self.get_open_speedtest_servers(geo).await
}
fn parse_speedtest_servers(
&self,
json: &str,
geo: &GeoLocation,
) -> Result<Vec<TestServer>, Box<dyn std::error::Error>> {
let mut servers = Vec::new();
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json) {
if let Some(array) = parsed.as_array() {
for server in array.iter().take(10) {
if let (Some(host), Some(name), Some(country), Some(lat), Some(lon)) = (
server.get("host").and_then(|v| v.as_str()),
server.get("name").and_then(|v| v.as_str()),
server.get("country").and_then(|v| v.as_str()),
server.get("lat").and_then(|v| v.as_f64()),
server.get("lon").and_then(|v| v.as_f64()),
) {
let distance =
self.calculate_distance(geo.latitude, geo.longitude, lat, lon);
servers.push(TestServer {
name: format!("{}, {}", name, country),
url: format!("https://{}", host),
location: format!("{}, {}", name, country),
distance_km: Some(distance),
latency_ms: None,
provider: ServerProvider::Custom(
host.split('.').next().unwrap_or("speedtest").to_string(),
),
capabilities: ServerCapabilities {
supports_download: true,
supports_upload: true,
supports_latency: true,
max_test_size_mb: 1000,
geographic_weight: 1.0,
},
quality_score: None,
country_code: Some(country.to_string()),
city: Some(name.to_string()),
is_backup: false,
});
}
}
}
}
if servers.is_empty() {
Err("No servers parsed".into())
} else {
Ok(servers)
}
}
async fn get_open_speedtest_servers(
&self,
geo: &GeoLocation,
) -> Result<Vec<TestServer>, Box<dyn std::error::Error>> {
let mut servers = Vec::new();
let endpoints = vec![
(
"Cloudflare (Anycast)",
"https://speed.cloudflare.com",
0.0,
0.0,
"Global",
),
(
"LibreSpeed DE-IX",
"https://frankfurt.speedtest.wtnet.de",
50.1109,
8.6821,
"Frankfurt, Germany",
),
(
"LibreSpeed AMS-IX",
"https://ams.speedtest.wtnet.de",
52.3676,
4.9041,
"Amsterdam, Netherlands",
),
(
"LibreSpeed Singapore",
"https://sg.speedtest.wtnet.de",
1.3521,
103.8198,
"Singapore",
),
(
"LibreSpeed New York",
"https://nyc.speedtest.wtnet.de",
40.7128,
-74.0060,
"New York, USA",
),
(
"LibreSpeed Los Angeles",
"https://la.speedtest.wtnet.de",
34.0522,
-118.2437,
"Los Angeles, USA",
),
(
"LibreSpeed Tokyo",
"https://tyo.speedtest.wtnet.de",
35.6762,
139.6503,
"Tokyo, Japan",
),
(
"LibreSpeed London",
"https://lon.speedtest.wtnet.de",
51.5074,
-0.1278,
"London, UK",
),
(
"LibreSpeed Sydney",
"https://syd.speedtest.wtnet.de",
-33.8688,
151.2093,
"Sydney, Australia",
),
];
for (name, url, lat, lon, location) in endpoints {
let distance = if lat == 0.0 && lon == 0.0 {
999999.0 } else {
self.calculate_distance(geo.latitude, geo.longitude, lat, lon)
};
servers.push(TestServer {
name: name.to_string(),
url: url.to_string(),
location: location.to_string(),
distance_km: Some(distance),
latency_ms: None,
provider: ServerProvider::Custom("LibreSpeed".to_string()),
capabilities: ServerCapabilities {
supports_download: true,
supports_upload: true,
supports_latency: true,
max_test_size_mb: 2000,
geographic_weight: 0.9,
},
quality_score: None,
country_code: Some(location.split(", ").last().unwrap_or("").to_string()),
city: Some(location.split(", ").next().unwrap_or(location).to_string()),
is_backup: false,
});
}
Ok(servers)
}
fn get_continent_servers(&self, geo: &GeoLocation) -> Vec<TestServer> {
let mut servers = Vec::new();
let continent = self.determine_continent(geo.latitude, geo.longitude);
match continent.as_str() {
"North America" => {
servers.push(self.create_server_with_coords(
geo,
"US East Coast Hub",
"https://ash.speedtest.wtnet.de",
"Ashburn, USA",
Some("US".to_string()),
39.0438,
-77.4874,
));
servers.push(self.create_server_with_coords(
geo,
"US West Coast Hub",
"https://lax.speedtest.wtnet.de",
"Los Angeles, USA",
Some("US".to_string()),
34.0522,
-118.2437,
));
}
"Europe" => {
servers.push(self.create_server_with_coords(
geo,
"Europe Central Hub",
"https://frankfurt.speedtest.wtnet.de",
"Frankfurt, Germany",
Some("DE".to_string()),
50.1109,
8.6821,
));
servers.push(self.create_server_with_coords(
geo,
"Europe West Hub",
"https://lon.speedtest.wtnet.de",
"London, UK",
Some("GB".to_string()),
51.5074,
-0.1278,
));
}
"Asia" => {
servers.push(self.create_server_with_coords(
geo,
"Asia Pacific Hub",
"https://sg.speedtest.wtnet.de",
"Singapore",
Some("SG".to_string()),
1.3521,
103.8198,
));
servers.push(self.create_server_with_coords(
geo,
"Asia East Hub",
"https://tokyo.speedtest.wtnet.de",
"Tokyo, Japan",
Some("JP".to_string()),
35.6762,
139.6503,
));
}
"South America" => {
servers.push(self.create_server_with_coords(
geo,
"South America Hub",
"https://saopaulo.speedtest.wtnet.de",
"São Paulo, Brazil",
Some("BR".to_string()),
-23.5505,
-46.6333,
));
}
"Africa" => {
servers.push(self.create_server_with_coords(
geo,
"Africa Hub",
"https://capetown.speedtest.wtnet.de",
"Cape Town, South Africa",
Some("ZA".to_string()),
-33.9249,
18.4241,
));
}
"Oceania" => {
servers.push(self.create_server_with_coords(
geo,
"Oceania Hub",
"https://syd.speedtest.wtnet.de",
"Sydney, Australia",
Some("AU".to_string()),
-33.8688,
151.2093,
));
}
_ => {}
}
servers
}
fn determine_continent(&self, lat: f64, lon: f64) -> String {
if lat > 15.0 && lon > -130.0 && lon < -50.0 {
"North America".to_string()
} else if lat < 15.0 && lat > -60.0 && lon > -85.0 && lon < -30.0 {
"South America".to_string()
} else if lat > 35.0 && lon > -15.0 && lon < 60.0 {
"Europe".to_string()
} else if lat > -40.0 && lat < 40.0 && lon > -20.0 && lon < 55.0 {
"Africa".to_string()
} else if lat > -15.0 && lon > 60.0 && lon < 180.0 {
"Asia".to_string()
} else if lat < -10.0 && lon > 110.0 && lon < 180.0 {
"Oceania".to_string()
} else {
"Unknown".to_string()
}
}
fn get_country_servers(&self, geo: &GeoLocation) -> Vec<TestServer> {
let mut servers = Vec::new();
match geo.country.as_str() {
"United States" | "US" => {
servers.push(self.create_server(
"US Central",
"https://dal.speedtest.wtnet.de",
"Dallas, USA",
Some("US".to_string()),
));
}
"United Kingdom" | "GB" | "UK" => {
servers.push(self.create_server(
"UK Primary",
"https://lon.speedtest.wtnet.de",
"London, UK",
Some("GB".to_string()),
));
}
"Germany" | "DE" => {
servers.push(self.create_server(
"DE Primary",
"https://frankfurt.speedtest.wtnet.de",
"Frankfurt, Germany",
Some("DE".to_string()),
));
}
"France" | "FR" => {
servers.push(self.create_server(
"FR Primary",
"https://paris.speedtest.wtnet.de",
"Paris, France",
Some("FR".to_string()),
));
}
"Japan" | "JP" => {
servers.push(self.create_server(
"JP Primary",
"https://tyo.speedtest.wtnet.de",
"Tokyo, Japan",
Some("JP".to_string()),
));
}
"Australia" | "AU" => {
servers.push(self.create_server(
"AU Primary",
"https://syd.speedtest.wtnet.de",
"Sydney, Australia",
Some("AU".to_string()),
));
}
"Canada" | "CA" => {
servers.push(self.create_server(
"CA Primary",
"https://tor.speedtest.wtnet.de",
"Toronto, Canada",
Some("CA".to_string()),
));
}
_ => {}
}
servers
}
fn calculate_distance(&self, lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let r = 6371.0; let d_lat = (lat2 - lat1).to_radians();
let d_lon = (lon2 - lon1).to_radians();
let lat1 = lat1.to_radians();
let lat2 = lat2.to_radians();
let a = (d_lat / 2.0).sin() * (d_lat / 2.0).sin()
+ lat1.cos() * lat2.cos() * (d_lon / 2.0).sin() * (d_lon / 2.0).sin();
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
r * c
}
#[allow(clippy::too_many_arguments)]
fn create_server_with_coords(
&self,
geo: &GeoLocation,
name: &str,
url: &str,
location: &str,
country_code: Option<String>,
lat: f64,
lon: f64,
) -> TestServer {
let distance = self.calculate_distance(geo.latitude, geo.longitude, lat, lon);
TestServer {
name: name.to_string(),
url: url.to_string(),
location: location.to_string(),
distance_km: Some(distance),
latency_ms: None,
provider: ServerProvider::Custom("LibreSpeed".to_string()),
capabilities: ServerCapabilities {
supports_download: true,
supports_upload: true,
supports_latency: true,
max_test_size_mb: 2000,
geographic_weight: 1.0,
},
quality_score: None,
country_code,
city: Some(location.split(", ").next().unwrap_or(location).to_string()),
is_backup: false,
}
}
fn create_server(
&self,
name: &str,
url: &str,
location: &str,
country_code: Option<String>,
) -> TestServer {
TestServer {
name: name.to_string(),
url: url.to_string(),
location: location.to_string(),
distance_km: None,
latency_ms: None,
provider: ServerProvider::Cloudflare,
capabilities: ServerCapabilities {
supports_download: true,
supports_upload: true,
supports_latency: true,
max_test_size_mb: 1000,
geographic_weight: 1.2,
},
quality_score: None,
country_code,
city: Some(location.split(',').next().unwrap_or("").trim().to_string()),
is_backup: false,
}
}
fn determine_region(&self, country: &str) -> String {
match country {
"United States" | "Canada" | "Mexico" => "North America".to_string(),
"United Kingdom" | "Germany" | "France" | "Spain" | "Italy" | "Netherlands"
| "Belgium" | "Switzerland" | "Austria" | "Poland" => "Europe".to_string(),
"Japan" | "China" | "South Korea" | "Singapore" | "Australia" | "New Zealand"
| "India" => "Asia Pacific".to_string(),
"Brazil" | "Argentina" | "Chile" => "South America".to_string(),
_ => "Other".to_string(),
}
}
fn estimate_distance(&self, geo: &GeoLocation, server: &TestServer) -> f64 {
let region = self.determine_region(&geo.country);
if let Some(city) = &server.city {
if city.contains(&geo.city) {
return 10.0; }
}
match (region.as_str(), server.location.as_str()) {
("North America", loc) if loc.contains("USA") || loc.contains("Canada") => 500.0,
("Europe", loc) if loc.contains("Europe") || loc.contains("UK") => 300.0,
("Asia Pacific", loc) if loc.contains("Asia") || loc.contains("Japan") => 400.0,
_ => 5000.0, }
}
async fn select_best_servers(&self) -> Result<Vec<TestServer>, Box<dyn std::error::Error>> {
if !self.config.json_output {
println!("{}", "⚡ Testing server performance...".bright_cyan());
}
let servers = self.server_pool.read().await.clone();
if servers.is_empty() {
return Err("No servers in pool".into());
}
let mut test_results = Vec::new();
let mut futures = FuturesUnordered::new();
for server in servers.into_iter().take(15) {
let client = self.client.clone();
futures.push(async move { Self::quick_latency_test(&client, &server).await });
}
while let Some(result) = futures.next().await {
if let Ok(mut server) = result {
if let Some(latency) = server.latency_ms {
let distance = server.distance_km.unwrap_or(1000.0);
let geographic_weight = server.capabilities.geographic_weight;
let latency_penalty = latency.max(1.0); let distance_penalty = (distance / 100.0).max(1.0);
server.quality_score =
Some((10000.0 * geographic_weight) / (latency_penalty + distance_penalty));
test_results.push(server);
}
}
}
if test_results.is_empty() {
return Err("No servers responded to latency tests".into());
}
test_results.sort_by(|a, b| {
b.quality_score
.unwrap_or(0.0)
.partial_cmp(&a.quality_score.unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
});
let selected = test_results
.into_iter()
.take(SERVER_SELECTION_COUNT)
.collect::<Vec<_>>();
if !self.config.json_output {
println!(
"{} {} servers selected for testing",
"✓".bright_green(),
selected.len()
);
for (i, server) in selected.iter().enumerate() {
println!(
" {}. {} - {:.1} ms ({:.0} km)",
i + 1,
server.name,
server.latency_ms.unwrap_or(0.0),
server.distance_km.unwrap_or(0.0)
);
}
}
Ok(selected)
}
async fn quick_latency_test(
client: &Client,
server: &TestServer,
) -> Result<TestServer, Box<dyn std::error::Error>> {
let mut latencies = Vec::new();
let mut server = server.clone();
for _ in 0..3 {
let start = Instant::now();
match client
.head(&server.url)
.timeout(Duration::from_secs(2))
.send()
.await
{
Ok(resp) if resp.status().is_success() || resp.status().is_redirection() => {
latencies.push(start.elapsed().as_millis() as f64);
}
_ => {}
}
}
if !latencies.is_empty() {
server.latency_ms = Some(latencies.iter().sum::<f64>() / latencies.len() as f64);
}
Ok(server)
}
async fn progressive_download_test(
&self,
servers: &[TestServer],
) -> Result<f64, Box<dyn std::error::Error>> {
if !self.config.json_output {
self.ui.show_section_header("Testing Download Speed")?;
}
let bw_monitor = if !self.config.json_output && self.config.animation_enabled {
let monitor = self
.ui
.create_bandwidth_monitor("DOWNLOAD SPEED BANDWIDTH MONITOR", "Download");
Some(monitor)
} else {
None
};
let total_bytes = Arc::new(Mutex::new(0usize));
let start = Instant::now();
let test_duration = Duration::from_secs(15);
let mut handles = Vec::new();
for i in 0..PARALLEL_CONNECTIONS {
let server = &servers[i % servers.len()];
let url = format!("{}/__down?bytes=100000000", server.url); let client = self.client.clone();
let total_bytes = Arc::clone(&total_bytes);
let test_start = start;
let handle = tokio::spawn(async move {
let end_time = test_start + test_duration;
while Instant::now() < end_time {
match client.get(&url).send().await {
Ok(response) => {
let mut stream = response.bytes_stream();
while let Some(chunk_result) = stream.next().await {
if Instant::now() >= end_time {
break;
}
if let Ok(chunk) = chunk_result {
let mut total = total_bytes.lock().await;
*total += chunk.len();
}
}
}
Err(_) => {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
if Instant::now() >= end_time {
break;
}
}
});
handles.push(handle);
}
let total_bytes_monitor = Arc::clone(&total_bytes);
let monitor_clone = bw_monitor.clone();
let monitor_handle = tokio::spawn(async move {
let mut last_bytes = 0;
let mut last_time = Instant::now();
let end_time = start + test_duration;
let mut first_render = true;
while Instant::now() < end_time {
tokio::time::sleep(Duration::from_millis(200)).await;
let bytes = *total_bytes_monitor.lock().await;
let time_diff = last_time.elapsed().as_secs_f64();
if time_diff >= 0.2 {
let bytes_diff = bytes.saturating_sub(last_bytes);
let speed = (bytes_diff as f64 * 8.0) / (time_diff * 1_000_000.0);
if let Some(ref monitor) = monitor_clone {
monitor.update(speed).await;
if first_render {
let _ = monitor.render_live().await;
first_render = false;
} else {
let _ = monitor.render_live_update().await;
}
}
last_bytes = bytes;
last_time = Instant::now();
}
}
});
for handle in handles {
let _ = handle.await;
}
let _ = monitor_handle.await;
let elapsed = start.elapsed().as_secs_f64();
let total = *total_bytes.lock().await;
let mbps = if total > 1_000_000 && elapsed > 1.0 {
let bits = total as f64 * 8.0;
bits / (elapsed * 1_000_000.0)
} else {
1.0 };
if let Some(ref monitor) = bw_monitor {
monitor.update(mbps).await;
monitor.mark_final().await;
let _ = monitor.render_live_update().await;
}
Ok(mbps.clamp(1.0, 10_000.0))
}
async fn progressive_upload_test(
&self,
servers: &[TestServer],
) -> Result<f64, Box<dyn std::error::Error>> {
if !self.config.json_output {
self.ui.show_section_header("Testing Upload Speed")?;
}
let bw_monitor = if !self.config.json_output && self.config.animation_enabled {
let monitor = self
.ui
.create_bandwidth_monitor("UPLOAD SPEED BANDWIDTH MONITOR", "Upload");
Some(monitor)
} else {
None
};
let total_bytes = Arc::new(Mutex::new(0usize));
let start = Instant::now();
let test_duration = Duration::from_secs(15);
let chunk_size = 5 * 1024 * 1024;
let test_data = vec![0u8; chunk_size];
let mut handles = Vec::new();
for i in 0..10 {
let server = &servers[i % servers.len()];
let url = format!("{}/__up", server.url);
let client = self.client.clone();
let total_bytes = Arc::clone(&total_bytes);
let data = test_data.clone();
let test_start = start;
let handle = tokio::spawn(async move {
let end_time = test_start + test_duration;
while Instant::now() < end_time {
match client
.post(&url)
.body(data.clone())
.timeout(Duration::from_secs(10))
.send()
.await
{
Ok(_) => {
let mut total = total_bytes.lock().await;
*total += data.len();
}
Err(_) => {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
});
handles.push(handle);
}
let total_bytes_monitor = Arc::clone(&total_bytes);
let monitor_clone = bw_monitor.clone();
let monitor_handle = tokio::spawn(async move {
let mut last_bytes = 0;
let mut last_time = Instant::now();
let end_time = start + test_duration;
let mut first_render = true;
while Instant::now() < end_time {
tokio::time::sleep(Duration::from_millis(200)).await;
let bytes = *total_bytes_monitor.lock().await;
let time_diff = last_time.elapsed().as_secs_f64();
if time_diff >= 0.2 {
let bytes_diff = bytes.saturating_sub(last_bytes);
let speed = (bytes_diff as f64 * 8.0) / (time_diff * 1_000_000.0);
if let Some(ref monitor) = monitor_clone {
monitor.update(speed).await;
if first_render {
let _ = monitor.render_live().await;
first_render = false;
} else {
let _ = monitor.render_live_update().await;
}
}
last_bytes = bytes;
last_time = Instant::now();
}
}
});
for handle in handles {
let _ = handle.await;
}
let _ = monitor_handle.await;
let elapsed = start.elapsed().as_secs_f64();
let total = *total_bytes.lock().await;
let mbps = if total > 1_000_000 && elapsed > 1.0 {
let bits = total as f64 * 8.0;
bits / (elapsed * 1_000_000.0)
} else {
1.0 };
if let Some(ref monitor) = bw_monitor {
monitor.update(mbps).await;
monitor.mark_final().await;
let _ = monitor.render_live_update().await;
}
Ok(mbps.clamp(1.0, 10_000.0))
}
async fn measure_latency(
&self,
server: &TestServer,
) -> Result<f64, Box<dyn std::error::Error>> {
if !self.config.json_output {
self.ui.show_section_header("Testing Latency")?;
}
let pb = if !self.config.json_output && self.config.animation_enabled {
Some(self.ui.create_ping_spinner("Latency: -- ms"))
} else {
None
};
let mut latencies = Vec::new();
for _i in 0..10 {
let start = Instant::now();
match self
.client
.head(&server.url)
.timeout(Duration::from_secs(2))
.send()
.await
{
Ok(resp) if resp.status().is_success() || resp.status().is_redirection() => {
let latency = start.elapsed().as_millis() as f64;
latencies.push(latency);
if let Some(pb) = &pb {
let current_avg = latencies.iter().sum::<f64>() / latencies.len() as f64;
pb.set_message(format!("Latency: {:.1} ms", current_avg));
}
}
_ => {}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let avg_latency = if !latencies.is_empty() {
latencies.iter().sum::<f64>() / latencies.len() as f64
} else {
50.0
};
if let Some(pb) = pb {
pb.finish_and_clear();
let (latency_colored, explanation) = if avg_latency <= 20.0 {
(
format!("{:.1} ms", avg_latency).bright_green(),
"(Excellent - ideal for gaming)".bright_green().dimmed(),
)
} else if avg_latency <= 50.0 {
(
format!("{:.1} ms", avg_latency).bright_cyan(),
"(Good - suitable for most activities)"
.bright_cyan()
.dimmed(),
)
} else if avg_latency <= 100.0 {
(
format!("{:.1} ms", avg_latency).bright_yellow(),
"(Fair - noticeable lag)".bright_yellow().dimmed(),
)
} else {
(
format!("{:.1} ms", avg_latency).bright_red(),
"(Poor - significant lag)".bright_red().dimmed(),
)
};
println!("✓ Latency: {} {}", latency_colored, explanation);
}
Ok(avg_latency)
}
async fn measure_jitter_and_loss(
&self,
server: &TestServer,
) -> Result<(f64, f64), Box<dyn std::error::Error>> {
let mut latencies = Vec::new();
let mut lost = 0;
let total = 20;
for _ in 0..total {
let start = Instant::now();
match self
.client
.head(&server.url)
.timeout(Duration::from_secs(1))
.send()
.await
{
Ok(resp) if resp.status().is_success() || resp.status().is_redirection() => {
latencies.push(start.elapsed().as_millis() as f64);
}
_ => {
lost += 1;
}
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
let jitter = if latencies.len() > 1 {
let mean = latencies.iter().sum::<f64>() / latencies.len() as f64;
let variance =
latencies.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / latencies.len() as f64;
variance.sqrt()
} else {
0.0
};
let packet_loss = (lost as f64 / total as f64) * 100.0;
Ok((jitter, packet_loss))
}
async fn get_client_ip(&self) -> Option<IpAddr> {
if let Ok(response) = self
.client
.get("https://api.ipify.org?format=json")
.timeout(Duration::from_secs(3))
.send()
.await
{
if let Ok(json) = response.json::<serde_json::Value>().await {
return json["ip"].as_str().and_then(|s| s.parse::<IpAddr>().ok());
}
}
None
}
async fn resolve_server_ip(&self, url: &str) -> Option<IpAddr> {
if let Ok(parsed) = url.parse::<reqwest::Url>() {
if let Some(host) = parsed.host_str() {
if let Ok(addrs) = tokio::net::lookup_host(format!("{}:443", host)).await {
return addrs.into_iter().next().map(|addr| addr.ip());
}
}
}
None
}
fn display_results(&self, result: &SpeedTestResult) -> std::io::Result<()> {
println!();
println!("{}", "═".repeat(60).bright_blue());
println!(
"{}",
" SPEED TEST RESULTS "
.bright_yellow()
.bold()
);
println!("{}", "═".repeat(60).bright_blue());
println!();
println!(
"{:20} {}",
"Download:".bright_blue().bold(),
format!("{:.1} Mbps", result.download_mbps)
.bright_green()
.bold()
);
println!(
"{:20} {}",
"Upload:".bright_blue().bold(),
format!("{:.1} Mbps", result.upload_mbps)
.bright_green()
.bold()
);
println!(
"{:20} {}",
"Ping:".bright_blue().bold(),
format!("{:.1} ms", result.ping_ms).bright_cyan().bold()
);
println!(
"{:20} {}",
"Jitter:".bright_blue().bold(),
format!("{:.1} ms", result.jitter_ms).bright_cyan()
);
if result.packet_loss_percent > 0.0 {
println!(
"{:20} {}",
"Packet Loss:".bright_blue().bold(),
format!("{:.1}%", result.packet_loss_percent).bright_red()
);
}
println!(
"{:20} {}",
"Server:".bright_blue().bold(),
result.server_location.bright_cyan()
);
if let Some(isp) = &result.isp {
println!("{:20} {}", "ISP:".bright_blue().bold(), isp.bright_cyan());
}
println!(
"{:20} {}",
"Quality:".bright_blue().bold(),
format!("{}", result.quality).bright_yellow().bold()
);
println!();
println!("{}", "═".repeat(60).bright_blue());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_region_determination() {
let _ = rustls::crypto::ring::default_provider().install_default();
let config = TestConfig::default();
let speed_test = SpeedTest::new(config).unwrap();
assert_eq!(
speed_test.determine_region("United States"),
"North America"
);
assert_eq!(speed_test.determine_region("Germany"), "Europe");
assert_eq!(speed_test.determine_region("Japan"), "Asia Pacific");
}
}