use std::fmt::{Display, Formatter, Write};
use std::time::Duration;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[cfg(feature = "tokio")]
use tokio::time::sleep;
use tracing::{debug, error, warn};
#[cfg(feature = "tokio")]
use crate::Scheduler;
use crate::error::DehashedError;
use crate::res::{Entry, Response};
const URL: &str = "https://api.dehashed.com/v2/search";
const RESERVED: [char; 21] = [
'+', '-', '=', '&', '|', '>', '<', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?',
':', '\\',
];
fn escape(q: &str) -> String {
let mut s = String::new();
for c in q.chars() {
if RESERVED.contains(&c) {
s.write_str(&format!("\\{c}")).unwrap();
} else {
s.write_char(c).unwrap();
}
}
s
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum SearchType {
Simple(String),
Exact(String),
Regex(String),
Or(Vec<SearchType>),
And(Vec<SearchType>),
}
impl Display for SearchType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
SearchType::Simple(x) => x.clone(),
SearchType::Exact(x) => format!("\"{}\"", escape(x)),
SearchType::Regex(x) => format!("/{}/", escape(x)),
SearchType::Or(x) => x
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(" OR "),
SearchType::And(x) => x
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(" "),
}
)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Query {
Email(SearchType),
IpAddress(SearchType),
Username(SearchType),
Password(SearchType),
HashedPassword(SearchType),
Name(SearchType),
Domain(SearchType),
Vin(SearchType),
Phone(SearchType),
Address(SearchType),
}
impl Display for Query {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Query::Email(x) => format!("email:{x}"),
Query::IpAddress(x) => format!("ip_address:{x}"),
Query::Username(x) => format!("username:{x}"),
Query::Password(x) => format!("password:{x}"),
Query::HashedPassword(x) => format!("hashed_password:{x}"),
Query::Name(x) => format!("name:{x}"),
Query::Domain(x) => format!("domain:{x}"),
Query::Vin(x) => format!("vin:{x}"),
Query::Phone(x) => format!("phone:{x}"),
Query::Address(x) => format!("address:{x}"),
}
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SearchResult {
pub entries: Vec<Entry>,
pub balance: usize,
}
#[derive(Clone, Debug)]
pub struct DehashedApi {
client: Client,
}
impl DehashedApi {
pub fn new(api_key: String) -> Result<Self, DehashedError> {
let mut header_map = HeaderMap::new();
header_map.insert("Accept", HeaderValue::from_static("application/json"));
header_map.insert("Dehashed-Api-Key", HeaderValue::from_str(&api_key)?);
let client = Client::builder()
.timeout(Duration::from_secs(10))
.https_only(true)
.default_headers(header_map)
.build()?;
Ok(Self { client })
}
async fn raw_req(
&self,
size: usize,
page: usize,
query: String,
) -> Result<Response, DehashedError> {
let res = self
.client
.post(URL)
.json(&json!({"query": query, "size": size, "page": page}))
.send()
.await?;
let status = res.status();
let raw = res.text().await?;
debug!("status code: {status}. Raw: {raw}");
if status == StatusCode::from_u16(302).unwrap() {
Err(DehashedError::InvalidQuery)
} else if status == StatusCode::from_u16(400).unwrap() {
Err(DehashedError::Unknown(raw))
} else if status == StatusCode::from_u16(401).unwrap() {
Err(DehashedError::Unauthorized)
} else if status == StatusCode::from_u16(200).unwrap() {
match serde_json::from_str(&raw) {
Ok(result) => Ok(result),
Err(err) => {
error!("Error deserializing data: {err}. Raw data: {raw}");
Err(DehashedError::Unknown(raw))
}
}
} else {
warn!("Invalid response, status code: {status}. Raw: {raw}");
Err(DehashedError::Unknown(raw))
}
}
pub async fn search(&self, query: Query) -> Result<SearchResult, DehashedError> {
let q = query.to_string();
debug!("Query: {q}");
let mut search_result = SearchResult {
entries: vec![],
balance: 0,
};
for page in 1.. {
let res = self.raw_req(10_000, page, q.clone()).await?;
if let Some(entries) = res.entries {
for entry in entries {
search_result.entries.push(entry)
}
}
search_result.balance = res.balance;
if res.total < page * 10_000 {
break;
}
#[cfg(feature = "tokio")]
sleep(Duration::from_millis(200)).await;
}
Ok(search_result)
}
#[cfg(feature = "tokio")]
pub fn start_scheduler(&self) -> Scheduler {
Scheduler::new(self)
}
}