Skip to main content

shardx/
geo.rs

1//! Live geo lookup for proxies — mirrors `geo_check_via` in the launcher.
2//! Supports ip-api.com / ipapi.co / ipwho.is. SOCKS5 routes via `socks5h`
3//! (DNS through the proxy); HTTP/HTTPS via the matching scheme.
4
5use anyhow::{anyhow, Context, Result};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::proxy::{ParsedProxy, ProxyScheme};
10
11#[derive(Clone, Debug, Default, Serialize, Deserialize)]
12pub struct GeoInfo {
13    pub ip: String,
14    pub country: String,
15    /// ISO-3166 alpha-2.
16    pub country_code: String,
17    pub region: String,
18    pub city: String,
19    pub isp: String,
20    /// IANA timezone.
21    pub timezone: String,
22    pub latitude: f64,
23    pub longitude: f64,
24    pub provider: String,
25}
26
27fn url_for(provider: &str) -> &'static str {
28    match provider {
29        "ipapi.co" => "https://ipapi.co/json/",
30        "ipwho.is" => "https://ipwho.is/",
31        _ => "http://ip-api.com/json/?fields=status,message,query,country,countryCode,regionName,city,isp,timezone,lat,lon",
32    }
33}
34
35/// Probe the geo `proxy` exits at, or direct geo when `proxy` is `None`.
36pub async fn geo_check_via(proxy: Option<&ParsedProxy>, provider: &str) -> Result<GeoInfo> {
37    let provider = if provider.is_empty() { "ip-api.com" } else { provider };
38    let url = url_for(provider);
39
40    let mut builder = reqwest::Client::builder().timeout(std::time::Duration::from_secs(8));
41    if let Some(p) = proxy {
42        let scheme = match p.scheme {
43            ProxyScheme::Socks5 => "socks5h", // DNS via proxy
44            ProxyScheme::Http => "http",
45            ProxyScheme::Https => "https",
46        };
47        let proxy_url = if p.username.is_empty() && p.password.is_empty() {
48            format!("{scheme}://{}:{}", p.host, p.port)
49        } else {
50            let enc = |s: &str| url::form_urlencoded::byte_serialize(s.as_bytes()).collect::<String>();
51            format!(
52                "{scheme}://{}:{}@{}:{}",
53                enc(&p.username),
54                enc(&p.password),
55                p.host,
56                p.port
57            )
58        };
59        builder = builder.proxy(reqwest::Proxy::all(&proxy_url).context("bad proxy URL")?);
60    } else {
61        builder = builder.no_proxy();
62    }
63
64    let body: Value = builder.build()?.get(url).send().await?.json().await?;
65
66    let s = |k: &str| body.get(k).and_then(|v| v.as_str()).unwrap_or("").to_string();
67    let f = |k: &str| body.get(k).and_then(|v| v.as_f64()).unwrap_or(0.0);
68
69    Ok(match provider {
70        "ip-api.com" => {
71            if s("status") == "fail" {
72                return Err(anyhow!("ip-api.com: {}", s("message")));
73            }
74            GeoInfo {
75                ip: s("query"),
76                country: s("country"),
77                country_code: s("countryCode"),
78                region: s("regionName"),
79                city: s("city"),
80                isp: s("isp"),
81                timezone: s("timezone"),
82                latitude: f("lat"),
83                longitude: f("lon"),
84                provider: provider.into(),
85            }
86        }
87        "ipapi.co" => GeoInfo {
88            ip: s("ip"),
89            country: s("country_name"),
90            country_code: s("country_code"),
91            region: s("region"),
92            city: s("city"),
93            isp: s("org"),
94            timezone: s("timezone"),
95            latitude: f("latitude"),
96            longitude: f("longitude"),
97            provider: provider.into(),
98        },
99        "ipwho.is" => GeoInfo {
100            ip: s("ip"),
101            country: s("country"),
102            country_code: s("country_code"),
103            region: s("region"),
104            city: s("city"),
105            isp: body
106                .get("connection")
107                .and_then(|c| c.get("isp"))
108                .and_then(|v| v.as_str())
109                .unwrap_or("")
110                .to_string(),
111            timezone: body
112                .get("timezone")
113                .and_then(|t| t.get("id"))
114                .and_then(|v| v.as_str())
115                .unwrap_or("")
116                .to_string(),
117            latitude: f("latitude"),
118            longitude: f("longitude"),
119            provider: provider.into(),
120        },
121        _ => GeoInfo {
122            ip: s("query"),
123            country: s("country"),
124            country_code: s("countryCode"),
125            provider: provider.into(),
126            ..Default::default()
127        },
128    })
129}