1use chrono::Utc;
19use futures::stream::{FuturesUnordered, StreamExt};
20use reqwest::Client;
21use serde::{Deserialize, Serialize};
22use std::net::IpAddr;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::sync::Arc;
25use std::time::{Duration, Instant};
26use tokio::sync::RwLock;
27
28use crate::events::{emit, EventSender, Phase, SelectedServer, TestEvent};
29use crate::types::{
30 ConnectionQuality, ServerCapabilities, ServerProvider, SpeedTestResult, TestConfig, TestServer,
31};
32
33const PARALLEL_CONNECTIONS: usize = 50;
34const SERVER_SELECTION_COUNT: usize = 3;
35const TEST_SECS: u64 = 15;
37const WARMUP_SECS: f64 = 3.0;
39const DOWNLOAD_ENDPOINT: &str = "https://speed.cloudflare.com/__down?bytes=100000000";
47const UPLOAD_ENDPOINT: &str = "https://speed.cloudflare.com/__up";
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct GeoLocation {
51 pub country: String,
52 pub city: String,
53 pub latitude: f64,
54 pub longitude: f64,
55 pub isp: Option<String>,
56}
57
58#[derive(Debug, Clone)]
59#[allow(dead_code)]
60pub struct ServerPerformance {
61 pub server: TestServer,
62 pub latency_ms: f64,
63 pub jitter_ms: f64,
64 pub packet_loss: f64,
65 pub download_score: f64,
66 pub upload_score: f64,
67 pub overall_score: f64,
68}
69
70pub struct SpeedTest {
71 config: TestConfig,
72 client: Client,
73 events: Option<EventSender>,
74 geo_location: Arc<RwLock<Option<GeoLocation>>>,
75 server_pool: Arc<RwLock<Vec<TestServer>>>,
76}
77
78impl SpeedTest {
79 pub fn new(config: TestConfig) -> Result<Self, Box<dyn std::error::Error>> {
80 Self::with_events(config, None)
81 }
82
83 pub fn with_events(
88 config: TestConfig,
89 events: Option<EventSender>,
90 ) -> Result<Self, Box<dyn std::error::Error>> {
91 let client = Client::builder()
92 .timeout(Duration::from_secs(30))
93 .pool_max_idle_per_host(100)
94 .pool_idle_timeout(Duration::from_secs(120))
95 .tcp_keepalive(Duration::from_secs(10))
96 .http2_keep_alive_interval(Duration::from_secs(10))
97 .http2_adaptive_window(true)
98 .http2_initial_stream_window_size(1024 * 1024) .http2_initial_connection_window_size(2 * 1024 * 1024) .danger_accept_invalid_certs(false)
101 .build()?;
102
103 Ok(Self {
104 config,
105 client,
106 events,
107 geo_location: Arc::new(RwLock::new(None)),
108 server_pool: Arc::new(RwLock::new(Vec::new())),
109 })
110 }
111
112 pub fn set_events(&mut self, events: Option<EventSender>) {
114 self.events = events;
115 }
116
117 pub async fn run_full_test(&self) -> Result<SpeedTestResult, Box<dyn std::error::Error>> {
119 let start = Instant::now();
120
121 emit(&self.events, TestEvent::PhaseStarted(Phase::Locating));
123 let geo = self.detect_location().await?;
124 *self.geo_location.write().await = Some(geo.clone());
125
126 emit(
128 &self.events,
129 TestEvent::PhaseStarted(Phase::BuildingServers),
130 );
131 self.build_server_pool(&geo).await?;
132
133 emit(
135 &self.events,
136 TestEvent::PhaseStarted(Phase::SelectingServers),
137 );
138 let best_servers = self.select_best_servers().await?;
139
140 emit(
141 &self.events,
142 TestEvent::PrimarySelected {
143 name: best_servers[0].name.clone(),
144 location: best_servers[0].location.clone(),
145 distance_km: best_servers[0].distance_km.unwrap_or(0.0),
146 },
147 );
148
149 let ping_ms = self.measure_latency(&best_servers[0]).await?;
151
152 let download_mbps = self.progressive_download_test().await?;
154
155 let upload_mbps = self.progressive_upload_test().await?;
157
158 emit(&self.events, TestEvent::PhaseStarted(Phase::Jitter));
160 let (jitter_ms, packet_loss) = self.measure_jitter_and_loss(&best_servers[0]).await?;
161 emit(
162 &self.events,
163 TestEvent::JitterComplete {
164 jitter_ms,
165 packet_loss_percent: packet_loss,
166 },
167 );
168
169 let quality = ConnectionQuality::from_speed_and_ping(download_mbps, upload_mbps, ping_ms);
170 let test_duration = start.elapsed().as_secs_f64();
171
172 let result = SpeedTestResult {
173 timestamp: Utc::now(),
174 download_mbps,
175 upload_mbps,
176 ping_ms,
177 jitter_ms,
178 packet_loss_percent: packet_loss,
179 server_location: best_servers[0].location.clone(),
180 server_ip: self.resolve_server_ip(&best_servers[0].url).await,
181 client_ip: self.get_client_ip().await,
182 quality,
183 test_duration_seconds: test_duration,
184 isp: geo.isp.clone(),
185 };
186
187 emit(&self.events, TestEvent::Completed(Box::new(result.clone())));
188
189 Ok(result)
190 }
191
192 async fn detect_location(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
194 emit(
195 &self.events,
196 TestEvent::Status("Detecting your location...".to_string()),
197 );
198
199 match self.try_ipapi_co().await {
202 Ok(geo) => {
203 self.emit_location(&geo, "ipapi.co");
204 return Ok(geo);
205 }
206 Err(e) => {
207 if std::env::var("NETRUNNER_DEBUG").is_ok() {
209 eprintln!("[TRACE] ipapi.co geolocation failed: {}", e);
210 }
211 }
212 }
213
214 match self.try_ip_api_com().await {
216 Ok(geo) => {
217 self.emit_location(&geo, "ip-api.com");
218 return Ok(geo);
219 }
220 Err(e) => {
221 if std::env::var("NETRUNNER_DEBUG").is_ok() {
223 eprintln!("[TRACE] ip-api.com geolocation failed: {}", e);
224 }
225 }
226 }
227
228 match self.try_ipinfo_io().await {
230 Ok(geo) => {
231 self.emit_location(&geo, "ipinfo.io");
232 return Ok(geo);
233 }
234 Err(e) => {
235 if std::env::var("NETRUNNER_DEBUG").is_ok() {
237 eprintln!("[TRACE] ipinfo.io geolocation failed: {}", e);
238 }
239 }
240 }
241
242 match self.try_freegeoip_app().await {
244 Ok(geo) => {
245 self.emit_location(&geo, "freegeoip.app");
246 return Ok(geo);
247 }
248 Err(e) => {
249 if std::env::var("NETRUNNER_DEBUG").is_ok() {
251 eprintln!("[TRACE] freegeoip.app geolocation failed: {}", e);
252 }
253 }
254 }
255
256 match self.try_ipwhois_app().await {
258 Ok(geo) => {
259 self.emit_location(&geo, "ipwhois.app");
260 return Ok(geo);
261 }
262 Err(e) => {
263 if std::env::var("NETRUNNER_DEBUG").is_ok() {
265 eprintln!("[TRACE] ipwhois.app geolocation failed: {}", e);
266 }
267 }
268 }
269
270 emit(
272 &self.events,
273 TestEvent::Status(
274 "Using default location (USA Central) - all geolocation services failed"
275 .to_string(),
276 ),
277 );
278
279 Ok(GeoLocation {
280 country: "United States".to_string(),
281 city: "Kansas City".to_string(),
282 latitude: 39.0997,
283 longitude: -94.5786,
284 isp: None,
285 })
286 }
287
288 fn emit_location(&self, geo: &GeoLocation, source: &str) {
290 emit(
291 &self.events,
292 TestEvent::LocationDetected {
293 city: geo.city.clone(),
294 country: geo.country.clone(),
295 isp: geo.isp.clone(),
296 source: source.to_string(),
297 },
298 );
299 }
300
301 async fn fetch_geo_json(
303 &self,
304 url: &str,
305 ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
306 let response = self
307 .client
308 .get(url)
309 .timeout(Duration::from_secs(5))
310 .send()
311 .await?;
312 if !response.status().is_success() {
313 return Err(format!("HTTP error: {}", response.status()).into());
314 }
315 Ok(response.json().await?)
316 }
317
318 async fn try_ipapi_co(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
319 let json = self.fetch_geo_json("https://ipapi.co/json/").await?;
320 if json.get("error").is_some() {
321 return Err(format!(
322 "API error: {}",
323 json["reason"].as_str().unwrap_or("Unknown")
324 )
325 .into());
326 }
327 build_geo(
328 geo_str_field(&json, "country_name")?,
329 geo_str_field(&json, "city")?,
330 json["latitude"].as_f64().ok_or("Invalid latitude")?,
331 json["longitude"].as_f64().ok_or("Invalid longitude")?,
332 json["org"].as_str().map(String::from),
333 )
334 }
335
336 async fn try_ip_api_com(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
337 let json = self
338 .fetch_geo_json(
339 "http://ip-api.com/json/?fields=status,message,country,city,lat,lon,isp",
340 )
341 .await?;
342 if json["status"].as_str() != Some("success") {
343 return Err(format!(
344 "API error: {}",
345 json["message"].as_str().unwrap_or("Unknown")
346 )
347 .into());
348 }
349 build_geo(
350 geo_str_field(&json, "country")?,
351 geo_str_field(&json, "city")?,
352 json["lat"].as_f64().ok_or("Invalid latitude")?,
353 json["lon"].as_f64().ok_or("Invalid longitude")?,
354 json["isp"].as_str().map(String::from),
355 )
356 }
357
358 async fn try_ipinfo_io(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
359 let json = self.fetch_geo_json("https://ipinfo.io/json").await?;
360 let (latitude, longitude) =
362 parse_latlon_pair(json["loc"].as_str().ok_or("Invalid location")?)?;
363 build_geo(
364 geo_str_field(&json, "country")?,
365 geo_str_field(&json, "city")?,
366 latitude,
367 longitude,
368 json["org"].as_str().map(String::from),
369 )
370 }
371
372 async fn try_freegeoip_app(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
373 let json = self.fetch_geo_json("https://freegeoip.app/json/").await?;
374 build_geo(
375 geo_str_field(&json, "country_name")?,
376 geo_str_field(&json, "city")?,
377 json["latitude"].as_f64().ok_or("Invalid latitude")?,
378 json["longitude"].as_f64().ok_or("Invalid longitude")?,
379 None,
380 )
381 }
382
383 async fn try_ipwhois_app(&self) -> Result<GeoLocation, Box<dyn std::error::Error>> {
384 let json = self.fetch_geo_json("https://ipwho.is/").await?;
385 if !json["success"].as_bool().unwrap_or(false) {
386 return Err(format!(
387 "API error: {}",
388 json["message"].as_str().unwrap_or("Unknown")
389 )
390 .into());
391 }
392 build_geo(
393 geo_str_field(&json, "country")?,
394 geo_str_field(&json, "city")?,
395 json["latitude"].as_f64().ok_or("Invalid latitude")?,
396 json["longitude"].as_f64().ok_or("Invalid longitude")?,
397 json["connection"]["isp"].as_str().map(String::from),
398 )
399 }
400
401 async fn build_server_pool(&self, geo: &GeoLocation) -> Result<(), Box<dyn std::error::Error>> {
403 emit(
404 &self.events,
405 TestEvent::Status("Building server pool...".to_string()),
406 );
407
408 let mut servers = Vec::new();
409
410 servers.extend(self.discover_nearby_servers(geo).await);
412
413 servers.extend(self.get_global_cdn_servers());
415
416 for server in &mut servers {
418 if server.distance_km.is_none() {
419 server.distance_km = Some(self.estimate_distance(geo, server));
420 }
421 }
422
423 servers.sort_by(|a, b| {
425 a.distance_km
426 .unwrap_or(f64::MAX)
427 .partial_cmp(&b.distance_km.unwrap_or(f64::MAX))
428 .unwrap_or(std::cmp::Ordering::Equal)
429 });
430
431 servers.truncate(20);
433
434 let server_count = servers.len();
435 *self.server_pool.write().await = servers;
436
437 emit(
438 &self.events,
439 TestEvent::ServerPoolBuilt {
440 count: server_count,
441 },
442 );
443
444 Ok(())
445 }
446
447 fn get_global_cdn_servers(&self) -> Vec<TestServer> {
448 vec![
450 TestServer {
451 name: "Cloudflare Global".to_string(),
452 url: "https://speed.cloudflare.com".to_string(),
453 location: "Global CDN".to_string(),
454 distance_km: Some(5000.0), latency_ms: None,
456 provider: ServerProvider::Cloudflare,
457 capabilities: ServerCapabilities {
458 supports_download: true,
459 supports_upload: true,
460 supports_latency: true,
461 max_test_size_mb: 2000,
462 geographic_weight: 0.5, },
464 quality_score: None,
465 country_code: None,
466 city: None,
467 is_backup: true,
468 },
469 TestServer {
470 name: "Google Global".to_string(),
471 url: "https://www.google.com".to_string(),
472 location: "Global CDN".to_string(),
473 distance_km: Some(5000.0),
474 latency_ms: None,
475 provider: ServerProvider::Google,
476 capabilities: ServerCapabilities {
477 supports_download: true,
478 supports_upload: false,
479 supports_latency: true,
480 max_test_size_mb: 100,
481 geographic_weight: 0.4,
482 },
483 quality_score: None,
484 country_code: None,
485 city: None,
486 is_backup: true,
487 },
488 ]
489 }
490
491 async fn discover_nearby_servers(&self, geo: &GeoLocation) -> Vec<TestServer> {
493 let mut servers = Vec::new();
494
495 emit(
496 &self.events,
497 TestEvent::Status("Discovering nearby speed test servers...".to_string()),
498 );
499
500 if let Ok(speedtest_servers) = self.fetch_speedtest_net_servers(geo).await {
502 servers.extend(speedtest_servers);
503 }
504
505 servers.extend(self.get_continent_servers(geo));
507
508 servers.extend(self.get_country_servers(geo));
510
511 emit(
512 &self.events,
513 TestEvent::NearbyServersFound {
514 count: servers.len(),
515 },
516 );
517
518 servers
519 }
520
521 async fn fetch_speedtest_net_servers(
523 &self,
524 geo: &GeoLocation,
525 ) -> Result<Vec<TestServer>, Box<dyn std::error::Error>> {
526 let url = "https://www.speedtest.net/api/js/servers?engine=js&limit=10";
528
529 if let Ok(response) = self.client.get(url).send().await {
530 if let Ok(text) = response.text().await {
531 if let Ok(servers) = self.parse_speedtest_servers(&text, geo) {
533 return Ok(servers);
534 }
535 }
536 }
537
538 self.get_open_speedtest_servers(geo).await
540 }
541
542 fn parse_speedtest_servers(
543 &self,
544 json: &str,
545 geo: &GeoLocation,
546 ) -> Result<Vec<TestServer>, Box<dyn std::error::Error>> {
547 let mut servers = Vec::new();
551
552 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json) {
554 if let Some(array) = parsed.as_array() {
555 for server in array.iter().take(10) {
556 if let (Some(host), Some(name), Some(country), Some(lat), Some(lon)) = (
557 server.get("host").and_then(|v| v.as_str()),
558 server.get("name").and_then(|v| v.as_str()),
559 server.get("country").and_then(|v| v.as_str()),
560 server.get("lat").and_then(|v| v.as_f64()),
561 server.get("lon").and_then(|v| v.as_f64()),
562 ) {
563 let distance =
564 self.calculate_distance(geo.latitude, geo.longitude, lat, lon);
565
566 servers.push(TestServer {
567 name: format!("{}, {}", name, country),
568 url: format!("https://{}", host),
569 location: format!("{}, {}", name, country),
570 distance_km: Some(distance),
571 latency_ms: None,
572 provider: ServerProvider::Custom(
573 host.split('.').next().unwrap_or("speedtest").to_string(),
574 ),
575 capabilities: ServerCapabilities {
576 supports_download: true,
577 supports_upload: true,
578 supports_latency: true,
579 max_test_size_mb: 1000,
580 geographic_weight: 1.0,
581 },
582 quality_score: None,
583 country_code: Some(country.to_string()),
584 city: Some(name.to_string()),
585 is_backup: false,
586 });
587 }
588 }
589 }
590 }
591
592 if servers.is_empty() {
593 Err("No servers parsed".into())
594 } else {
595 Ok(servers)
596 }
597 }
598
599 async fn get_open_speedtest_servers(
600 &self,
601 geo: &GeoLocation,
602 ) -> Result<Vec<TestServer>, Box<dyn std::error::Error>> {
603 let mut servers = Vec::new();
605
606 let endpoints = vec![
608 (
609 "Cloudflare (Anycast)",
610 "https://speed.cloudflare.com",
611 0.0,
612 0.0,
613 "Global",
614 ),
615 (
616 "LibreSpeed DE-IX",
617 "https://frankfurt.speedtest.wtnet.de",
618 50.1109,
619 8.6821,
620 "Frankfurt, Germany",
621 ),
622 (
623 "LibreSpeed AMS-IX",
624 "https://ams.speedtest.wtnet.de",
625 52.3676,
626 4.9041,
627 "Amsterdam, Netherlands",
628 ),
629 (
630 "LibreSpeed Singapore",
631 "https://sg.speedtest.wtnet.de",
632 1.3521,
633 103.8198,
634 "Singapore",
635 ),
636 (
637 "LibreSpeed New York",
638 "https://nyc.speedtest.wtnet.de",
639 40.7128,
640 -74.0060,
641 "New York, USA",
642 ),
643 (
644 "LibreSpeed Los Angeles",
645 "https://la.speedtest.wtnet.de",
646 34.0522,
647 -118.2437,
648 "Los Angeles, USA",
649 ),
650 (
651 "LibreSpeed Tokyo",
652 "https://tyo.speedtest.wtnet.de",
653 35.6762,
654 139.6503,
655 "Tokyo, Japan",
656 ),
657 (
658 "LibreSpeed London",
659 "https://lon.speedtest.wtnet.de",
660 51.5074,
661 -0.1278,
662 "London, UK",
663 ),
664 (
665 "LibreSpeed Sydney",
666 "https://syd.speedtest.wtnet.de",
667 -33.8688,
668 151.2093,
669 "Sydney, Australia",
670 ),
671 ];
672
673 for (name, url, lat, lon, location) in endpoints {
674 let distance = if lat == 0.0 && lon == 0.0 {
675 999999.0 } else {
677 self.calculate_distance(geo.latitude, geo.longitude, lat, lon)
678 };
679
680 servers.push(TestServer {
681 name: name.to_string(),
682 url: url.to_string(),
683 location: location.to_string(),
684 distance_km: Some(distance),
685 latency_ms: None,
686 provider: ServerProvider::Custom("LibreSpeed".to_string()),
687 capabilities: ServerCapabilities {
688 supports_download: true,
689 supports_upload: true,
690 supports_latency: true,
691 max_test_size_mb: 2000,
692 geographic_weight: 0.9,
693 },
694 quality_score: None,
695 country_code: Some(location.split(", ").last().unwrap_or("").to_string()),
696 city: Some(location.split(", ").next().unwrap_or(location).to_string()),
697 is_backup: false,
698 });
699 }
700
701 Ok(servers)
702 }
703
704 fn get_continent_servers(&self, geo: &GeoLocation) -> Vec<TestServer> {
705 let mut servers = Vec::new();
706
707 let continent = self.determine_continent(geo.latitude, geo.longitude);
709
710 match continent.as_str() {
711 "North America" => {
712 servers.push(self.create_server_with_coords(
713 geo,
714 "US East Coast Hub",
715 "https://ash.speedtest.wtnet.de",
716 "Ashburn, USA",
717 Some("US".to_string()),
718 39.0438,
719 -77.4874,
720 ));
721 servers.push(self.create_server_with_coords(
722 geo,
723 "US West Coast Hub",
724 "https://lax.speedtest.wtnet.de",
725 "Los Angeles, USA",
726 Some("US".to_string()),
727 34.0522,
728 -118.2437,
729 ));
730 }
731 "Europe" => {
732 servers.push(self.create_server_with_coords(
733 geo,
734 "Europe Central Hub",
735 "https://frankfurt.speedtest.wtnet.de",
736 "Frankfurt, Germany",
737 Some("DE".to_string()),
738 50.1109,
739 8.6821,
740 ));
741 servers.push(self.create_server_with_coords(
742 geo,
743 "Europe West Hub",
744 "https://lon.speedtest.wtnet.de",
745 "London, UK",
746 Some("GB".to_string()),
747 51.5074,
748 -0.1278,
749 ));
750 }
751 "Asia" => {
752 servers.push(self.create_server_with_coords(
753 geo,
754 "Asia Pacific Hub",
755 "https://sg.speedtest.wtnet.de",
756 "Singapore",
757 Some("SG".to_string()),
758 1.3521,
759 103.8198,
760 ));
761 servers.push(self.create_server_with_coords(
762 geo,
763 "Asia East Hub",
764 "https://tokyo.speedtest.wtnet.de",
765 "Tokyo, Japan",
766 Some("JP".to_string()),
767 35.6762,
768 139.6503,
769 ));
770 }
771 "South America" => {
772 servers.push(self.create_server_with_coords(
773 geo,
774 "South America Hub",
775 "https://saopaulo.speedtest.wtnet.de",
776 "São Paulo, Brazil",
777 Some("BR".to_string()),
778 -23.5505,
779 -46.6333,
780 ));
781 }
782 "Africa" => {
783 servers.push(self.create_server_with_coords(
784 geo,
785 "Africa Hub",
786 "https://capetown.speedtest.wtnet.de",
787 "Cape Town, South Africa",
788 Some("ZA".to_string()),
789 -33.9249,
790 18.4241,
791 ));
792 }
793 "Oceania" => {
794 servers.push(self.create_server_with_coords(
795 geo,
796 "Oceania Hub",
797 "https://syd.speedtest.wtnet.de",
798 "Sydney, Australia",
799 Some("AU".to_string()),
800 -33.8688,
801 151.2093,
802 ));
803 }
804 _ => {}
805 }
806
807 servers
808 }
809
810 fn determine_continent(&self, lat: f64, lon: f64) -> String {
811 if lat > 15.0 && lon > -130.0 && lon < -50.0 {
813 "North America".to_string()
814 } else if lat < 15.0 && lat > -60.0 && lon > -85.0 && lon < -30.0 {
815 "South America".to_string()
816 } else if lat > 35.0 && lon > -15.0 && lon < 60.0 {
817 "Europe".to_string()
818 } else if lat > -40.0 && lat < 40.0 && lon > -20.0 && lon < 55.0 {
819 "Africa".to_string()
820 } else if lat > -15.0 && lon > 60.0 && lon < 180.0 {
821 "Asia".to_string()
822 } else if lat < -10.0 && lon > 110.0 && lon < 180.0 {
823 "Oceania".to_string()
824 } else {
825 "Unknown".to_string()
826 }
827 }
828
829 fn get_country_servers(&self, geo: &GeoLocation) -> Vec<TestServer> {
830 let mut servers = Vec::new();
831
832 match geo.country.as_str() {
834 "United States" | "US" => {
835 servers.push(self.create_server(
836 "US Central",
837 "https://dal.speedtest.wtnet.de",
838 "Dallas, USA",
839 Some("US".to_string()),
840 ));
841 }
842 "United Kingdom" | "GB" | "UK" => {
843 servers.push(self.create_server(
844 "UK Primary",
845 "https://lon.speedtest.wtnet.de",
846 "London, UK",
847 Some("GB".to_string()),
848 ));
849 }
850 "Germany" | "DE" => {
851 servers.push(self.create_server(
852 "DE Primary",
853 "https://frankfurt.speedtest.wtnet.de",
854 "Frankfurt, Germany",
855 Some("DE".to_string()),
856 ));
857 }
858 "France" | "FR" => {
859 servers.push(self.create_server(
860 "FR Primary",
861 "https://paris.speedtest.wtnet.de",
862 "Paris, France",
863 Some("FR".to_string()),
864 ));
865 }
866 "Japan" | "JP" => {
867 servers.push(self.create_server(
868 "JP Primary",
869 "https://tyo.speedtest.wtnet.de",
870 "Tokyo, Japan",
871 Some("JP".to_string()),
872 ));
873 }
874 "Australia" | "AU" => {
875 servers.push(self.create_server(
876 "AU Primary",
877 "https://syd.speedtest.wtnet.de",
878 "Sydney, Australia",
879 Some("AU".to_string()),
880 ));
881 }
882 "Canada" | "CA" => {
883 servers.push(self.create_server(
884 "CA Primary",
885 "https://tor.speedtest.wtnet.de",
886 "Toronto, Canada",
887 Some("CA".to_string()),
888 ));
889 }
890 _ => {}
891 }
892
893 servers
894 }
895
896 fn calculate_distance(&self, lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
897 let r = 6371.0; let d_lat = (lat2 - lat1).to_radians();
900 let d_lon = (lon2 - lon1).to_radians();
901 let lat1 = lat1.to_radians();
902 let lat2 = lat2.to_radians();
903
904 let a = (d_lat / 2.0).sin() * (d_lat / 2.0).sin()
905 + lat1.cos() * lat2.cos() * (d_lon / 2.0).sin() * (d_lon / 2.0).sin();
906 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
907
908 r * c
909 }
910
911 #[allow(clippy::too_many_arguments)]
912 fn create_server_with_coords(
913 &self,
914 geo: &GeoLocation,
915 name: &str,
916 url: &str,
917 location: &str,
918 country_code: Option<String>,
919 lat: f64,
920 lon: f64,
921 ) -> TestServer {
922 let distance = self.calculate_distance(geo.latitude, geo.longitude, lat, lon);
923
924 TestServer {
925 name: name.to_string(),
926 url: url.to_string(),
927 location: location.to_string(),
928 distance_km: Some(distance),
929 latency_ms: None,
930 provider: ServerProvider::Custom("LibreSpeed".to_string()),
931 capabilities: ServerCapabilities {
932 supports_download: true,
933 supports_upload: true,
934 supports_latency: true,
935 max_test_size_mb: 2000,
936 geographic_weight: 1.0,
937 },
938 quality_score: None,
939 country_code,
940 city: Some(location.split(", ").next().unwrap_or(location).to_string()),
941 is_backup: false,
942 }
943 }
944
945 fn create_server(
946 &self,
947 name: &str,
948 url: &str,
949 location: &str,
950 country_code: Option<String>,
951 ) -> TestServer {
952 TestServer {
953 name: name.to_string(),
954 url: url.to_string(),
955 location: location.to_string(),
956 distance_km: None,
957 latency_ms: None,
958 provider: ServerProvider::Cloudflare,
959 capabilities: ServerCapabilities {
960 supports_download: true,
961 supports_upload: true,
962 supports_latency: true,
963 max_test_size_mb: 1000,
964 geographic_weight: 1.2,
965 },
966 quality_score: None,
967 country_code,
968 city: Some(location.split(',').next().unwrap_or("").trim().to_string()),
969 is_backup: false,
970 }
971 }
972
973 fn determine_region(&self, country: &str) -> String {
974 match country {
975 "United States" | "Canada" | "Mexico" => "North America".to_string(),
976 "United Kingdom" | "Germany" | "France" | "Spain" | "Italy" | "Netherlands"
977 | "Belgium" | "Switzerland" | "Austria" | "Poland" => "Europe".to_string(),
978 "Japan" | "China" | "South Korea" | "Singapore" | "Australia" | "New Zealand"
979 | "India" => "Asia Pacific".to_string(),
980 "Brazil" | "Argentina" | "Chile" => "South America".to_string(),
981 _ => "Other".to_string(),
982 }
983 }
984
985 fn estimate_distance(&self, geo: &GeoLocation, server: &TestServer) -> f64 {
986 let region = self.determine_region(&geo.country);
989
990 if let Some(city) = &server.city {
991 if city.contains(&geo.city) {
992 return 10.0; }
994 }
995
996 match (region.as_str(), server.location.as_str()) {
997 ("North America", loc) if loc.contains("USA") || loc.contains("Canada") => 500.0,
998 ("Europe", loc) if loc.contains("Europe") || loc.contains("UK") => 300.0,
999 ("Asia Pacific", loc) if loc.contains("Asia") || loc.contains("Japan") => 400.0,
1000 _ => 5000.0, }
1002 }
1003
1004 async fn select_best_servers(&self) -> Result<Vec<TestServer>, Box<dyn std::error::Error>> {
1006 emit(
1007 &self.events,
1008 TestEvent::Status("Testing server performance...".to_string()),
1009 );
1010
1011 let servers = self.server_pool.read().await.clone();
1012
1013 if servers.is_empty() {
1014 return Err("No servers in pool".into());
1015 }
1016
1017 let mut test_results = Vec::new();
1018
1019 let mut futures = FuturesUnordered::new();
1021
1022 for server in servers.into_iter().take(15) {
1023 let client = self.client.clone();
1024 futures.push(async move { Self::quick_latency_test(&client, &server).await });
1025 }
1026
1027 while let Some(result) = futures.next().await {
1028 if let Ok(mut server) = result {
1029 if let Some(latency) = server.latency_ms {
1030 let distance = server.distance_km.unwrap_or(1000.0);
1031 let geographic_weight = server.capabilities.geographic_weight;
1032
1033 let latency_penalty = latency.max(1.0); let distance_penalty = (distance / 100.0).max(1.0);
1038 server.quality_score =
1039 Some((10000.0 * geographic_weight) / (latency_penalty + distance_penalty));
1040
1041 test_results.push(server);
1042 }
1043 }
1044 }
1045
1046 if test_results.is_empty() {
1047 return Err("No servers responded to latency tests".into());
1048 }
1049
1050 test_results.sort_by(|a, b| {
1052 b.quality_score
1053 .unwrap_or(0.0)
1054 .partial_cmp(&a.quality_score.unwrap_or(0.0))
1055 .unwrap_or(std::cmp::Ordering::Equal)
1056 });
1057
1058 let selected = test_results
1059 .into_iter()
1060 .take(SERVER_SELECTION_COUNT)
1061 .collect::<Vec<_>>();
1062
1063 if !self.config.json_output {
1064 emit(
1065 &self.events,
1066 TestEvent::ServersSelected {
1067 servers: selected
1068 .iter()
1069 .map(|server| SelectedServer {
1070 name: server.name.clone(),
1071 location: server.location.clone(),
1072 latency_ms: server.latency_ms.unwrap_or(0.0),
1073 distance_km: server.distance_km.unwrap_or(0.0),
1074 })
1075 .collect(),
1076 },
1077 );
1078 }
1079
1080 Ok(selected)
1081 }
1082
1083 async fn quick_latency_test(
1084 client: &Client,
1085 server: &TestServer,
1086 ) -> Result<TestServer, Box<dyn std::error::Error>> {
1087 let mut latencies = Vec::new();
1088 let mut server = server.clone();
1089
1090 for _ in 0..3 {
1091 let start = Instant::now();
1092 match client
1093 .head(&server.url)
1094 .timeout(Duration::from_secs(2))
1095 .send()
1096 .await
1097 {
1098 Ok(resp) if resp.status().is_success() || resp.status().is_redirection() => {
1099 latencies.push(start.elapsed().as_millis() as f64);
1100 }
1101 _ => {}
1102 }
1103 }
1104
1105 if !latencies.is_empty() {
1106 server.latency_ms = Some(latencies.iter().sum::<f64>() / latencies.len() as f64);
1107 }
1108
1109 Ok(server)
1110 }
1111
1112 async fn progressive_download_test(&self) -> Result<f64, Box<dyn std::error::Error>> {
1119 emit(&self.events, TestEvent::PhaseStarted(Phase::Download));
1120
1121 let total_bytes = Arc::new(AtomicU64::new(0));
1124 let start = Instant::now();
1125 let test_duration = Duration::from_secs(TEST_SECS);
1126
1127 let mut handles = Vec::new();
1128 for _ in 0..PARALLEL_CONNECTIONS {
1129 let url = DOWNLOAD_ENDPOINT.to_string();
1130 let client = self.client.clone();
1131 let total_bytes = Arc::clone(&total_bytes);
1132 let test_start = start;
1133
1134 let handle = tokio::spawn(async move {
1135 let end_time = test_start + test_duration;
1136 while Instant::now() < end_time {
1137 match client.get(&url).send().await {
1138 Ok(response) => {
1139 let mut stream = response.bytes_stream();
1140 while let Some(chunk_result) = stream.next().await {
1141 if Instant::now() >= end_time {
1142 break;
1143 }
1144 if let Ok(chunk) = chunk_result {
1145 total_bytes.fetch_add(chunk.len() as u64, Ordering::Relaxed);
1146 }
1147 }
1148 }
1149 Err(_) => tokio::time::sleep(Duration::from_millis(100)).await,
1150 }
1151 if Instant::now() >= end_time {
1152 break;
1153 }
1154 }
1155 });
1156 handles.push(handle);
1157 }
1158
1159 let events = self.events.clone();
1161 let total_bytes_monitor = Arc::clone(&total_bytes);
1162 let monitor_handle = tokio::spawn(async move {
1163 collect_throughput_samples(total_bytes_monitor, start, test_duration, events, false)
1164 .await
1165 });
1166
1167 for handle in handles {
1168 let _ = handle.await;
1169 }
1170 let samples = monitor_handle.await.unwrap_or_default();
1171
1172 let mbps = sustained_mbps(&samples, WARMUP_SECS).clamp(0.0, 10_000.0);
1173 emit(&self.events, TestEvent::DownloadComplete { mbps });
1174 Ok(mbps)
1175 }
1176
1177 async fn progressive_upload_test(&self) -> Result<f64, Box<dyn std::error::Error>> {
1179 emit(&self.events, TestEvent::PhaseStarted(Phase::Upload));
1180
1181 let total_bytes = Arc::new(AtomicU64::new(0));
1182 let start = Instant::now();
1183 let test_duration = Duration::from_secs(TEST_SECS);
1184
1185 let chunk_size = 5 * 1024 * 1024;
1187 let test_data = vec![0u8; chunk_size];
1188
1189 let mut handles = Vec::new();
1190 for _ in 0..10 {
1191 let url = UPLOAD_ENDPOINT.to_string();
1192 let client = self.client.clone();
1193 let total_bytes = Arc::clone(&total_bytes);
1194 let data = test_data.clone();
1195 let test_start = start;
1196
1197 let handle = tokio::spawn(async move {
1198 let end_time = test_start + test_duration;
1199 while Instant::now() < end_time {
1200 match client
1201 .post(&url)
1202 .body(data.clone())
1203 .timeout(Duration::from_secs(10))
1204 .send()
1205 .await
1206 {
1207 Ok(_) => {
1208 total_bytes.fetch_add(data.len() as u64, Ordering::Relaxed);
1209 }
1210 Err(_) => tokio::time::sleep(Duration::from_millis(100)).await,
1211 }
1212 }
1213 });
1214 handles.push(handle);
1215 }
1216
1217 let events = self.events.clone();
1218 let total_bytes_monitor = Arc::clone(&total_bytes);
1219 let monitor_handle = tokio::spawn(async move {
1220 collect_throughput_samples(total_bytes_monitor, start, test_duration, events, true)
1221 .await
1222 });
1223
1224 for handle in handles {
1225 let _ = handle.await;
1226 }
1227 let samples = monitor_handle.await.unwrap_or_default();
1228
1229 let mbps = sustained_mbps(&samples, WARMUP_SECS).clamp(0.0, 10_000.0);
1230 emit(&self.events, TestEvent::UploadComplete { mbps });
1231 Ok(mbps)
1232 }
1233
1234 async fn measure_latency(
1235 &self,
1236 server: &TestServer,
1237 ) -> Result<f64, Box<dyn std::error::Error>> {
1238 emit(&self.events, TestEvent::PhaseStarted(Phase::Latency));
1239
1240 let mut latencies = Vec::new();
1241
1242 for _i in 0..10 {
1243 let start = Instant::now();
1244 match self
1245 .client
1246 .head(&server.url)
1247 .timeout(Duration::from_secs(2))
1248 .send()
1249 .await
1250 {
1251 Ok(resp) if resp.status().is_success() || resp.status().is_redirection() => {
1252 let latency = start.elapsed().as_millis() as f64;
1253 latencies.push(latency);
1254
1255 let current_avg = latencies.iter().sum::<f64>() / latencies.len() as f64;
1256 emit(
1257 &self.events,
1258 TestEvent::LatencyProgress {
1259 avg_ms: current_avg,
1260 },
1261 );
1262 }
1263 _ => {}
1264 }
1265
1266 tokio::time::sleep(Duration::from_millis(100)).await;
1267 }
1268
1269 let avg_latency = if !latencies.is_empty() {
1270 latencies.iter().sum::<f64>() / latencies.len() as f64
1271 } else {
1272 50.0
1273 };
1274
1275 emit(
1276 &self.events,
1277 TestEvent::LatencyComplete {
1278 avg_ms: avg_latency,
1279 },
1280 );
1281
1282 Ok(avg_latency)
1283 }
1284
1285 async fn measure_jitter_and_loss(
1286 &self,
1287 server: &TestServer,
1288 ) -> Result<(f64, f64), Box<dyn std::error::Error>> {
1289 let mut latencies = Vec::new();
1290 let mut lost = 0;
1291 let total = 20;
1292
1293 for _ in 0..total {
1294 let start = Instant::now();
1295 match self
1296 .client
1297 .head(&server.url)
1298 .timeout(Duration::from_secs(1))
1299 .send()
1300 .await
1301 {
1302 Ok(resp) if resp.status().is_success() || resp.status().is_redirection() => {
1303 latencies.push(start.elapsed().as_millis() as f64);
1304 }
1305 _ => {
1306 lost += 1;
1307 }
1308 }
1309 tokio::time::sleep(Duration::from_millis(50)).await;
1310 }
1311
1312 let jitter = if latencies.len() > 1 {
1313 let mean = latencies.iter().sum::<f64>() / latencies.len() as f64;
1314 let variance =
1315 latencies.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / latencies.len() as f64;
1316 variance.sqrt()
1317 } else {
1318 0.0
1319 };
1320
1321 let packet_loss = (lost as f64 / total as f64) * 100.0;
1322
1323 Ok((jitter, packet_loss))
1324 }
1325
1326 async fn get_client_ip(&self) -> Option<IpAddr> {
1327 if let Ok(response) = self
1328 .client
1329 .get("https://api.ipify.org?format=json")
1330 .timeout(Duration::from_secs(3))
1331 .send()
1332 .await
1333 {
1334 if let Ok(json) = response.json::<serde_json::Value>().await {
1335 return json["ip"].as_str().and_then(|s| s.parse::<IpAddr>().ok());
1336 }
1337 }
1338 None
1339 }
1340
1341 async fn resolve_server_ip(&self, url: &str) -> Option<IpAddr> {
1342 if let Ok(parsed) = url.parse::<reqwest::Url>() {
1343 if let Some(host) = parsed.host_str() {
1344 if let Ok(addrs) = tokio::net::lookup_host(format!("{}:443", host)).await {
1345 return addrs.into_iter().next().map(|addr| addr.ip());
1346 }
1347 }
1348 }
1349 None
1350 }
1351}
1352
1353fn geo_str_field(
1355 json: &serde_json::Value,
1356 key: &str,
1357) -> Result<String, Box<dyn std::error::Error>> {
1358 json[key]
1359 .as_str()
1360 .filter(|s| !s.is_empty() && *s != "Unknown")
1361 .map(str::to_string)
1362 .ok_or_else(|| format!("Invalid {key}").into())
1363}
1364
1365fn parse_latlon_pair(loc: &str) -> Result<(f64, f64), Box<dyn std::error::Error>> {
1367 let coords: Vec<&str> = loc.split(',').collect();
1368 if coords.len() != 2 {
1369 return Err("Invalid coordinates format".into());
1370 }
1371 let lat = coords[0].trim().parse().map_err(|_| "Invalid latitude")?;
1372 let lon = coords[1].trim().parse().map_err(|_| "Invalid longitude")?;
1373 Ok((lat, lon))
1374}
1375
1376fn build_geo(
1380 country: String,
1381 city: String,
1382 latitude: f64,
1383 longitude: f64,
1384 isp: Option<String>,
1385) -> Result<GeoLocation, Box<dyn std::error::Error>> {
1386 if latitude == 0.0 && longitude == 0.0 {
1387 return Err("Invalid coordinates".into());
1388 }
1389 Ok(GeoLocation {
1390 country,
1391 city,
1392 latitude,
1393 longitude,
1394 isp,
1395 })
1396}
1397
1398async fn collect_throughput_samples(
1403 total_bytes: Arc<AtomicU64>,
1404 start: Instant,
1405 test_duration: Duration,
1406 events: Option<EventSender>,
1407 is_upload: bool,
1408) -> Vec<(f64, f64)> {
1409 let mut samples = Vec::new();
1410 let mut last_bytes = 0u64;
1411 let mut last_time = Instant::now();
1412 let mut peak = 0.0f64;
1413 let end_time = start + test_duration;
1414
1415 while Instant::now() < end_time {
1416 tokio::time::sleep(Duration::from_millis(200)).await;
1417
1418 let bytes = total_bytes.load(Ordering::Relaxed);
1419 let time_diff = last_time.elapsed().as_secs_f64();
1420 if time_diff >= 0.2 {
1421 let bytes_diff = bytes.saturating_sub(last_bytes);
1422 let mbps = (bytes_diff as f64 * 8.0) / (time_diff * 1_000_000.0);
1423 peak = peak.max(mbps);
1424 let elapsed = start.elapsed().as_secs_f64();
1425 samples.push((elapsed, mbps));
1426
1427 let event = if is_upload {
1428 TestEvent::UploadSample {
1429 mbps,
1430 peak_mbps: peak,
1431 elapsed_secs: elapsed,
1432 }
1433 } else {
1434 TestEvent::DownloadSample {
1435 mbps,
1436 peak_mbps: peak,
1437 elapsed_secs: elapsed,
1438 }
1439 };
1440 emit(&events, event);
1441
1442 last_bytes = bytes;
1443 last_time = Instant::now();
1444 }
1445 }
1446 samples
1447}
1448
1449fn sustained_mbps(samples: &[(f64, f64)], warmup_secs: f64) -> f64 {
1455 let mut steady: Vec<f64> = samples
1456 .iter()
1457 .filter(|(t, mbps)| *t >= warmup_secs && *mbps > 0.0)
1458 .map(|(_, mbps)| *mbps)
1459 .collect();
1460
1461 if steady.is_empty() {
1463 steady = samples
1464 .iter()
1465 .map(|(_, mbps)| *mbps)
1466 .filter(|mbps| *mbps > 0.0)
1467 .collect();
1468 }
1469 if steady.is_empty() {
1470 return 0.0;
1471 }
1472
1473 steady.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1474 let trim = steady.len() / 10;
1475 let slice = &steady[trim..steady.len() - trim];
1476 let slice = if slice.is_empty() { &steady[..] } else { slice };
1477 slice.iter().sum::<f64>() / slice.len() as f64
1478}
1479
1480#[cfg(test)]
1481mod tests {
1482 use super::*;
1483
1484 #[test]
1485 fn sustained_mbps_excludes_warmup() {
1486 let samples = vec![
1488 (0.2, 5.0),
1489 (1.0, 20.0),
1490 (2.0, 60.0),
1491 (3.2, 100.0),
1492 (4.0, 101.0),
1493 (5.0, 99.0),
1494 (6.0, 100.0),
1495 ];
1496 let mbps = sustained_mbps(&samples, WARMUP_SECS);
1497 assert!(
1498 (mbps - 100.0).abs() < 5.0,
1499 "expected ~100 sustained, got {mbps}"
1500 );
1501 }
1502
1503 #[test]
1504 fn sustained_mbps_trims_outliers() {
1505 let mut samples = vec![(3.5, 0.1), (3.6, 5000.0)];
1507 for i in 0..20 {
1508 samples.push((4.0 + i as f64 * 0.1, 500.0));
1509 }
1510 let mbps = sustained_mbps(&samples, WARMUP_SECS);
1511 assert!((mbps - 500.0).abs() < 20.0, "expected ~500, got {mbps}");
1512 }
1513
1514 #[test]
1515 fn sustained_mbps_zero_when_no_data() {
1516 assert_eq!(sustained_mbps(&[], WARMUP_SECS), 0.0);
1517 assert_eq!(sustained_mbps(&[(4.0, 0.0), (5.0, 0.0)], WARMUP_SECS), 0.0);
1518 }
1519
1520 #[test]
1521 fn sustained_mbps_falls_back_for_short_tests() {
1522 let samples = vec![(0.5, 40.0), (1.0, 60.0), (1.5, 50.0)];
1524 let mbps = sustained_mbps(&samples, WARMUP_SECS);
1525 assert!(mbps > 0.0, "short test should still estimate, got {mbps}");
1526 }
1527
1528 #[test]
1529 fn geo_str_field_extracts_and_rejects() {
1530 let json = serde_json::json!({ "city": "Berlin", "empty": "", "unk": "Unknown" });
1531 assert_eq!(geo_str_field(&json, "city").unwrap(), "Berlin");
1532 assert!(geo_str_field(&json, "empty").is_err());
1533 assert!(geo_str_field(&json, "unk").is_err());
1534 assert!(geo_str_field(&json, "missing").is_err());
1535 }
1536
1537 #[test]
1538 fn parse_latlon_pair_ok_and_err() {
1539 assert_eq!(parse_latlon_pair("52.52,13.40").unwrap(), (52.52, 13.40));
1540 assert_eq!(parse_latlon_pair(" 1.0 , 2.0 ").unwrap(), (1.0, 2.0));
1541 assert!(parse_latlon_pair("52.52").is_err());
1542 assert!(parse_latlon_pair("a,b").is_err());
1543 }
1544
1545 #[test]
1546 fn build_geo_rejects_zero_coords() {
1547 assert!(build_geo("C".into(), "City".into(), 0.0, 0.0, None).is_err());
1548 let g = build_geo("C".into(), "City".into(), 1.0, 2.0, Some("ISP".into())).unwrap();
1549 assert_eq!(g.latitude, 1.0);
1550 assert_eq!(g.isp.as_deref(), Some("ISP"));
1551 }
1552
1553 #[test]
1554 fn test_region_determination() {
1555 let _ = rustls::crypto::ring::default_provider().install_default();
1557 let config = TestConfig::default();
1558 let speed_test = SpeedTest::new(config).unwrap();
1559
1560 assert_eq!(
1561 speed_test.determine_region("United States"),
1562 "North America"
1563 );
1564 assert_eq!(speed_test.determine_region("Germany"), "Europe");
1565 assert_eq!(speed_test.determine_region("Japan"), "Asia Pacific");
1566 }
1567}