1use std::time::Instant;
2
3use reqwest::blocking::Client;
4use serde::Deserialize;
5use url::Url;
6
7use crate::Error;
8use crate::server::Server;
9
10pub const DEFAULT_LIBRESPEED_SERVERS_URL: &str =
11 "https://librespeed.org/backend-servers/servers.php";
12
13pub const DEFAULT_IPERF3_SERVERS_URL: &str =
14 "https://export.iperf3serverlist.net/listed_iperf3_servers.json";
15
16pub const FAILOVER_EXTRA: usize = 2;
19
20#[derive(Debug, Clone, Deserialize)]
21pub struct LibreSpeedListEntry {
22 pub id: Option<u64>,
23 pub name: String,
24 pub server: String,
25 #[serde(rename = "dlURL")]
26 pub dl_url: String,
27 #[serde(rename = "ulURL")]
28 pub ul_url: String,
29 #[serde(rename = "pingURL")]
30 pub ping_url: String,
31 #[serde(rename = "sponsorName")]
32 pub sponsor_name: Option<String>,
33}
34
35impl LibreSpeedListEntry {
36 pub fn into_server(self) -> Server {
37 let base_url = normalize_base_url(&self.server);
38 let id = self
39 .id
40 .map(|i| i.to_string())
41 .unwrap_or_else(|| base_url.clone());
42 Server {
43 id,
44 name: self.name,
45 base_url,
46 country: None,
47 sponsor: self.sponsor_name,
48 port: None,
49 dl_path: self.dl_url,
50 ul_path: self.ul_url,
51 ping_path: self.ping_url,
52 }
53 }
54}
55
56fn normalize_base_url(raw: &str) -> String {
57 let s = if let Some(rest) = raw.strip_prefix("//") {
58 format!("https://{rest}")
59 } else {
60 raw.to_string()
61 };
62 if s.ends_with('/') { s } else { format!("{s}/") }
63}
64
65fn join(base: &str, path: &str) -> Result<Url, Error> {
66 let base = if base.ends_with('/') {
67 base.to_string()
68 } else {
69 format!("{base}/")
70 };
71 Ok(Url::parse(&base)?.join(path)?)
72}
73
74pub fn parse_librespeed_servers(json: &str) -> Result<Vec<Server>, Error> {
76 let entries: Vec<LibreSpeedListEntry> = serde_json::from_str(json)?;
77 Ok(entries.into_iter().map(|e| e.into_server()).collect())
78}
79
80pub fn fetch_librespeed_servers(client: &Client, list_url: &str) -> Result<Vec<Server>, Error> {
82 let text = client.get(list_url).send()?.error_for_status()?.text()?;
83 parse_librespeed_servers(&text)
84}
85
86#[derive(Debug, Clone, Deserialize)]
87pub struct Iperf3ListEntry {
88 #[serde(rename = "IP/HOST")]
89 pub host: String,
90 #[serde(rename = "PORT")]
91 pub port: String,
92 #[serde(rename = "CONTINENT")]
93 #[allow(dead_code)]
94 pub continent: Option<String>,
95 #[serde(rename = "COUNTRY")]
96 pub country: Option<String>,
97 #[serde(rename = "SITE")]
98 pub site: Option<String>,
99 #[serde(rename = "PROVIDER")]
100 pub provider: Option<String>,
101}
102
103fn iperf3_display_name(entry: &Iperf3ListEntry, port: u16) -> String {
104 let site = entry
105 .site
106 .as_deref()
107 .filter(|s| !s.is_empty())
108 .unwrap_or(&entry.host);
109 let country = entry
110 .country
111 .as_deref()
112 .filter(|s| !s.is_empty())
113 .unwrap_or("??");
114 let provider = entry
115 .provider
116 .as_deref()
117 .filter(|s| !s.is_empty())
118 .unwrap_or("unknown");
119 format!("{site}, {country} ({provider}) :{port}")
120}
121
122pub fn parse_port_range(raw: &str) -> Result<Vec<u16>, Error> {
123 let raw = raw.trim();
124 if raw.is_empty() {
125 return Err(Error::Message("empty iperf3 port".into()));
126 }
127 if let Some((start, end)) = raw.split_once('-') {
128 let start: u16 = start
129 .trim()
130 .parse()
131 .map_err(|_| Error::Message(format!("invalid iperf3 port range start: {raw}")))?;
132 let end: u16 = end
133 .trim()
134 .parse()
135 .map_err(|_| Error::Message(format!("invalid iperf3 port range end: {raw}")))?;
136 if start > end {
137 return Err(Error::Message(format!(
138 "invalid iperf3 port range: (start > end): {raw}"
139 )));
140 }
141 return Ok((start..=end).collect());
142 }
143 let port: u16 = raw
144 .parse()
145 .map_err(|_| Error::Message(format!("invalid iperf3 port: {raw}")))?;
146 Ok(vec![port])
147}
148
149pub fn parse_iperf3_servers(json: &str) -> Result<Vec<Server>, Error> {
151 let entries: Vec<Iperf3ListEntry> = serde_json::from_str(json)?;
152 let mut servers = Vec::new();
153 let mut next_id = 1_u64;
154 for entry in entries {
155 for port in parse_port_range(&entry.port)? {
156 let id = next_id.to_string();
157 next_id += 1;
158 servers.push(Server {
159 id,
160 name: iperf3_display_name(&entry, port),
161 base_url: entry.host.clone(),
162 country: entry.country.clone(),
163 sponsor: entry.provider.clone(),
164 port: Some(port),
165 ..Server::iperf3(&entry.host, port)
166 });
167 }
168 }
169 Ok(servers)
170}
171
172pub fn fetch_iperf3_servers(client: &Client, list_url: &str) -> Result<Vec<Server>, Error> {
174 let text = client.get(list_url).send()?.error_for_status()?.text()?;
175 parse_iperf3_servers(&text)
176}
177
178pub fn servers_list_url(backend_is_iperf3: bool, servers_url: &str) -> &str {
180 if backend_is_iperf3 && servers_url == DEFAULT_LIBRESPEED_SERVERS_URL {
181 DEFAULT_IPERF3_SERVERS_URL
182 } else {
183 servers_url
184 }
185}
186
187pub fn ping_ms(client: &Client, server: &Server) -> Result<f64, Error> {
189 let url = join(&server.base_url, &server.ping_path)?;
190 let start = Instant::now();
191 let resp = client.get(url).send()?.error_for_status()?;
192 let _ = resp.bytes()?;
193 Ok(start.elapsed().as_secs_f64() * 1000.0)
194}
195
196pub fn rank_by_latency(client: &Client, servers: &[Server]) -> Vec<(Server, f64)> {
198 let mut ranked = Vec::new();
199 for s in servers {
200 if let Ok(ms) = ping_ms(client, s) {
201 ranked.push((s.clone(), ms));
202 }
203 }
204 ranked.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
205 ranked
206}
207
208pub fn pick_lowest_latency(client: &Client, servers: &[Server]) -> Result<(Server, f64), Error> {
210 rank_by_latency(client, servers)
211 .into_iter()
212 .next()
213 .ok_or_else(|| Error::Message("no LibreSpeed servers responded to ping".into()))
214}
215
216pub fn failover_candidates(
218 ranked: &[(Server, f64)],
219 preferred: &Server,
220 extra: usize,
221) -> Vec<Server> {
222 let mut out = vec![preferred.clone()];
223 for (s, _) in ranked {
224 if out.len() > extra {
225 break;
226 }
227 if s.id == preferred.id || s.base_url == preferred.base_url {
228 continue;
229 }
230 out.push(s.clone());
231 }
232 out
233}
234
235pub fn server_by_id(servers: &[Server], id: u64) -> Result<Server, Error> {
237 let key = id.to_string();
238 servers
239 .iter()
240 .find(|s| s.id == key)
241 .cloned()
242 .ok_or_else(|| Error::Message(format!("no server with id {id} found")))
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn parses_fixture_list() {
251 let json = include_str!("../tests/fixtures/librespeed/servers.json");
252 let servers = parse_librespeed_servers(json).unwrap();
253 assert_eq!(servers.len(), 2);
254 assert_eq!(servers[0].id, "52");
255 assert!(servers[0].base_url.starts_with("https://"));
256 assert_eq!(
257 servers[0].base_url,
258 ("https://nyc.speedtest.clouvider.net/backend/")
259 ); assert_eq!(servers[0].dl_path, "garbage.php");
261 assert_eq!(servers[1].ping_path, "backend/empty.php");
262 }
263
264 #[test]
265 fn normalizes_protocol_relative() {
266 let json = r#"[{
267 "id": 1,
268 "name": "Test",
269 "server": "//example.com/backend",
270 "dlURL": "garbage.php",
271 "ulURL": "empty.php",
272 "pingURL": "empty.php"
273 }]"#;
274 let servers = parse_librespeed_servers(json).unwrap();
275 assert_eq!(servers[0].base_url, "https://example.com/backend/");
276 }
277
278 #[test]
279 fn parses_iperf3_fixture_list() {
280 let json = include_str!("../tests/fixtures/iperf3/servers.json");
281 let servers = parse_iperf3_servers(json).unwrap();
282 assert_eq!(servers.len(), 4);
283 assert_eq!(servers[0].id, "1");
284 assert_eq!(servers[0].base_url, "41.110.39.130");
285 assert_eq!(servers[0].port, Some(5201));
286 assert_eq!(servers[0].name, "Algiers, DZ (DATAPACKET) :5201");
287 assert_eq!(servers[3].port, Some(5203));
288 let picked = server_by_id(&servers, 3).unwrap();
289 assert_eq!(picked.base_url, "105.235.237.2");
290 assert_eq!(picked.port, Some(5202));
291 }
292
293 #[test]
294 fn parse_port_range_single_and_span() {
295 assert_eq!(parse_port_range("5201").unwrap(), vec![5201]);
296 assert_eq!(
297 parse_port_range("5201-5203").unwrap(),
298 vec![5201, 5202, 5203]
299 );
300 assert!(parse_port_range("5203-5201").is_err());
301 }
302
303 #[test]
304 fn servers_list_url_switches_for_iperf3() {
305 assert_eq!(
306 servers_list_url(true, DEFAULT_LIBRESPEED_SERVERS_URL),
307 DEFAULT_IPERF3_SERVERS_URL
308 );
309 assert_eq!(
310 servers_list_url(true, "https://example/list.json"),
311 "https://example/list.json"
312 );
313 }
314
315 #[test]
316 fn failover_candidates_preferred_then_ping_order() {
317 let a = Server::librespeed("https://a/");
318 let b = Server::librespeed("https://b/");
319 let c = Server::librespeed("https://c/");
320 let ranked = vec![(a.clone(), 5.0), (b.clone(), 10.0), (c.clone(), 20.0)];
321 let got = failover_candidates(&ranked, &b, 2);
322 assert_eq!(got.len(), 3);
323 assert_eq!(got[0].base_url, "https://b/");
324 assert_eq!(got[1].base_url, "https://a/");
325 assert_eq!(got[2].base_url, "https://c/");
326 }
327}