Skip to main content

shodan_client/
lib.rs

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", &parameters)?).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", &parameters)?).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", &parameters)?).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(), &parameters)?)
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", &parameters)?).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", &parameters)?).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(), &parameters)?)
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", &parameters)?).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", &parameters)?).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", &parameters)?).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        // Set API key
194        url.query_pairs_mut()
195            .append_pair("key", self.api_key.as_str());
196
197        // Set any additional parameters
198        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}