abuseipdb/
lib.rs

1//! AbuseIPDB API Client.
2//! 
3//! ```rust
4//! use abuseipdb::Client;
5//! use std::net::Ipv4Addr;
6//! 
7//! async fn example() {
8//!     let my_ip = Ipv4Addr::new(127, 0, 0, 1).into();
9//!     let client = Client::new("<API-KEY>");
10//!     let response = client.check(my_ip, None, false).await.unwrap();
11//!     println!("abuseConfidenceScore: {}", response.data.abuse_confidence_score);
12//! }
13//! ```
14
15use 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///////////////////////////////////////////////////////////////////////////////
35// Common
36
37/// Represents a failure while making a request to AbuseIPDB.
38#[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/// Represents an error returned by AbuseIPDB via the API.
53#[derive(Debug)]
54pub struct ApiError {
55    pub rate_limit: RateLimit,
56    pub retry_after: Option<Duration>,
57    pub errors: Vec<JsonApiError>,
58}
59
60/// AbuseIPDB attack categories.
61#[derive(Debug, Clone, Serialize_repr, Deserialize_repr, PartialEq)]
62#[repr(u8)]
63pub enum Category {
64    /// Fraudulent orders.
65    FraudOrder = 3,
66    /// Participating in distributed denial-of-service (usually part of botnet).
67    DdosAttack = 4,
68    /// FTP Brute-Force
69    FtpBruteForce = 5,
70    /// Oversized IP packet.
71    PingOfDeath = 6,
72    /// Phishing websites and/or email.
73    Phishing = 7,
74    /// Fraud VoIP
75    FraudVoip = 8,
76    /// Open proxy, open relay, or Tor exit node.
77    OpenProxy = 9,
78    /// Comment/forum spam, HTTP referer spam, or other CMS spam.
79    WebSpam = 10,
80    /// Spam email content, infected attachments, and phishing emails.
81    /// Note: Limit comments to only relevent information (instead of log dumps)
82    /// and be sure to remove PII if you want to remain anonymous.
83    EmailSpam = 11,
84    /// CMS blog comment spam.
85    BlogSpam = 12,
86    /// VPN IP - Conjunctive category.
87    VpnIp = 13,
88    /// Scanning for open ports and vulnerable services.
89    PortScan = 14,
90    /// Hacking
91    Hacking = 15,
92    /// Attempts at SQL injection.
93    SqlInjection = 16,
94    /// Email sender spoofing.
95    Spoofing = 17,
96    /// Brute-force attacks on webpage logins and services
97    /// like SSH, FTP, SIP, SMTP, RDP, etc.
98    /// This category is seperate from DDoS attacks.
99    BruteForceCredential = 18,
100    /// Webpage scraping (for email addresses, content, etc) and crawlers that
101    /// do not honor robots.txt. Excessive requests and user agent spoofing
102    /// can also be reported here.
103    BadWebBot = 19,
104    /// Host is likely infected with malware and being used for other
105    /// attacks or to host malicious content. The host owner may not be aware
106    /// of the compromise. This category is often used in combination with
107    /// other attack categories.
108    ExploitedHost = 20,
109    /// Attempts to probe for or exploit installed web applications such
110    /// as a CMS like WordPress/Drupal, e-commerce solutions, forum software,
111    /// phpMyAdmin and various other software plugins/solutions.
112    WebAppAttack = 21,
113    /// Secure Shell (SSH) abuse. Use this category in combination with more
114    /// specific categories.
115    SshAbuse = 22,
116    /// Abuse was targeted at an "Internet of Things" type device.
117    /// Include information about what type of device was targeted
118    /// in the comments.
119    IotTargeted = 23,
120}
121
122///////////////////////////////////////////////////////////////////////////////
123// Requests and Responses
124
125trait Request {
126    type Response: DeserializeOwned;
127
128    fn into_builder(self, client: &Client) -> RequestBuilder;
129}
130
131/// Wrapper type for successful API responses with rate limit
132/// and meta information.
133#[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/// The rate limit information returned from a request to AbuseIPDB.
141#[derive(Debug)]
142pub struct RateLimit {
143    /// Your daily limit.
144    pub limit: u32,
145    /// Remaining requests available for this endpoint.
146    pub remaining: u32,
147    /// The timestamp for the daily limit reset.
148    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///////////////////////////////////////////////////////////////////////////////
176// CHECK
177
178#[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    /// The `is_whitelisted` property reflects whether the IP is spotted in any AbuseDB whitelists.
185    ///
186    /// The whitelists give the benefit of the doubt to many IPs, so it generally should not be
187    /// used as a basis for action. The `abuse_confidence_score` is a better basis for action,
188    /// because it is nonbinary and allows for nuance. The `is_whitelisted` property may be null
189    /// if a whitelist lookup was not performed.
190    #[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// Verbose flag if `false`, will exclude reports and the country name field.
226#[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
243///////////////////////////////////////////////////////////////////////////////
244// BLACKLIST
245
246pub 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///////////////////////////////////////////////////////////////////////////////
266// REPORT
267
268#[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///////////////////////////////////////////////////////////////////////////////
285// CHECK-BLOCK
286
287#[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
333///////////////////////////////////////////////////////////////////////////////
334// Client
335
336/// The AbuseIPDB client.
337pub 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    /// Create a new AbuseIPDB client with the given API key and 
346    /// default settings.
347    pub fn new<S: Into<String>>(api_key: S) -> Self {
348        Self::builder().api_key(api_key.into()).build()
349    }
350
351    /// Returns a AbuseIPDB client builder.
352    pub fn builder() -> ClientBuilder {
353        ClientBuilder::new()
354    }
355
356    /// Queries AbuseIPDB given a IP address.
357    ///
358    /// The check endpoint accepts a single IP address (v4 or v6). 
359    /// Optionally you may set the `max_age_in_days` parameter to only 
360    /// return reports within the last x amount of days.
361    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    /// Queries AbuseIPDB given a subnet.
376    ///
377    /// The check-block endpoint accepts a subnet (v4 or v6) 
378    /// denoted with CIDR notation.
379    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    /// Queries AbuseIPDB for the blacklist.
392    ///
393    /// The blacklist is the culmination of all of the valiant reporting 
394    /// by AbuseIPDB users. It's a list of the most reported IP addresses.
395    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    /// Reports an IP address to AbuseIPDB.
410    ///
411    /// At least one category is required.
412    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        // Add the API key to the request if set.
445        let req = match self.api_key {
446            Some(ref api_key) => req.header("Key", api_key.as_ref()),
447            None => req,
448        };
449        // Set the request user agent and set accept to json mime
450        // and then send.
451        let res = req
452            .header(header::ACCEPT, "application/json")
453            .header(header::USER_AGENT, self.user_agent.as_ref())
454            .send()
455            .await?;
456        // Extract the rate limit information from the response headers.
457        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        // Deserialize the JSON document body.
461        let JsonApiDocument { errors, data, meta } = res.json().await?;
462        // Handle the parsed JSON response.
463        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    ///////////////////////////////////////////////////////////////////////////////
489    // Helpers
490
491    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///////////////////////////////////////////////////////////////////////////////
500// Client Builder
501
502/// A builder to create a configured AbuseIPDB client.
503#[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    /// Create the client builder with the default HTTP client.
513    pub fn new() -> Self {
514        Self::default()
515    }
516
517    /// Creates the client builder, given a pre-existing HTTP client.
518    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    /// Sets the base URL for the AbuseIPDB API.
528    pub fn base_url<S>(mut self, base_url: Url) -> Self {
529        self.base_url = base_url.into();
530        self
531    }
532
533    /// Set the user agent the client will use when making a
534    /// request to AbuseIPDB.
535    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    /// Sets the API key to be used to authenticate when 
544    /// making a request AbuseIPDB.
545    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    /// Consumes the client builder and returns the built
554    /// AbuseIPDB client.
555    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
571///////////////////////////////////////////////////////////////////////////////
572// Helpers
573
574fn 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}