use std::time::Duration;
use serde::de::DeserializeOwned;
use crate::error::{api_error_message, Error};
use crate::models::{AsnBrief, GeoInfo, IpInfo, ThreatMatch};
pub const DEFAULT_BASE_URL: &str = "https://ipdata.info";
pub const PRO_BASE_URL: &str = "https://pro.ipdata.info";
const VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
pub struct ClientBuilder {
api_key: Option<String>,
base_url: Option<String>,
timeout: Duration,
}
impl ClientBuilder {
fn new() -> Self {
Self {
api_key: None,
base_url: None,
timeout: DEFAULT_TIMEOUT,
}
}
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.api_key = Some(key.into());
self
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn build(self) -> Client {
let base_url = self.base_url.unwrap_or_else(|| {
if self.api_key.is_some() {
PRO_BASE_URL.to_string()
} else {
DEFAULT_BASE_URL.to_string()
}
});
let agent = ureq::AgentBuilder::new()
.timeout(self.timeout)
.user_agent(&format!("ipdata-rust/{VERSION}"))
.build();
Client {
api_key: self.api_key,
base_url,
agent,
}
}
}
impl Default for ClientBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct Client {
api_key: Option<String>,
base_url: String,
agent: ureq::Agent,
}
impl Client {
pub fn new() -> Self {
ClientBuilder::new().build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn lookup(&self, ip: &str) -> Result<IpInfo, Error> {
self.get(&format!("/json/{}", encode_segment(ip)))
}
pub fn geo(&self, ip: &str) -> Result<GeoInfo, Error> {
self.get(&format!("/api/v1/{}/geo", encode_segment(ip)))
}
pub fn asn(&self, ip: &str) -> Result<AsnBrief, Error> {
self.get(&format!("/api/v1/{}/asn", encode_segment(ip)))
}
pub fn batch(&self, ips: &[&str]) -> Result<Vec<IpInfo>, Error> {
self.post("/api/v1/batch", ips)
}
pub fn asn_detail(&self, number: u32) -> Result<serde_json::Value, Error> {
self.get(&format!("/api/v1/asn/{number}"))
}
pub fn asn_whois_history(&self, number: u32) -> Result<serde_json::Value, Error> {
self.get(&format!("/api/v1/asn/{number}/whois-history"))
}
pub fn asn_changes(&self) -> Result<serde_json::Value, Error> {
self.get("/api/v1/asn-changes")
}
pub fn threat_domain(&self, domain: &str) -> Result<ThreatMatch, Error> {
self.get(&format!("/api/v1/threat/domain/{}", encode_segment(domain)))
}
pub fn threat_hash(&self, hash: &str) -> Result<ThreatMatch, Error> {
self.get(&format!("/api/v1/threat/hash/{}", encode_segment(hash)))
}
pub fn threat_url(&self, url: &str) -> Result<ThreatMatch, Error> {
let full = format!("{}{}", self.base_url, "/api/v1/threat/url");
let mut req = self.agent.get(&full).query("u", url);
if let Some(key) = &self.api_key {
req = req.set("X-Api-Key", key);
}
self.send(req)
}
fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
let url = format!("{}{}", self.base_url, path);
let mut req = self.agent.get(&url);
if let Some(key) = &self.api_key {
req = req.set("X-Api-Key", key);
}
self.send(req)
}
fn post<T: DeserializeOwned>(&self, path: &str, body: &[&str]) -> Result<T, Error> {
let url = format!("{}{}", self.base_url, path);
let mut req = self.agent.post(&url);
if let Some(key) = &self.api_key {
req = req.set("X-Api-Key", key);
}
match req.send_json(serde_json::json!(body)) {
Ok(resp) => decode(resp),
Err(e) => Err(map_ureq_error(e)),
}
}
fn send<T: DeserializeOwned>(&self, req: ureq::Request) -> Result<T, Error> {
match req.call() {
Ok(resp) => decode(resp),
Err(e) => Err(map_ureq_error(e)),
}
}
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
fn decode<T: DeserializeOwned>(resp: ureq::Response) -> Result<T, Error> {
resp.into_json::<T>()
.map_err(|e| Error::Decode(e.to_string()))
}
fn map_ureq_error(e: ureq::Error) -> Error {
match e {
ureq::Error::Status(status, resp) => {
let body = resp.into_string().unwrap_or_default();
Error::Api {
status,
message: api_error_message(&body),
}
}
ureq::Error::Transport(t) => Error::Transport(t.to_string()),
}
}
fn encode_segment(segment: &str) -> String {
let mut out = String::with_capacity(segment.len());
for byte in segment.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'.' | b':' | b'-' | b'_' => {
out.push(byte as char)
}
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}