Skip to main content

ip_api_io/
blocking.rs

1//! Blocking (synchronous) client, enabled with the `blocking` cargo feature.
2
3use std::time::Duration;
4
5use serde::de::DeserializeOwned;
6use serde_json::json;
7
8use crate::error::{classify, extract_message, Error};
9use crate::models::*;
10use crate::{check_batch, encode_segment, DEFAULT_BASE_URL, USER_AGENT};
11
12/// Blocking client for the ip-api.io API.
13///
14/// ```no_run
15/// # fn run() -> Result<(), ip_api_io::Error> {
16/// let client = ip_api_io::blocking::Client::with_api_key("YOUR_API_KEY");
17/// let info = client.lookup_ip("8.8.8.8")?;
18/// println!("{:?}", info.location.country);
19/// # Ok(())
20/// # }
21/// ```
22#[derive(Debug, Clone)]
23pub struct Client {
24    api_key: Option<String>,
25    base_url: String,
26    http: reqwest::blocking::Client,
27}
28
29/// Builder for the blocking [`Client`].
30#[derive(Debug, Default)]
31pub struct ClientBuilder {
32    api_key: Option<String>,
33    base_url: Option<String>,
34    timeout: Option<Duration>,
35}
36
37impl ClientBuilder {
38    pub fn api_key(mut self, key: impl Into<String>) -> Self {
39        self.api_key = Some(key.into());
40        self
41    }
42
43    pub fn base_url(mut self, base: impl Into<String>) -> Self {
44        self.base_url = Some(base.into());
45        self
46    }
47
48    pub fn timeout(mut self, timeout: Duration) -> Self {
49        self.timeout = Some(timeout);
50        self
51    }
52
53    pub fn build(self) -> Client {
54        let http = reqwest::blocking::Client::builder()
55            .timeout(self.timeout.unwrap_or(Duration::from_secs(10)))
56            .build()
57            .expect("failed to build reqwest client");
58        Client {
59            api_key: self.api_key,
60            base_url: self
61                .base_url
62                .unwrap_or_else(|| DEFAULT_BASE_URL.to_string())
63                .trim_end_matches('/')
64                .to_string(),
65            http,
66        }
67    }
68}
69
70impl Default for Client {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76impl Client {
77    pub fn new() -> Self {
78        Self::builder().build()
79    }
80
81    pub fn with_api_key(key: impl Into<String>) -> Self {
82        Self::builder().api_key(key).build()
83    }
84
85    pub fn builder() -> ClientBuilder {
86        ClientBuilder::default()
87    }
88
89    pub fn lookup(&self) -> Result<IpInfo, Error> {
90        self.get("/api/v1/ip".into())
91    }
92
93    pub fn lookup_ip(&self, ip: &str) -> Result<IpInfo, Error> {
94        self.get(format!("/api/v1/ip/{}", encode_segment(ip)))
95    }
96
97    pub fn lookup_batch(&self, ips: &[&str]) -> Result<BatchIpLookupResponse, Error> {
98        check_batch(ips, "ips")?;
99        self.post("/api/v1/ip/batch".into(), json!({ "ips": ips }))
100    }
101
102    pub fn ip_reputation(&self, ip: &str) -> Result<serde_json::Value, Error> {
103        self.get(format!("/api/v1/ip-reputation/{}", encode_segment(ip)))
104    }
105
106    pub fn tor_check(&self, ip: &str) -> Result<TorDetection, Error> {
107        self.get(format!("/api/v1/tor/{}", encode_segment(ip)))
108    }
109
110    pub fn asn(&self, ip: &str) -> Result<AsnLookup, Error> {
111        self.get(format!("/api/v1/asn/{}", encode_segment(ip)))
112    }
113
114    pub fn email_info(&self, email: &str) -> Result<EmailInfo, Error> {
115        self.get(format!("/api/v1/email/{}", encode_segment(email)))
116    }
117
118    pub fn validate_email(&self, email: &str) -> Result<AdvancedEmailValidation, Error> {
119        self.get(format!("/api/v1/email/advanced/{}", encode_segment(email)))
120    }
121
122    pub fn validate_email_batch(
123        &self,
124        emails: &[&str],
125    ) -> Result<BatchEmailValidationResponse, Error> {
126        check_batch(emails, "emails")?;
127        self.post("/api/v1/email/advanced/batch".into(), json!({ "emails": emails }))
128    }
129
130    pub fn risk_score(&self) -> Result<RiskScore, Error> {
131        self.get("/api/v1/risk-score".into())
132    }
133
134    pub fn risk_score_ip(&self, ip: &str) -> Result<RiskScore, Error> {
135        self.get(format!("/api/v1/risk-score/{}", encode_segment(ip)))
136    }
137
138    pub fn email_risk_score(&self, email: &str) -> Result<RiskScore, Error> {
139        self.get(format!("/api/v1/risk-score/email/{}", encode_segment(email)))
140    }
141
142    pub fn whois(&self, domain: &str) -> Result<Whois, Error> {
143        self.get(format!("/api/v1/dns/whois/{}", encode_segment(domain)))
144    }
145
146    pub fn reverse_dns(&self, ip: &str) -> Result<ReverseDns, Error> {
147        self.get(format!("/api/v1/dns/reverse/{}", encode_segment(ip)))
148    }
149
150    pub fn forward_dns(&self, hostname: &str) -> Result<ForwardDns, Error> {
151        self.get(format!("/api/v1/dns/forward/{}", encode_segment(hostname)))
152    }
153
154    pub fn mx_records(&self, domain: &str) -> Result<MxLookup, Error> {
155        self.get(format!("/api/v1/dns/mx/{}", encode_segment(domain)))
156    }
157
158    pub fn domain_age(&self, domain: &str) -> Result<DomainAge, Error> {
159        self.get(format!("/api/v1/domain/age/{}", encode_segment(domain)))
160    }
161
162    pub fn domain_age_batch(&self, domains: &[&str]) -> Result<BatchDomainAgeResponse, Error> {
163        if domains.is_empty() {
164            return Err(Error::InvalidArgument("domains must not be empty".into()));
165        }
166        self.post("/api/v1/domain/age/batch".into(), json!({ "domains": domains }))
167    }
168
169    pub fn rate_limit(&self) -> Result<RateLimitInfo, Error> {
170        self.get("/api/v1/ratelimit".into())
171    }
172
173    pub fn usage_summary(&self) -> Result<UsageSummary, Error> {
174        self.get("/api/v1/usage/summary".into())
175    }
176
177    fn get<T: DeserializeOwned>(&self, path: String) -> Result<T, Error> {
178        self.request(reqwest::Method::GET, path, None)
179    }
180
181    fn post<T: DeserializeOwned>(
182        &self,
183        path: String,
184        body: serde_json::Value,
185    ) -> Result<T, Error> {
186        self.request(reqwest::Method::POST, path, Some(body))
187    }
188
189    fn request<T: DeserializeOwned>(
190        &self,
191        method: reqwest::Method,
192        path: String,
193        body: Option<serde_json::Value>,
194    ) -> Result<T, Error> {
195        let mut request = self
196            .http
197            .request(method, format!("{}{}", self.base_url, path))
198            .header(reqwest::header::USER_AGENT, USER_AGENT)
199            .header(reqwest::header::ACCEPT, "application/json");
200        if let Some(key) = &self.api_key {
201            request = request.query(&[("api_key", key)]);
202        }
203        if let Some(body) = body {
204            request = request.json(&body);
205        }
206
207        let response = request.send()?;
208        let status = response.status().as_u16();
209        if !response.status().is_success() {
210            let limit = header_i64(&response, "x-ratelimit-limit");
211            let remaining = header_i64(&response, "x-ratelimit-remaining");
212            let reset = header_i64(&response, "x-ratelimit-reset");
213            let body = response.text().unwrap_or_default();
214            let message = extract_message(status, &body);
215            if status == 429 {
216                return Err(Error::RateLimit {
217                    status,
218                    message,
219                    body,
220                    limit,
221                    remaining,
222                    reset,
223                });
224            }
225            return Err(classify(status, message, body));
226        }
227        Ok(response.json::<T>()?)
228    }
229}
230
231fn header_i64(response: &reqwest::blocking::Response, name: &str) -> Option<i64> {
232    response
233        .headers()
234        .get(name)
235        .and_then(|value| value.to_str().ok())
236        .and_then(|value| value.parse().ok())
237}