use std::borrow::Cow;
use std::collections::HashMap;
use std::net::IpAddr;
use std::time::Duration;
use chrono::prelude::{DateTime, NaiveDateTime, Utc};
use jsonapi::api::JsonApiError;
use reqwest::header::{self, HeaderMap};
use reqwest::{RequestBuilder, StatusCode, Url};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_urlencoded::to_string as serialize_url_query;
pub const DEFAULT_BASE_URL: &str = "https://api.abuseipdb.com/api/v2/";
pub const DEFAULT_USER_AGENT: &str = concat!("rust-abuseipdb-client/", env!("CARGO_PKG_VERSION"));
#[derive(Debug)]
pub enum Error {
InvalidBody,
InvalidHeaders,
Api(ApiError),
Client(reqwest::Error),
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Self::Client(err)
}
}
#[derive(Debug)]
pub struct ApiError {
pub rate_limit: RateLimit,
pub retry_after: Option<Duration>,
pub errors: Vec<JsonApiError>,
}
#[derive(Debug, Clone, Serialize_repr, Deserialize_repr, PartialEq)]
#[repr(u8)]
pub enum Category {
FraudOrder = 3,
DdosAttack = 4,
FtpBruteForce = 5,
PingOfDeath = 6,
Phishing = 7,
FraudVoip = 8,
OpenProxy = 9,
WebSpam = 10,
EmailSpam = 11,
BlogSpam = 12,
VpnIp = 13,
PortScan = 14,
Hacking = 15,
SqlInjection = 16,
Spoofing = 17,
BruteForceCredential = 18,
BadWebBot = 19,
ExploitedHost = 20,
WebAppAttack = 21,
SshAbuse = 22,
IotTargeted = 23,
}
trait Request {
type Response: DeserializeOwned;
fn into_builder(self, client: &Client) -> RequestBuilder;
}
#[derive(Debug)]
pub struct Response<T> {
pub data: T,
pub meta: Option<HashMap<String, Value>>,
pub rate_limit: RateLimit,
}
#[derive(Debug)]
pub struct RateLimit {
pub limit: u32,
pub remaining: u32,
pub reset_at: Option<DateTime<Utc>>,
}
impl RateLimit {
fn from_headers(header_map: &HeaderMap) -> Result<Self, Error> {
let limit = parse_i64_header(&header_map, "x-ratelimit-limit")?
.ok_or(Error::InvalidHeaders)? as u32;
let remaining = parse_i64_header(&header_map, "x-ratelimit-remaining")?
.ok_or(Error::InvalidHeaders)? as u32;
let reset_at = parse_i64_header(&header_map, "x-ratelimit-reset")?
.map(|ts| DateTime::from_utc(NaiveDateTime::from_timestamp(ts, 0), Utc));
Ok(RateLimit {
limit,
remaining,
reset_at,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AddressAbuse {
#[serde(rename = "ipAddress")]
pub ip_addr: IpAddr,
#[serde(rename = "abuseConfidenceScore")]
pub abuse_confidence_score: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Check {
#[serde(rename = "ipAddress")]
pub ip_addr: IpAddr,
#[serde(rename = "isPublic")]
pub is_public: bool,
#[serde(rename = "isWhitelisted")]
pub is_whitelisted: Option<bool>,
#[serde(rename = "abuseConfidenceScore")]
pub abuse_confidence_score: u32,
#[serde(rename = "countryCode")]
pub country_code: Option<String>,
#[serde(rename = "countryName")]
pub country_name: Option<String>,
#[serde(rename = "usageType")]
pub usage_type: String,
pub isp: String,
pub domain: Option<String>,
#[serde(rename = "totalReports")]
pub total_reports: u64,
#[serde(rename = "numDistinctUsers")]
pub num_distinct_users: u64,
#[serde(rename = "lastReportedAt")]
pub last_reported_at: DateTime<Utc>,
pub reports: Option<Vec<CheckReport>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CheckReport {
#[serde(rename = "reportedAt")]
pub reported_at: DateTime<Utc>,
pub comment: Option<String>,
pub categories: Vec<Category>,
#[serde(rename = "reporterId")]
pub reporter_id: u64,
#[serde(rename = "reporterCountryCode")]
pub reporter_country_code: String,
#[serde(rename = "reporterCountryName")]
pub reporter_country_name: String,
}
#[derive(Serialize)]
struct CheckRequest {
pub verbose: bool,
#[serde(rename = "ipAddress")]
pub ip_addr: IpAddr,
#[serde(rename = "maxAgeInDays")]
pub max_age_in_days: Option<u16>,
}
impl Request for CheckRequest {
type Response = Check;
fn into_builder(self, client: &Client) -> RequestBuilder {
client.inner.get(client.endpoint("check", self))
}
}
pub type Blacklist = Vec<AddressAbuse>;
#[derive(Serialize)]
struct BlacklistRequest {
#[serde(rename = "confidenceMinimum")]
pub confidence_min: Option<u8>,
pub limit: Option<u32>,
#[serde(rename = "self")]
pub for_self: bool,
}
impl Request for BlacklistRequest {
type Response = Blacklist;
fn into_builder(self, client: &Client) -> RequestBuilder {
client.inner.get(client.endpoint("blacklist", self))
}
}
#[derive(Serialize)]
struct ReportRequest<'a> {
#[serde(rename = "ip")]
pub ip_addr: IpAddr,
pub categories: &'a [Category],
pub comment: Option<&'a str>,
}
impl<'a> Request for ReportRequest<'a> {
type Response = AddressAbuse;
fn into_builder(self, client: &Client) -> RequestBuilder {
client.inner.post(client.endpoint("report", self))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BlockCheck {
#[serde(rename = "networkAddress")]
pub network_address: IpAddr,
pub netmask: IpAddr,
#[serde(rename = "minAddress")]
pub min_address: IpAddr,
#[serde(rename = "maxAddress")]
pub max_address: IpAddr,
#[serde(rename = "numPossibleHosts")]
pub num_possible_hosts: u64,
#[serde(rename = "addressSpaceDesc")]
pub address_space_desc: String,
#[serde(rename = "reportedAddress")]
pub reported_address: Vec<BlockCheckReport>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BlockCheckReport {
#[serde(rename = "ipAddress")]
pub ip_addr: IpAddr,
#[serde(rename = "numReports")]
pub num_reports: u64,
#[serde(rename = "mostRecentReport")]
pub most_recent_report: DateTime<Utc>,
#[serde(rename = "abuseConfidenceScore")]
pub abuse_confidence_score: u8,
#[serde(rename = "countryCode")]
pub country_code: Option<String>,
}
#[derive(Serialize)]
struct CheckBlockRequest<'a> {
pub network: &'a str,
#[serde(rename = "maxAgeInDays")]
pub max_age_in_days: Option<u16>,
}
impl<'a> Request for CheckBlockRequest<'a> {
type Response = BlockCheck;
fn into_builder(self, client: &Client) -> RequestBuilder {
client.inner.get(client.endpoint("check-block", self))
}
}
pub struct Client {
base_url: Url,
inner: reqwest::Client,
user_agent: Cow<'static, str>,
api_key: Option<Cow<'static, str>>,
}
impl Client {
pub fn new<S: Into<String>>(api_key: S) -> Self {
Self::builder().api_key(api_key.into()).build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
pub async fn check(
&self,
ip_addr: IpAddr,
max_age_in_days: Option<u16>,
verbose: bool,
) -> Result<Response<Check>, Error> {
self.request(CheckRequest {
ip_addr,
max_age_in_days,
verbose,
})
.await
}
pub async fn check_block(
&self,
network: &str,
max_age_in_days: Option<u16>,
) -> Result<Response<BlockCheck>, Error> {
self.request(CheckBlockRequest {
network,
max_age_in_days,
})
.await
}
pub async fn blacklist(
&self,
confidence_min: Option<u8>,
limit: Option<u32>,
for_self: bool,
) -> Result<Response<Blacklist>, Error> {
self.request(BlacklistRequest {
confidence_min,
limit,
for_self,
})
.await
}
pub async fn report(
&self,
ip_addr: IpAddr,
categories: &[Category],
comment: Option<&str>,
) -> Result<Response<AddressAbuse>, Error> {
self.request(ReportRequest {
ip_addr,
categories,
comment,
})
.await
}
async fn request<R>(&self, req: R) -> Result<Response<R::Response>, Error>
where
R: Request,
{
self.do_request(req.into_builder(&self)).await
}
async fn do_request<T: DeserializeOwned>(
&self,
req: RequestBuilder,
) -> Result<Response<T>, Error> {
#[derive(Deserialize)]
struct JsonApiDocument<D> {
data: Option<D>,
meta: Option<HashMap<String, Value>>,
errors: Option<Vec<JsonApiError>>,
}
let req = match self.api_key {
Some(ref api_key) => req.header("Key", api_key.as_ref()),
None => req,
};
let res = req
.header(header::ACCEPT, "application/json")
.header(header::USER_AGENT, self.user_agent.as_ref())
.send()
.await?;
let res_status = res.status();
let rate_limit = RateLimit::from_headers(res.headers())?;
let retry_after_opt = parse_i64_header(res.headers(), header::RETRY_AFTER.as_str())?;
let JsonApiDocument { errors, data, meta } = res.json().await?;
match (data, errors) {
(Some(data), None) => Ok(Response {
meta,
data,
rate_limit,
}),
(None, Some(errors)) => {
let retry_after = match res_status {
StatusCode::TOO_MANY_REQUESTS => {
let retry_after_secs = retry_after_opt.ok_or(Error::InvalidHeaders)?;
Some(Duration::from_secs(retry_after_secs as u64))
}
_ => None,
};
let err = ApiError {
errors,
rate_limit,
retry_after,
};
Err(Error::Api(err))
}
_ => Err(Error::InvalidBody),
}
}
fn endpoint<P: Serialize>(&self, endpoint: &str, params: P) -> Url {
let mut url = self.base_url.join(endpoint).unwrap();
let query = serialize_url_query(params).unwrap();
url.set_query(Some(query.as_str()));
url
}
}
#[derive(Debug, Clone)]
pub struct ClientBuilder {
inner: reqwest::Client,
api_key: Option<Cow<'static, str>>,
user_agent: Cow<'static, str>,
base_url: Url,
}
impl ClientBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_client(client: reqwest::Client) -> Self {
Self {
api_key: None,
inner: client,
user_agent: DEFAULT_USER_AGENT.into(),
base_url: Url::parse(DEFAULT_BASE_URL).unwrap(),
}
}
pub fn base_url<S>(mut self, base_url: Url) -> Self {
self.base_url = base_url.into();
self
}
pub fn user_agent<S>(mut self, user_agent: S) -> Self
where
S: Into<Cow<'static, str>>,
{
self.user_agent = user_agent.into();
self
}
pub fn api_key<S>(mut self, api_key: S) -> Self
where
S: Into<Cow<'static, str>>,
{
self.api_key = Some(api_key.into());
self
}
pub fn build(self) -> Client {
Client {
inner: self.inner,
api_key: self.api_key,
base_url: self.base_url,
user_agent: self.user_agent,
}
}
}
impl Default for ClientBuilder {
fn default() -> Self {
Self::with_client(reqwest::Client::new())
}
}
fn parse_i64_header(header_map: &HeaderMap, key: &str) -> Result<Option<i64>, Error> {
header_map
.get(key)
.map(|val| {
let s = val.to_str().map_err(|_| Error::InvalidHeaders)?;
i64::from_str_radix(s, 10).map_err(|_| Error::InvalidHeaders)
})
.transpose()
}