1use crate::common;
2use crate::config::Config;
3use crate::error::SpeedtestError;
4use reqwest::Client;
5
6pub fn create_client(config: &Config) -> Result<Client, SpeedtestError> {
13 let mut builder = Client::builder()
14 .timeout(std::time::Duration::from_secs(config.timeout))
15 .http1_only()
16 .no_gzip()
17 .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
18
19 if let Some(ref source_ip) = config.source {
20 let addr: std::net::SocketAddr = source_ip
21 .parse()
22 .map_err(|e| SpeedtestError::with_source("Invalid source IP", e))?;
23 builder = builder.local_address(addr.ip());
24 }
25
26 let client = builder.build().map_err(SpeedtestError::NetworkError)?;
27
28 Ok(client)
29}
30
31pub async fn discover_client_ip(client: &Client) -> Result<String, SpeedtestError> {
37 if let Ok(response) = client
38 .get("https://www.speedtest.net/api/ip.php")
39 .send()
40 .await
41 {
42 if let Ok(text) = response.text().await {
43 let trimmed = text.trim().to_string();
44 if common::is_valid_ipv4(&trimmed) {
45 return Ok(trimmed);
46 }
47 }
48 }
49
50 if let Ok(response) = client
51 .get("https://www.speedtest.net/api/ios-config.php")
52 .send()
53 .await
54 {
55 if let Ok(text) = response.text().await {
56 if let Some(ip) = parse_ip_from_xml(&text) {
57 return Ok(ip);
58 }
59 }
60 }
61
62 Ok("unknown".to_string())
63}
64
65fn parse_ip_from_xml(xml: &str) -> Option<String> {
66 #[derive(serde::Deserialize)]
69 struct Settings {
70 client: ClientElement,
71 }
72 #[derive(serde::Deserialize)]
73 struct ClientElement {
74 #[serde(rename = "@ip")]
75 ip: Option<String>,
76 }
77
78 let settings: Settings = quick_xml::de::from_str(xml).ok()?;
79 let ip = settings.client.ip?;
80 if common::is_valid_ipv4(&ip) {
81 Some(ip)
82 } else {
83 None
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn test_parse_ip_from_xml() {
93 let xml = r#"<settings><client country="CA" ip="173.35.57.235" isp="Rogers"/></settings>"#;
94 assert_eq!(parse_ip_from_xml(xml), Some("173.35.57.235".to_string()));
95 }
96
97 #[test]
98 fn test_parse_ip_from_xml_full_response() {
99 let xml = r#"<?xml version="1.0"?>
100<settings>
101 <config downloadThreadCountV3="4"/>
102 <client country="CA" ip="173.35.57.235" isp="Rogers"/>
103</settings>"#;
104 assert_eq!(parse_ip_from_xml(xml), Some("173.35.57.235".to_string()));
105 }
106
107 #[test]
108 fn test_parse_ip_from_xml_invalid() {
109 assert!(parse_ip_from_xml("not xml").is_none());
110 assert!(parse_ip_from_xml("<html></html>").is_none());
111 assert!(parse_ip_from_xml("<settings><client ip=\"invalid\"/></settings>").is_none());
112 }
113
114 #[test]
115 fn test_create_client_invalid_source_ip() {
116 use crate::cli::CliArgs;
117 use clap::Parser;
118 let args = CliArgs::parse_from(["netspeed-cli"]);
119 let mut config = Config::from_args(&args);
120 config.source = Some("invalid-ip".to_string());
121 let result = create_client(&config);
122 assert!(result.is_err());
123 assert!(matches!(
124 result.unwrap_err(),
125 SpeedtestError::Context { .. }
126 ));
127 }
128
129 #[test]
130 fn test_create_client_valid_config() {
131 use crate::cli::CliArgs;
132 use clap::Parser;
133 let args = CliArgs::parse_from(["netspeed-cli"]);
134 let config = Config::from_args(&args);
135 let result = create_client(&config);
136 assert!(result.is_ok());
137 }
138
139 #[test]
140 fn test_create_client_with_source_ip() {
141 use crate::cli::CliArgs;
142 use clap::Parser;
143 let args = CliArgs::parse_from(["netspeed-cli", "--source", "0.0.0.0"]);
144 let config = Config::from_args(&args);
145 let result = create_client(&config);
146 match result {
147 Ok(_) | Err(SpeedtestError::NetworkError(_)) | Err(SpeedtestError::Context { .. }) => {}
148 Err(e) => panic!("Unexpected error type: {e:?}"),
149 }
150 }
151
152 #[test]
153 fn test_create_client_custom_timeout() {
154 use crate::cli::CliArgs;
155 use clap::Parser;
156 let args = CliArgs::parse_from(["netspeed-cli", "--timeout", "30"]);
157 let config = Config::from_args(&args);
158 let result = create_client(&config);
159 assert!(result.is_ok());
160 }
161}