use crate::common;
use crate::config::Config;
use crate::error::SpeedtestError;
use reqwest::Client;
pub fn create_client(config: &Config) -> Result<Client, SpeedtestError> {
let mut builder = Client::builder()
.timeout(std::time::Duration::from_secs(config.timeout))
.http1_only()
.no_gzip()
.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");
if let Some(ref source_ip) = config.source {
let addr: std::net::SocketAddr = source_ip
.parse()
.map_err(|e| SpeedtestError::with_source("Invalid source IP", e))?;
builder = builder.local_address(addr.ip());
}
let client = builder.build().map_err(SpeedtestError::NetworkError)?;
Ok(client)
}
pub async fn discover_client_ip(client: &Client) -> Result<String, SpeedtestError> {
if let Ok(response) = client
.get("https://www.speedtest.net/api/ip.php")
.send()
.await
{
if let Ok(text) = response.text().await {
let trimmed = text.trim().to_string();
if common::is_valid_ipv4(&trimmed) {
return Ok(trimmed);
}
}
}
if let Ok(response) = client
.get("https://www.speedtest.net/api/ios-config.php")
.send()
.await
{
if let Ok(text) = response.text().await {
if let Some(ip) = parse_ip_from_xml(&text) {
return Ok(ip);
}
}
}
Ok("unknown".to_string())
}
fn parse_ip_from_xml(xml: &str) -> Option<String> {
#[derive(serde::Deserialize)]
struct Settings {
client: ClientElement,
}
#[derive(serde::Deserialize)]
struct ClientElement {
#[serde(rename = "@ip")]
ip: Option<String>,
}
let settings: Settings = quick_xml::de::from_str(xml).ok()?;
let ip = settings.client.ip?;
if common::is_valid_ipv4(&ip) {
Some(ip)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ip_from_xml() {
let xml = r#"<settings><client country="CA" ip="173.35.57.235" isp="Rogers"/></settings>"#;
assert_eq!(parse_ip_from_xml(xml), Some("173.35.57.235".to_string()));
}
#[test]
fn test_parse_ip_from_xml_full_response() {
let xml = r#"<?xml version="1.0"?>
<settings>
<config downloadThreadCountV3="4"/>
<client country="CA" ip="173.35.57.235" isp="Rogers"/>
</settings>"#;
assert_eq!(parse_ip_from_xml(xml), Some("173.35.57.235".to_string()));
}
#[test]
fn test_parse_ip_from_xml_invalid() {
assert!(parse_ip_from_xml("not xml").is_none());
assert!(parse_ip_from_xml("<html></html>").is_none());
assert!(parse_ip_from_xml("<settings><client ip=\"invalid\"/></settings>").is_none());
}
#[test]
fn test_create_client_invalid_source_ip() {
use crate::cli::CliArgs;
use clap::Parser;
let args = CliArgs::parse_from(["netspeed-cli"]);
let mut config = Config::from_args(&args);
config.source = Some("invalid-ip".to_string());
let result = create_client(&config);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SpeedtestError::Context { .. }
));
}
#[test]
fn test_create_client_valid_config() {
use crate::cli::CliArgs;
use clap::Parser;
let args = CliArgs::parse_from(["netspeed-cli"]);
let config = Config::from_args(&args);
let result = create_client(&config);
assert!(result.is_ok());
}
#[test]
fn test_create_client_with_source_ip() {
use crate::cli::CliArgs;
use clap::Parser;
let args = CliArgs::parse_from(["netspeed-cli", "--source", "0.0.0.0"]);
let config = Config::from_args(&args);
let result = create_client(&config);
match result {
Ok(_) | Err(SpeedtestError::NetworkError(_)) | Err(SpeedtestError::Context { .. }) => {}
Err(e) => panic!("Unexpected error type: {e:?}"),
}
}
#[test]
fn test_create_client_custom_timeout() {
use crate::cli::CliArgs;
use clap::Parser;
let args = CliArgs::parse_from(["netspeed-cli", "--timeout", "30"]);
let config = Config::from_args(&args);
let result = create_client(&config);
assert!(result.is_ok());
}
}