1use std::borrow::Cow;
16use std::collections::HashMap;
17use std::net::IpAddr;
18use std::time::Duration;
19
20use chrono::prelude::{DateTime, NaiveDateTime, Utc};
21use jsonapi::api::JsonApiError;
22use reqwest::header::{self, HeaderMap};
23use reqwest::{RequestBuilder, StatusCode, Url};
24
25use serde::de::DeserializeOwned;
26use serde::{Deserialize, Serialize};
27use serde_json::Value;
28use serde_repr::{Deserialize_repr, Serialize_repr};
29use serde_urlencoded::to_string as serialize_url_query;
30
31pub const DEFAULT_BASE_URL: &str = "https://api.abuseipdb.com/api/v2/";
32pub const DEFAULT_USER_AGENT: &str = concat!("rust-abuseipdb-client/", env!("CARGO_PKG_VERSION"));
33
34#[derive(Debug)]
39pub enum Error {
40 InvalidBody,
41 InvalidHeaders,
42 Api(ApiError),
43 Client(reqwest::Error),
44}
45
46impl From<reqwest::Error> for Error {
47 fn from(err: reqwest::Error) -> Self {
48 Self::Client(err)
49 }
50}
51
52#[derive(Debug)]
54pub struct ApiError {
55 pub rate_limit: RateLimit,
56 pub retry_after: Option<Duration>,
57 pub errors: Vec<JsonApiError>,
58}
59
60#[derive(Debug, Clone, Serialize_repr, Deserialize_repr, PartialEq)]
62#[repr(u8)]
63pub enum Category {
64 FraudOrder = 3,
66 DdosAttack = 4,
68 FtpBruteForce = 5,
70 PingOfDeath = 6,
72 Phishing = 7,
74 FraudVoip = 8,
76 OpenProxy = 9,
78 WebSpam = 10,
80 EmailSpam = 11,
84 BlogSpam = 12,
86 VpnIp = 13,
88 PortScan = 14,
90 Hacking = 15,
92 SqlInjection = 16,
94 Spoofing = 17,
96 BruteForceCredential = 18,
100 BadWebBot = 19,
104 ExploitedHost = 20,
109 WebAppAttack = 21,
113 SshAbuse = 22,
116 IotTargeted = 23,
120}
121
122trait Request {
126 type Response: DeserializeOwned;
127
128 fn into_builder(self, client: &Client) -> RequestBuilder;
129}
130
131#[derive(Debug)]
134pub struct Response<T> {
135 pub data: T,
136 pub meta: Option<HashMap<String, Value>>,
137 pub rate_limit: RateLimit,
138}
139
140#[derive(Debug)]
142pub struct RateLimit {
143 pub limit: u32,
145 pub remaining: u32,
147 pub reset_at: Option<DateTime<Utc>>,
149}
150
151impl RateLimit {
152 fn from_headers(header_map: &HeaderMap) -> Result<Self, Error> {
153 let limit = parse_i64_header(&header_map, "x-ratelimit-limit")?
154 .ok_or(Error::InvalidHeaders)? as u32;
155 let remaining = parse_i64_header(&header_map, "x-ratelimit-remaining")?
156 .ok_or(Error::InvalidHeaders)? as u32;
157 let reset_at = parse_i64_header(&header_map, "x-ratelimit-reset")?
158 .map(|ts| DateTime::from_utc(NaiveDateTime::from_timestamp(ts, 0), Utc));
159 Ok(RateLimit {
160 limit,
161 remaining,
162 reset_at,
163 })
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
168pub struct AddressAbuse {
169 #[serde(rename = "ipAddress")]
170 pub ip_addr: IpAddr,
171 #[serde(rename = "abuseConfidenceScore")]
172 pub abuse_confidence_score: u8,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
179pub struct Check {
180 #[serde(rename = "ipAddress")]
181 pub ip_addr: IpAddr,
182 #[serde(rename = "isPublic")]
183 pub is_public: bool,
184 #[serde(rename = "isWhitelisted")]
191 pub is_whitelisted: Option<bool>,
192 #[serde(rename = "abuseConfidenceScore")]
193 pub abuse_confidence_score: u32,
194 #[serde(rename = "countryCode")]
195 pub country_code: Option<String>,
196 #[serde(rename = "countryName")]
197 pub country_name: Option<String>,
198 #[serde(rename = "usageType")]
199 pub usage_type: String,
200 pub isp: String,
201 pub domain: Option<String>,
202 #[serde(rename = "totalReports")]
203 pub total_reports: u64,
204 #[serde(rename = "numDistinctUsers")]
205 pub num_distinct_users: u64,
206 #[serde(rename = "lastReportedAt")]
207 pub last_reported_at: DateTime<Utc>,
208 pub reports: Option<Vec<CheckReport>>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
212pub struct CheckReport {
213 #[serde(rename = "reportedAt")]
214 pub reported_at: DateTime<Utc>,
215 pub comment: Option<String>,
216 pub categories: Vec<Category>,
217 #[serde(rename = "reporterId")]
218 pub reporter_id: u64,
219 #[serde(rename = "reporterCountryCode")]
220 pub reporter_country_code: String,
221 #[serde(rename = "reporterCountryName")]
222 pub reporter_country_name: String,
223}
224
225#[derive(Serialize)]
227struct CheckRequest {
228 pub verbose: bool,
229 #[serde(rename = "ipAddress")]
230 pub ip_addr: IpAddr,
231 #[serde(rename = "maxAgeInDays")]
232 pub max_age_in_days: Option<u16>,
233}
234
235impl Request for CheckRequest {
236 type Response = Check;
237
238 fn into_builder(self, client: &Client) -> RequestBuilder {
239 client.inner.get(client.endpoint("check", self))
240 }
241}
242
243pub type Blacklist = Vec<AddressAbuse>;
247
248#[derive(Serialize)]
249struct BlacklistRequest {
250 #[serde(rename = "confidenceMinimum")]
251 pub confidence_min: Option<u8>,
252 pub limit: Option<u32>,
253 #[serde(rename = "self")]
254 pub for_self: bool,
255}
256
257impl Request for BlacklistRequest {
258 type Response = Blacklist;
259
260 fn into_builder(self, client: &Client) -> RequestBuilder {
261 client.inner.get(client.endpoint("blacklist", self))
262 }
263}
264
265#[derive(Serialize)]
269struct ReportRequest<'a> {
270 #[serde(rename = "ip")]
271 pub ip_addr: IpAddr,
272 pub categories: &'a [Category],
273 pub comment: Option<&'a str>,
274}
275
276impl<'a> Request for ReportRequest<'a> {
277 type Response = AddressAbuse;
278
279 fn into_builder(self, client: &Client) -> RequestBuilder {
280 client.inner.post(client.endpoint("report", self))
281 }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
288pub struct BlockCheck {
289 #[serde(rename = "networkAddress")]
290 pub network_address: IpAddr,
291 pub netmask: IpAddr,
292 #[serde(rename = "minAddress")]
293 pub min_address: IpAddr,
294 #[serde(rename = "maxAddress")]
295 pub max_address: IpAddr,
296 #[serde(rename = "numPossibleHosts")]
297 pub num_possible_hosts: u64,
298 #[serde(rename = "addressSpaceDesc")]
299 pub address_space_desc: String,
300 #[serde(rename = "reportedAddress")]
301 pub reported_address: Vec<BlockCheckReport>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
305pub struct BlockCheckReport {
306 #[serde(rename = "ipAddress")]
307 pub ip_addr: IpAddr,
308 #[serde(rename = "numReports")]
309 pub num_reports: u64,
310 #[serde(rename = "mostRecentReport")]
311 pub most_recent_report: DateTime<Utc>,
312 #[serde(rename = "abuseConfidenceScore")]
313 pub abuse_confidence_score: u8,
314 #[serde(rename = "countryCode")]
315 pub country_code: Option<String>,
316}
317
318#[derive(Serialize)]
319struct CheckBlockRequest<'a> {
320 pub network: &'a str,
321 #[serde(rename = "maxAgeInDays")]
322 pub max_age_in_days: Option<u16>,
323}
324
325impl<'a> Request for CheckBlockRequest<'a> {
326 type Response = BlockCheck;
327
328 fn into_builder(self, client: &Client) -> RequestBuilder {
329 client.inner.get(client.endpoint("check-block", self))
330 }
331}
332
333pub struct Client {
338 base_url: Url,
339 inner: reqwest::Client,
340 user_agent: Cow<'static, str>,
341 api_key: Option<Cow<'static, str>>,
342}
343
344impl Client {
345 pub fn new<S: Into<String>>(api_key: S) -> Self {
348 Self::builder().api_key(api_key.into()).build()
349 }
350
351 pub fn builder() -> ClientBuilder {
353 ClientBuilder::new()
354 }
355
356 pub async fn check(
362 &self,
363 ip_addr: IpAddr,
364 max_age_in_days: Option<u16>,
365 verbose: bool,
366 ) -> Result<Response<Check>, Error> {
367 self.request(CheckRequest {
368 ip_addr,
369 max_age_in_days,
370 verbose,
371 })
372 .await
373 }
374
375 pub async fn check_block(
380 &self,
381 network: &str,
382 max_age_in_days: Option<u16>,
383 ) -> Result<Response<BlockCheck>, Error> {
384 self.request(CheckBlockRequest {
385 network,
386 max_age_in_days,
387 })
388 .await
389 }
390
391 pub async fn blacklist(
396 &self,
397 confidence_min: Option<u8>,
398 limit: Option<u32>,
399 for_self: bool,
400 ) -> Result<Response<Blacklist>, Error> {
401 self.request(BlacklistRequest {
402 confidence_min,
403 limit,
404 for_self,
405 })
406 .await
407 }
408
409 pub async fn report(
413 &self,
414 ip_addr: IpAddr,
415 categories: &[Category],
416 comment: Option<&str>,
417 ) -> Result<Response<AddressAbuse>, Error> {
418 self.request(ReportRequest {
419 ip_addr,
420 categories,
421 comment,
422 })
423 .await
424 }
425
426
427 async fn request<R>(&self, req: R) -> Result<Response<R::Response>, Error>
428 where
429 R: Request,
430 {
431 self.do_request(req.into_builder(&self)).await
432 }
433
434 async fn do_request<T: DeserializeOwned>(
435 &self,
436 req: RequestBuilder,
437 ) -> Result<Response<T>, Error> {
438 #[derive(Deserialize)]
439 struct JsonApiDocument<D> {
440 data: Option<D>,
441 meta: Option<HashMap<String, Value>>,
442 errors: Option<Vec<JsonApiError>>,
443 }
444 let req = match self.api_key {
446 Some(ref api_key) => req.header("Key", api_key.as_ref()),
447 None => req,
448 };
449 let res = req
452 .header(header::ACCEPT, "application/json")
453 .header(header::USER_AGENT, self.user_agent.as_ref())
454 .send()
455 .await?;
456 let res_status = res.status();
458 let rate_limit = RateLimit::from_headers(res.headers())?;
459 let retry_after_opt = parse_i64_header(res.headers(), header::RETRY_AFTER.as_str())?;
460 let JsonApiDocument { errors, data, meta } = res.json().await?;
462 match (data, errors) {
464 (Some(data), None) => Ok(Response {
465 meta,
466 data,
467 rate_limit,
468 }),
469 (None, Some(errors)) => {
470 let retry_after = match res_status {
471 StatusCode::TOO_MANY_REQUESTS => {
472 let retry_after_secs = retry_after_opt.ok_or(Error::InvalidHeaders)?;
473 Some(Duration::from_secs(retry_after_secs as u64))
474 }
475 _ => None,
476 };
477 let err = ApiError {
478 errors,
479 rate_limit,
480 retry_after,
481 };
482 Err(Error::Api(err))
483 }
484 _ => Err(Error::InvalidBody),
485 }
486 }
487
488 fn endpoint<P: Serialize>(&self, endpoint: &str, params: P) -> Url {
492 let mut url = self.base_url.join(endpoint).unwrap();
493 let query = serialize_url_query(params).unwrap();
494 url.set_query(Some(query.as_str()));
495 url
496 }
497}
498
499#[derive(Debug, Clone)]
504pub struct ClientBuilder {
505 inner: reqwest::Client,
506 api_key: Option<Cow<'static, str>>,
507 user_agent: Cow<'static, str>,
508 base_url: Url,
509}
510
511impl ClientBuilder {
512 pub fn new() -> Self {
514 Self::default()
515 }
516
517 pub fn with_client(client: reqwest::Client) -> Self {
519 Self {
520 api_key: None,
521 inner: client,
522 user_agent: DEFAULT_USER_AGENT.into(),
523 base_url: Url::parse(DEFAULT_BASE_URL).unwrap(),
524 }
525 }
526
527 pub fn base_url<S>(mut self, base_url: Url) -> Self {
529 self.base_url = base_url.into();
530 self
531 }
532
533 pub fn user_agent<S>(mut self, user_agent: S) -> Self
536 where
537 S: Into<Cow<'static, str>>,
538 {
539 self.user_agent = user_agent.into();
540 self
541 }
542
543 pub fn api_key<S>(mut self, api_key: S) -> Self
546 where
547 S: Into<Cow<'static, str>>,
548 {
549 self.api_key = Some(api_key.into());
550 self
551 }
552
553 pub fn build(self) -> Client {
556 Client {
557 inner: self.inner,
558 api_key: self.api_key,
559 base_url: self.base_url,
560 user_agent: self.user_agent,
561 }
562 }
563}
564
565impl Default for ClientBuilder {
566 fn default() -> Self {
567 Self::with_client(reqwest::Client::new())
568 }
569}
570
571fn parse_i64_header(header_map: &HeaderMap, key: &str) -> Result<Option<i64>, Error> {
575 header_map
576 .get(key)
577 .map(|val| {
578 let s = val.to_str().map_err(|_| Error::InvalidHeaders)?;
579 i64::from_str_radix(s, 10).map_err(|_| Error::InvalidHeaders)
580 })
581 .transpose()
582}