1use 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 pub country_code: String,
17 pub region: String,
18 pub city: String,
19 pub isp: String,
20 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
35pub 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", 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}