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