1use serde::Deserialize;
2use std::collections::{hash_map, HashMap};
3use url::Url;
4
5mod account;
6mod api_status;
7mod builders;
8mod directory;
9mod dns;
10mod error;
11mod response;
12mod scanning;
13mod search;
14mod utility;
15
16pub use account::*;
17pub use api_status::*;
18pub use builders::*;
19pub use directory::*;
20pub use dns::*;
21pub use error::*;
22pub use response::*;
23pub use scanning::*;
24pub use search::*;
25pub use utility::*;
26
27const BASE_API_URL: &str = "https://api.shodan.io";
28
29pub struct ShodanClient {
30 api_key: String,
31}
32
33impl ShodanClient {
34 pub fn new(api_key: String) -> Self {
35 Self { api_key }
36 }
37
38 fn build_request_url(
39 &self,
40 endpoint: &str,
41 parameters: &ParameterBag,
42 ) -> Result<String, error::Error> {
43 let mut url = Url::parse(BASE_API_URL)?;
44 url.set_path(endpoint);
45
46 url.query_pairs_mut()
48 .append_pair("key", self.api_key.as_str());
49
50 url.query_pairs_mut().extend_pairs(parameters.pairs());
52
53 Ok(url.to_string())
54 }
55
56 async fn fetch<T: for<'a> Deserialize<'a>>(url: String) -> Result<T, Error> {
57 let response = reqwest::get(url)
58 .await?
59 .json::<ShodanClientResponse<T>>()
60 .await?;
61
62 match response {
63 ShodanClientResponse::Error(e) => {
64 Err(error::Error::Shodan(format!("Error response: {}", e.error)))
65 }
66 ShodanClientResponse::Response(r) => Ok(r),
67 }
68 }
69}
70
71#[derive(Default)]
72struct ParameterBag(HashMap<String, String>);
73
74impl ParameterBag {
75 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
76 self.0.insert(key.into(), value.into());
77 }
78
79 pub fn set_optional(
80 &mut self,
81 key: impl Into<String>,
82 value: Option<impl Into<ParameterValue>>,
83 ) {
84 if let Some(value) = value {
85 self.set(key.into(), value.into().0);
86 }
87 }
88
89 pub fn pairs(&self) -> hash_map::Iter<'_, String, String> {
90 self.0.iter()
91 }
92}
93
94pub struct ParameterValue(String);
95
96impl From<u32> for ParameterValue {
97 fn from(value: u32) -> Self {
98 Self(value.to_string())
99 }
100}
101
102impl From<String> for ParameterValue {
103 fn from(value: String) -> Self {
104 Self(value)
105 }
106}
107
108impl From<&str> for ParameterValue {
109 fn from(value: &str) -> Self {
110 Self(value.into())
111 }
112}
113
114impl From<bool> for ParameterValue {
115 fn from(value: bool) -> Self {
116 Self(if value { "true".into() } else { "false".into() })
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use std::env;
123
124 pub fn get_test_api_key() -> String {
125 let api_key = env::var("SHODAN_TEST_KEY");
126 match api_key {
127 Ok(key) => key,
128 Err(_) => panic!("Did not specify a shodan API key for testing"),
129 }
130 }
131}