interactsh_rs/client/
builder.rs

1use std::net::{IpAddr, SocketAddr};
2use std::time::Duration;
3
4use rand::distributions::{Alphanumeric, DistString};
5use rand::seq::SliceRandom;
6use rand::thread_rng;
7use reqwest::Proxy;
8use secrecy::Secret;
9use snafu::{OptionExt, ResultExt};
10use uuid::Uuid;
11
12use super::errors::{client_build_error, ClientBuildError};
13use super::unregistered::UnregisteredClient;
14use crate::crypto::rsa::RSAPrivKey;
15
16/// The default list of servers provided by the Interactsh team
17const DEFAULT_INTERACTSH_SERVERS: &[&str] = &[
18    "oast.pro",
19    "oast.live",
20    "oast.site",
21    "oast.online",
22    "oast.fun",
23    // "oast.me",
24];
25
26/// Builds an [UnregisteredClient](crate::client::UnregisteredClient)
27pub struct ClientBuilder {
28    rsa_key_size: Option<usize>,
29    server: Option<String>,
30    auth_token: Option<Secret<String>>,
31    proxies: Option<Vec<Proxy>>,
32    timeout: Option<Duration>,
33    ssl_verify: bool,
34    parse_logs: bool,
35    dns_override: Option<IpAddr>,
36}
37
38impl ClientBuilder {
39    /// Create a new builder with no options defined.
40    pub fn new() -> Self {
41        Self {
42            rsa_key_size: None,
43            server: None,
44            auth_token: None,
45            proxies: None,
46            timeout: None,
47            ssl_verify: false,
48            parse_logs: true,
49            dns_override: None,
50        }
51    }
52
53    /// Sets the RSA key size that the builder will generate for the client.
54    pub fn with_rsa_key_size(self, num_bits: usize) -> Self {
55        Self {
56            rsa_key_size: Some(num_bits),
57            ..self
58        }
59    }
60
61    /// Sets the Interactsh server that the client will connect to.
62    pub fn with_server(self, server: String) -> Self {
63        Self {
64            server: Some(server),
65            ..self
66        }
67    }
68
69    /// Sets an optional auth token that the client will use to authenticate
70    /// with the Interactsh server.
71    ///
72    /// If this is not set, then no auth header will be sent to the
73    /// server.
74    pub fn with_auth_token(self, auth_token: String) -> Self {
75        let token = Secret::new(auth_token);
76        Self {
77            auth_token: Some(token),
78            ..self
79        }
80    }
81
82    /// Sets an optional proxy that the client can use.
83    ///
84    /// This can be set more than once; each new proxy will be added
85    /// to a list of proxies that the client will try. Proxies will be
86    /// tried in the order added.
87    pub fn with_proxy(self, proxy: Proxy) -> Self {
88        let proxies = match self.proxies {
89            Some(mut proxies) => {
90                proxies.push(proxy);
91                Some(proxies)
92            }
93            None => Some(vec![proxy]),
94        };
95
96        Self { proxies, ..self }
97    }
98
99    /// Sets the timeout value for server requests.
100    pub fn with_timeout(self, timeout: Duration) -> Self {
101        Self {
102            timeout: Some(timeout),
103            ..self
104        }
105    }
106
107    /// Sets whether or not the client should verify the
108    /// server's SSL certificate.
109    pub fn verify_ssl(self, ssl_verify: bool) -> Self {
110        Self { ssl_verify, ..self }
111    }
112
113    /// Sets whether or not the client should parse the logs
114    /// or just return the raw logs.
115    pub fn parse_logs(self, parse_logs: bool) -> Self {
116        Self { parse_logs, ..self }
117    }
118
119    /// Sets an option on the client to override normal DNS
120    /// resolution for the server and instead use the provided
121    /// IP address.
122    pub fn set_dns_override(self, server_ip_address: IpAddr) -> Self {
123        Self {
124            dns_override: Some(server_ip_address),
125            ..self
126        }
127    }
128
129    /// Builds an [UnregisteredClient](crate::client::UnregisteredClient).
130    ///
131    /// The server must be set and the RSA key generated in order for
132    /// this to succeed. If the build succeeds, the
133    /// register function must be called on the returned
134    ///  [UnregisteredClient](crate::client::UnregisteredClient)
135    /// to turn it into a [RegisteredClient](crate::client::RegisteredClient).
136    pub fn build(self) -> Result<UnregisteredClient, ClientBuildError> {
137        // Ensure rsa_key and server are set
138        let rsa_key_size = self
139            .rsa_key_size
140            .context(client_build_error::MissingRsaKeySize)?;
141        let server = self.server.context(client_build_error::MissingServer)?;
142
143        // Get the other values needed
144        let rsa_key = RSAPrivKey::generate(rsa_key_size).context(client_build_error::RsaGen)?;
145        let pubkey = rsa_key
146            .get_pub_key()
147            .context(client_build_error::PubKeyExtract)?;
148        let secret = Uuid::new_v4().to_string();
149        let encoded_pub_key = pubkey
150            .b64_encode()
151            .context(client_build_error::PubKeyEncode)?;
152
153        let sub_domain = Alphanumeric
154            .sample_string(&mut thread_rng(), 33)
155            .to_ascii_lowercase();
156        let mut correlation_id = sub_domain.clone();
157        correlation_id.truncate(20);
158
159        // Build the reqwest client
160        let mut reqwest_client_builder = reqwest::Client::builder();
161
162        reqwest_client_builder = match self.proxies {
163            None => reqwest_client_builder,
164            Some(proxies) => {
165                let mut builder = reqwest_client_builder;
166
167                for proxy in proxies.into_iter() {
168                    builder = builder.proxy(proxy);
169                }
170
171                builder
172            }
173        };
174
175        let timeout = self.timeout.unwrap_or(Duration::from_secs(15));
176        reqwest_client_builder = reqwest_client_builder.timeout(timeout);
177
178        cfg_if::cfg_if! {
179            if #[cfg(all(feature = "reqwest-rustls-tls", feature = "reqwest-native-tls"))] {
180                reqwest_client_builder = reqwest_client_builder.use_rustls_tls();
181            }
182        }
183
184        reqwest_client_builder =
185            reqwest_client_builder.danger_accept_invalid_certs(!self.ssl_verify);
186
187        reqwest_client_builder = match self.dns_override {
188            Some(server_ip_address) => {
189                let socket_addr = SocketAddr::new(server_ip_address, 443);
190                reqwest_client_builder.resolve(server.as_str(), socket_addr)
191            }
192            None => reqwest_client_builder,
193        };
194
195        let reqwest_client = reqwest_client_builder
196            .build()
197            .context(client_build_error::ReqwestBuildFailed)?;
198
199        // Create the UnregisteredClient object
200        let unreg_client = UnregisteredClient {
201            rsa_key,
202            server,
203            sub_domain,
204            correlation_id,
205            auth_token: self.auth_token,
206            secret_key: Secret::new(secret),
207            encoded_pub_key,
208            reqwest_client,
209            parse_logs: self.parse_logs,
210        };
211
212        Ok(unreg_client)
213    }
214}
215
216impl Default for ClientBuilder {
217    /// Create a new builder with the default options.
218    ///
219    /// This will create a builder with a 2048 bit RSA key and server randomly picked from the
220    /// [list of default servers](https://github.com/projectdiscovery/interactsh#using-self-hosted-server)
221    /// provided and maintained by the Interactsh team. This will also set the timeout
222    /// to 15 seconds, SSL verification to false, and parse_logs to true.
223    fn default() -> Self {
224        let server = *DEFAULT_INTERACTSH_SERVERS
225            .choose(&mut rand::thread_rng())
226            .unwrap_or(&"oast.pro"); // if random choice somehow returns None, just use oast.pro
227
228        Self {
229            rsa_key_size: Some(2048),
230            server: Some(server.to_string()),
231            auth_token: None,
232            proxies: None,
233            timeout: Some(Duration::from_secs(15)),
234            ssl_verify: false,
235            parse_logs: true,
236            dns_override: None,
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use std::time::Duration;
244
245    use rand::{Rng, RngCore};
246
247    use super::*;
248
249    #[test]
250    fn default_build_succeeds() {
251        let _builder = ClientBuilder::default()
252            .build()
253            .expect("Default build failed");
254    }
255
256    #[test]
257    fn empty_builder_fails() {
258        let _builder = ClientBuilder::new()
259            .build()
260            .expect_err("Empty builder did not fail as expected");
261    }
262
263    #[test]
264    fn build_with_server_and_rsa_only_succeeds() {
265        let _builder = ClientBuilder::new()
266            .with_server("oast.pro".into())
267            .with_rsa_key_size(2048)
268            .build()
269            .expect("Build with only server and rsa failed");
270    }
271
272    #[test]
273    // Note: does not test dns override; that is tested in integration testing
274    fn build_with_all_options_succeeds() {
275        let mut rng = rand::thread_rng();
276
277        // Generate a random token string
278        let mut rand_bytes: [u8; 32] = [0; 32];
279        rng.fill_bytes(&mut rand_bytes);
280        let token = hex::encode(rand_bytes);
281
282        // Get a random duration in seconds
283        let duration_secs = rng.gen_range(5..=30);
284
285        // Generate boolean values
286        let verify_ssl = rng.gen_bool(1.0 / 2.0);
287        let parse_logs = rng.gen_bool(1.0 / 2.0);
288
289        let _builder = ClientBuilder::new()
290            .with_server("oast.pro".into())
291            .with_rsa_key_size(2048)
292            .with_auth_token(token)
293            .with_timeout(Duration::from_secs(duration_secs))
294            .verify_ssl(verify_ssl)
295            .parse_logs(parse_logs)
296            .build()
297            .expect("Build with all options failed");
298    }
299
300    #[test]
301    fn build_with_only_server_fails() {
302        let _builder = ClientBuilder::new()
303            .with_server("oast.pro".into())
304            .build()
305            .expect_err("Server-only build did not fail as expected");
306    }
307
308    #[test]
309    fn build_with_only_rsa_fails() {
310        let _builder = ClientBuilder::new()
311            .with_rsa_key_size(2048)
312            .build()
313            .expect_err("RSA-only build did not fail as expected");
314    }
315}