1use std::fmt::{Display, Formatter, Write};
2use std::time::Duration;
3
4use reqwest::header::{HeaderMap, HeaderValue};
5use reqwest::{Client, StatusCode};
6use serde::{Deserialize, Serialize};
7use serde_json::json;
8#[cfg(feature = "tokio")]
9use tokio::time::sleep;
10use tracing::{debug, error, warn};
11
12#[cfg(feature = "tokio")]
13use crate::Scheduler;
14use crate::error::DehashedError;
15use crate::res::{Entry, Response};
16
17const URL: &str = "https://api.dehashed.com/v2/search";
18const RESERVED: [char; 21] = [
19 '+', '-', '=', '&', '|', '>', '<', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?',
20 ':', '\\',
21];
22
23fn escape(q: &str) -> String {
24 let mut s = String::new();
25 for c in q.chars() {
26 if RESERVED.contains(&c) {
27 s.write_str(&format!("\\{c}")).unwrap();
28 } else {
29 s.write_char(c).unwrap();
30 }
31 }
32 s
33}
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
37#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
38#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
39pub enum SearchType {
40 Simple(String),
42 Exact(String),
44 Regex(String),
46 Or(Vec<SearchType>),
48 And(Vec<SearchType>),
50}
51
52impl Display for SearchType {
53 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54 write!(
55 f,
56 "{}",
57 match self {
58 SearchType::Simple(x) => x.clone(),
59 SearchType::Exact(x) => format!("\"{}\"", escape(x)),
60 SearchType::Regex(x) => format!("/{}/", escape(x)),
61 SearchType::Or(x) => x
62 .iter()
63 .map(|x| x.to_string())
64 .collect::<Vec<_>>()
65 .join(" OR "),
66 SearchType::And(x) => x
67 .iter()
68 .map(|x| x.to_string())
69 .collect::<Vec<_>>()
70 .join(" "),
71 }
72 )
73 }
74}
75
76#[derive(Clone, Debug, Serialize, Deserialize)]
78#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
79#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
80pub enum Query {
81 Email(SearchType),
83 IpAddress(SearchType),
85 Username(SearchType),
87 Password(SearchType),
89 HashedPassword(SearchType),
91 Name(SearchType),
93 Domain(SearchType),
95 Vin(SearchType),
97 Phone(SearchType),
99 Address(SearchType),
101}
102
103impl Display for Query {
104 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105 write!(
106 f,
107 "{}",
108 match self {
109 Query::Email(x) => format!("email:{x}"),
110 Query::IpAddress(x) => format!("ip_address:{x}"),
111 Query::Username(x) => format!("username:{x}"),
112 Query::Password(x) => format!("password:{x}"),
113 Query::HashedPassword(x) => format!("hashed_password:{x}"),
114 Query::Name(x) => format!("name:{x}"),
115 Query::Domain(x) => format!("domain:{x}"),
116 Query::Vin(x) => format!("vin:{x}"),
117 Query::Phone(x) => format!("phone:{x}"),
118 Query::Address(x) => format!("address:{x}"),
119 }
120 )
121 }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
127#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
128pub struct SearchResult {
129 pub entries: Vec<Entry>,
131 pub balance: usize,
133}
134
135#[derive(Clone, Debug)]
137pub struct DehashedApi {
138 client: Client,
139}
140
141impl DehashedApi {
142 pub fn new(api_key: String) -> Result<Self, DehashedError> {
150 let mut header_map = HeaderMap::new();
151 header_map.insert("Accept", HeaderValue::from_static("application/json"));
152 header_map.insert("Dehashed-Api-Key", HeaderValue::from_str(&api_key)?);
153
154 let client = Client::builder()
155 .timeout(Duration::from_secs(10))
156 .https_only(true)
157 .default_headers(header_map)
158 .build()?;
159
160 Ok(Self { client })
161 }
162
163 async fn raw_req(
164 &self,
165 size: usize,
166 page: usize,
167 query: String,
168 ) -> Result<Response, DehashedError> {
169 let res = self
170 .client
171 .post(URL)
172 .json(&json!({"query": query, "size": size, "page": page}))
173 .send()
174 .await?;
175
176 let status = res.status();
177 let raw = res.text().await?;
178 debug!("status code: {status}. Raw: {raw}");
179 if status == StatusCode::from_u16(302).unwrap() {
180 Err(DehashedError::InvalidQuery)
181 } else if status == StatusCode::from_u16(400).unwrap() {
182 Err(DehashedError::Unknown(raw))
183 } else if status == StatusCode::from_u16(401).unwrap() {
184 Err(DehashedError::Unauthorized)
185 } else if status == StatusCode::from_u16(200).unwrap() {
186 match serde_json::from_str(&raw) {
187 Ok(result) => Ok(result),
188 Err(err) => {
189 error!("Error deserializing data: {err}. Raw data: {raw}");
190 Err(DehashedError::Unknown(raw))
191 }
192 }
193 } else {
194 warn!("Invalid response, status code: {status}. Raw: {raw}");
195
196 Err(DehashedError::Unknown(raw))
197 }
198 }
199
200 pub async fn search(&self, query: Query) -> Result<SearchResult, DehashedError> {
207 let q = query.to_string();
208 debug!("Query: {q}");
209
210 let mut search_result = SearchResult {
211 entries: vec![],
212 balance: 0,
213 };
214 for page in 1.. {
215 let res = self.raw_req(10_000, page, q.clone()).await?;
216
217 if let Some(entries) = res.entries {
218 for entry in entries {
219 search_result.entries.push(entry)
220 }
221 }
222
223 search_result.balance = res.balance;
224
225 if res.total < page * 10_000 {
226 break;
227 }
228
229 #[cfg(feature = "tokio")]
230 sleep(Duration::from_millis(200)).await;
231 }
232
233 Ok(search_result)
234 }
235
236 #[cfg(feature = "tokio")]
241 pub fn start_scheduler(&self) -> Scheduler {
242 Scheduler::new(self)
243 }
244}