interactsh_rs/client/
builder.rs1use 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
16const DEFAULT_INTERACTSH_SERVERS: &[&str] = &[
18 "oast.pro",
19 "oast.live",
20 "oast.site",
21 "oast.online",
22 "oast.fun",
23 ];
25
26pub 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 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 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 pub fn with_server(self, server: String) -> Self {
63 Self {
64 server: Some(server),
65 ..self
66 }
67 }
68
69 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 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 pub fn with_timeout(self, timeout: Duration) -> Self {
101 Self {
102 timeout: Some(timeout),
103 ..self
104 }
105 }
106
107 pub fn verify_ssl(self, ssl_verify: bool) -> Self {
110 Self { ssl_verify, ..self }
111 }
112
113 pub fn parse_logs(self, parse_logs: bool) -> Self {
116 Self { parse_logs, ..self }
117 }
118
119 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 pub fn build(self) -> Result<UnregisteredClient, ClientBuildError> {
137 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 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 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 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 fn default() -> Self {
224 let server = *DEFAULT_INTERACTSH_SERVERS
225 .choose(&mut rand::thread_rng())
226 .unwrap_or(&"oast.pro"); 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 fn build_with_all_options_succeeds() {
275 let mut rng = rand::thread_rng();
276
277 let mut rand_bytes: [u8; 32] = [0; 32];
279 rng.fill_bytes(&mut rand_bytes);
280 let token = hex::encode(rand_bytes);
281
282 let duration_secs = rng.gen_range(5..=30);
284
285 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}