1use std::collections::HashMap;
2
3use serde::Deserialize;
4use thiserror::Error;
5use url::Url;
6
7mod parameter;
8use parameter::*;
9
10mod response;
11pub use response::*;
12
13mod builders;
14pub use builders::*;
15
16const BASE_API_URL: &str = "https://api.shodan.io";
17
18#[derive(Debug, Error)]
19pub enum Error {
20 #[error("Couldn't parse URL: {0}")]
21 UrlParse(#[from] url::ParseError),
22
23 #[error("Shodan API error: {0}")]
24 Shodan(String),
25
26 #[error("Caught reqwest error: {0}")]
27 Reqwest(#[from] reqwest::Error),
28}
29
30pub type Result<T> = std::result::Result<T, Error>;
31
32#[derive(Clone)]
33pub struct ShodanClient {
34 api_key: String,
35}
36
37impl ShodanClient {
38 pub fn new(api_key: String) -> Self {
39 Self { api_key }
40 }
41
42 pub async fn account_profile(&self) -> Result<AccountProfileResponse> {
43 Self::fetch(self.build_request_url("/account/profile", &ParameterBag::empty())?).await
44 }
45
46 pub async fn api_info(&self) -> Result<ApiInfoResponse> {
47 Self::fetch(self.build_request_url("/api-info", &ParameterBag::empty())?).await
48 }
49
50 pub async fn directory_query(
51 &self,
52 page: Option<u32>,
53 sort: Option<&str>,
54 order: Option<&str>,
55 ) -> Result<ShodanClientResponse<DirectoryQueryResponse>> {
56 let mut parameters = ParameterBag::default();
57 parameters.set_optional("page", page);
58 parameters.set_optional("sort", sort);
59 parameters.set_optional("order", order);
60
61 Self::fetch(self.build_request_url("/shodan/query", ¶meters)?).await
62 }
63
64 pub async fn directory_query_search(
65 &self,
66 query: &str,
67 page: Option<u32>,
68 ) -> Result<ShodanClientResponse<DirectoryQueryResponse>> {
69 let mut parameters = ParameterBag::default();
70 parameters.set("query", query);
71 parameters.set_optional("page", page);
72
73 Self::fetch(self.build_request_url("/shodan/query/search", ¶meters)?).await
74 }
75
76 pub async fn directory_query_tags(
77 &self,
78 size: Option<u32>,
79 ) -> Result<ShodanClientResponse<DirectoryQueryTagsResponse>> {
80 let mut parameters = ParameterBag::default();
81 parameters.set_optional("size", size);
82
83 Self::fetch(self.build_request_url("/shodan/query/tags", ¶meters)?).await
84 }
85
86 pub async fn dns_domain(
87 &self,
88 domain: &str,
89 history: Option<bool>,
90 dns_type: Option<&str>,
91 page: Option<u32>,
92 ) -> Result<DnsDomainResponse> {
93 let mut parameters = ParameterBag::default();
94 parameters.set_optional("history", history);
95 parameters.set_optional("dns_type", dns_type);
96 parameters.set_optional("page", page);
97
98 Self::fetch(self.build_request_url(format!("/dns/domain/{domain}").as_str(), ¶meters)?)
99 .await
100 }
101
102 pub async fn dns_resolve(&self, hostnames: &[&str]) -> Result<HashMap<String, Option<String>>> {
103 let mut parameters = ParameterBag::default();
104 parameters.set("hostnames", hostnames.join(","));
105
106 Self::fetch(self.build_request_url("/dns/resolve", ¶meters)?).await
107 }
108
109 pub async fn dns_reverse(&self, ips: &[&str]) -> Result<HashMap<String, Vec<String>>> {
110 let mut parameters = ParameterBag::default();
111 parameters.set("ips", ips.join(","));
112
113 Self::fetch(self.build_request_url("/dns/reverse", ¶meters)?).await
114 }
115
116 pub async fn scanning_ports(&self) -> Result<ShodanClientResponse<Vec<u16>>> {
117 Self::fetch(self.build_request_url("/shodan/ports", &ParameterBag::empty())?).await
118 }
119
120 pub async fn scanning_protocols(
121 &self,
122 ) -> Result<ShodanClientResponse<HashMap<String, String>>> {
123 Self::fetch(self.build_request_url("/shodan/protocols", &ParameterBag::empty())?).await
124 }
125
126 pub async fn host_ip(
127 &self,
128 ip: &str,
129 history: Option<bool>,
130 minifi: Option<bool>,
131 ) -> Result<SearchHostIpResponse> {
132 let mut parameters = ParameterBag::default();
133 parameters.set_optional("history", history);
134 parameters.set_optional("minifi", minifi);
135
136 Self::fetch(self.build_request_url(format!("/shodan/host/{ip}").as_str(), ¶meters)?)
137 .await
138 }
139
140 pub async fn host_search(
141 &self,
142 query: &str,
143 facets: Option<&str>,
144 page: Option<u32>,
145 minifi: Option<bool>,
146 ) -> Result<SearchResult> {
147 let mut parameters = ParameterBag::default();
148 parameters.set("query", query);
149 parameters.set_optional("facets", facets);
150 parameters.set_optional("page", page);
151 parameters.set_optional("minifi", minifi);
152
153 Self::fetch(self.build_request_url("/shodan/host/search", ¶meters)?).await
154 }
155
156 pub async fn host_count(&self, query: &str, facets: Option<&str>) -> Result<CountResponse> {
157 let mut parameters = ParameterBag::default();
158 parameters.set("query", query);
159 parameters.set_optional("facets", facets);
160
161 Self::fetch(self.build_request_url("/shodan/host/count", ¶meters)?).await
162 }
163
164 pub async fn host_facets(&self) -> Result<Vec<String>> {
165 Self::fetch(self.build_request_url("/shodan/host/search/facets", &ParameterBag::empty())?)
166 .await
167 }
168
169 pub async fn host_filters(&self) -> Result<Vec<String>> {
170 Self::fetch(self.build_request_url("/shodan/host/search/filters", &ParameterBag::empty())?)
171 .await
172 }
173
174 pub async fn host_tokens(&self, query: &str) -> Result<TokenResponse> {
175 let mut parameters = ParameterBag::default();
176 parameters.set("query", query);
177
178 Self::fetch(self.build_request_url("/shodan/host/search/tokens", ¶meters)?).await
179 }
180
181 pub async fn my_ip(&self) -> Result<String> {
182 Self::fetch(self.build_request_url("/tools/myip", &ParameterBag::empty())?).await
183 }
184
185 pub async fn http_headers(&self) -> Result<HashMap<String, String>> {
186 Self::fetch(self.build_request_url("/tools/httpheaders", &ParameterBag::empty())?).await
187 }
188
189 fn build_request_url(&self, endpoint: &str, parameters: &ParameterBag) -> Result<String> {
190 let mut url = Url::parse(BASE_API_URL)?;
191 url.set_path(endpoint);
192
193 url.query_pairs_mut()
195 .append_pair("key", self.api_key.as_str());
196
197 url.query_pairs_mut().extend_pairs(parameters.pairs());
199
200 Ok(url.to_string())
201 }
202
203 async fn fetch<T: for<'a> Deserialize<'a>>(url: String) -> Result<T> {
204 let response = reqwest::get(url)
205 .await?
206 .json::<ShodanClientResponse<T>>()
207 .await?;
208
209 match response {
210 ShodanClientResponse::Error(e) => {
211 Err(Error::Shodan(format!("Error response: {}", e.error)))
212 }
213 ShodanClientResponse::Response(r) => Ok(r),
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use std::env;
222
223 pub fn get_test_api_key() -> String {
224 let api_key = env::var("SHODAN_TEST_KEY");
225 match api_key {
226 Ok(key) => key,
227 Err(_) => panic!("Did not specify a shodan API key for testing"),
228 }
229 }
230
231 #[tokio::test]
232 async fn can_get_account_profile() {
233 let client = ShodanClient::new(get_test_api_key());
234 client.account_profile().await.unwrap();
235 }
236
237 #[tokio::test]
238 async fn can_get_api_info() {
239 let client = ShodanClient::new(get_test_api_key());
240 client.api_info().await.unwrap();
241 }
242
243 #[tokio::test]
244 async fn can_get_directory_query() {
245 let client = ShodanClient::new(get_test_api_key());
246 client.directory_query(None, None, None).await.unwrap();
247 }
248
249 #[tokio::test]
250 async fn can_get_directory_query_search() {
251 let client = ShodanClient::new(get_test_api_key());
252 client.directory_query_search("webcam", None).await.unwrap();
253 }
254
255 #[tokio::test]
256 async fn can_get_directory_query_tags() {
257 let client = ShodanClient::new(get_test_api_key());
258 client.directory_query_tags(None).await.unwrap();
259 }
260
261 #[tokio::test]
262 async fn can_get_dns_domain() {
263 let client = ShodanClient::new(get_test_api_key());
264 client
265 .dns_domain("google.com", None, None, None)
266 .await
267 .unwrap();
268 }
269
270 #[tokio::test]
271 async fn can_get_dns_resolve() {
272 let client = ShodanClient::new(get_test_api_key());
273 client
274 .dns_resolve(&["google.com", "facebook.com"])
275 .await
276 .unwrap();
277 }
278
279 #[tokio::test]
280 async fn can_get_dns_reverse() {
281 let client = ShodanClient::new(get_test_api_key());
282 client.dns_reverse(&["8.8.8.8", "1.1.1.1"]).await.unwrap();
283 }
284
285 #[tokio::test]
286 async fn can_get_ports() {
287 let client = ShodanClient::new(get_test_api_key());
288 client.scanning_ports().await.unwrap();
289 }
290
291 #[tokio::test]
292 async fn can_get_protocols() {
293 let client = ShodanClient::new(get_test_api_key());
294 client.scanning_protocols().await.unwrap();
295 }
296
297 #[tokio::test]
298 async fn can_get_google_host_ip() {
299 let client = ShodanClient::new(get_test_api_key());
300 client.host_ip("8.8.8.8", None, None).await.unwrap();
301 }
302
303 #[tokio::test]
304 async fn can_get_host_facets() {
305 let client = ShodanClient::new(get_test_api_key());
306 client.host_facets().await.unwrap();
307 }
308
309 #[tokio::test]
310 async fn can_get_host_filters() {
311 let client = ShodanClient::new(get_test_api_key());
312 client.host_filters().await.unwrap();
313 }
314
315 #[tokio::test]
316 async fn can_get_google_count() {
317 let client = ShodanClient::new(get_test_api_key());
318 client.host_count("google", None).await.unwrap();
319 }
320
321 #[tokio::test]
322 async fn can_get_google_count_with_facets() {
323 let client = ShodanClient::new(get_test_api_key());
324 client
325 .host_count("google", Some("os,country"))
326 .await
327 .unwrap();
328 }
329
330 #[tokio::test]
331 async fn can_get_google_search() {
332 let client = ShodanClient::new(get_test_api_key());
333 client
334 .host_search("google", None, None, Some(true))
335 .await
336 .unwrap();
337 }
338
339 #[tokio::test]
340 async fn can_get_raspbian_tokens() {
341 let client = ShodanClient::new(get_test_api_key());
342 client.host_tokens("Raspbian port:22").await.unwrap();
343 }
344
345 #[tokio::test]
346 async fn can_get_my_ip() {
347 let client = ShodanClient::new(get_test_api_key());
348 client.my_ip().await.unwrap();
349 }
350
351 #[tokio::test]
352 async fn can_get_http_headers() {
353 let client = ShodanClient::new(get_test_api_key());
354 client.http_headers().await.unwrap();
355 }
356}