rust_tls_client/
client.rs

1use std::collections::HashMap;
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4use crate::request::{RequestPayload};
5use crate::types::{AeadId, ClientIdentifier, DelegatedCredential, H2Setting, KdfId, KeyShareCurve, SignatureAlgorithm, SupportedVersion};
6
7
8#[derive(Debug, Serialize)]
9#[serde(rename_all = "camelCase")]
10pub struct TlsClient {
11    // Session Parameters
12    // |- Can NOT be changed after first request
13    session_id:                         String,
14    insecure_skip_verify:               bool,
15    timeout_seconds:                    u32,
16
17    // |- Can be changed at anytime
18    follow_redirects:                   bool,
19    proxy_url:                          Option<String>,
20
21    // Profile
22    tls_client_identifier:              Option<ClientIdentifier>,
23    with_random_tls_extension_order:    Option<bool>,
24    custom_tls_client:                  Option<CustomClient>,
25
26    // Request parameters
27    request_url:                        Option<String>,
28    request_method:                     Option<String>,
29}
30
31
32
33impl TlsClient {
34    fn build_for_request(&self, method: String, url: String) -> RequestPayload {
35        let tc = TlsClient {
36            session_id: self.session_id.clone(),
37            insecure_skip_verify: self.insecure_skip_verify,
38            timeout_seconds: self.timeout_seconds,
39            follow_redirects: self.follow_redirects,
40            proxy_url: self.proxy_url.clone(),
41            tls_client_identifier: self.tls_client_identifier.clone(),
42            with_random_tls_extension_order: self.with_random_tls_extension_order,
43            custom_tls_client: self.custom_tls_client.clone(),
44            request_url: Some(url),
45            request_method: Some(method),
46        };
47        serde_json::from_value(serde_json::to_value(tc).unwrap()).unwrap()
48    }
49
50    pub fn new(client_identifier: ClientIdentifier, random_tls_order: bool) -> TlsClient {
51        TlsClient {
52            session_id: Uuid::new_v4().to_string(),
53            insecure_skip_verify: false,
54            timeout_seconds: 30,
55            follow_redirects: true,
56            tls_client_identifier: Some(client_identifier),
57            with_random_tls_extension_order: Some(random_tls_order),
58            custom_tls_client: None,
59            proxy_url: None,
60            request_url: None,
61            request_method: None,
62        }
63    }
64
65    pub fn new_custom(custom_profile: CustomClient) -> TlsClient {
66        TlsClient {
67            session_id: Uuid::new_v4().to_string(),
68            insecure_skip_verify: false,
69            timeout_seconds: 30,
70            follow_redirects: true,
71            tls_client_identifier: None,
72            with_random_tls_extension_order: Some(false),
73            custom_tls_client: Some(custom_profile),
74            proxy_url: None,
75            request_url: None,
76            request_method: None,
77        }
78    }
79
80    pub fn set_insecure_skip_verify(mut self, skip: bool) -> Self {
81        self.insecure_skip_verify = skip;
82        self
83    }
84
85    pub fn set_timeout_seconds(mut self, seconds: u32) -> Self {
86        self.timeout_seconds = seconds;
87        self
88    }
89
90    pub fn set_follow_redirects(mut self, follow: bool) -> Self {
91        self.follow_redirects = follow;
92        self
93    }
94
95    pub fn set_proxy_url(mut self, url: String) -> Self {
96        self.proxy_url = Some(url);
97        self
98    }
99
100    pub fn remove_proxy_url(mut self) -> Self {
101        self.proxy_url = None;
102        self
103    }
104
105    pub fn get(&self, url: &str) -> RequestPayload {
106        self.build_for_request("GET".to_string(), url.to_string())
107    }
108
109    pub fn post(&self, url: &str) -> RequestPayload {
110        self.build_for_request("POST".to_string(), url.to_string())
111    }
112
113}
114
115impl Default for TlsClient {
116    fn default() -> Self {
117        Self::new(ClientIdentifier::Chrome120, false)
118    }
119}
120
121/// Simple builder client for generating custom profiles for the `TlsClient`
122///
123#[derive(Default, Debug, Serialize, Deserialize, Clone)]
124#[serde(rename_all = "camelCase")]
125pub struct CustomClient {
126    cert_compression_algo:                      Option<String>,
127    connection_flow:                            Option<u32>,
128    h2_settings:                                Option<HashMap<H2Setting, u32>>,
129    h2_settings_order:                          Option<Vec<H2Setting>>,
130    header_priority:                            Option<PriorityParam>,
131    ja3_string:                                 Option<String>,
132    key_share_curves:                           Option<Vec<KeyShareCurve>>,
133    priority_frames:                            Option<Vec<PriorityFrames>>,
134    alpn_protocols:                             Option<Vec<String>>,
135    alps_protocols:                             Option<Vec<String>>,
136    #[serde(rename = "ECHCandidateCipherSuites")]
137    ech_candidate_cipher_suites:                Option<Vec<CandidateCipherSuite>>,
138    #[serde(rename = "ECHCandidatePayloads")]
139    ech_candidate_payloads:                     Option<u16>,
140    pseudo_header_order:                        Option<Vec<String>>,
141    supported_delegated_credentials_algorithms: Option<Vec<DelegatedCredential>>,
142    supported_signature_algorithms:             Option<Vec<SignatureAlgorithm>>,
143    supported_versions:                         Option<Vec<SupportedVersion>>,
144}
145
146impl CustomClient {
147    pub fn new() -> Self {
148        Self::default()
149    }
150
151    pub fn with_cert_compression_algo(mut self, algo: String) -> Self {
152        self.cert_compression_algo = Some(algo);
153        self
154    }
155
156    pub fn with_connection_flow(mut self, flow: u32) -> Self {
157        self.connection_flow = Some(flow);
158        self
159    }
160
161    pub fn with_h2_settings(mut self, settings: HashMap<H2Setting, u32>) -> Self {
162        self.h2_settings = Some(settings);
163        self
164    }
165
166    pub fn with_h2_settings_order(mut self, order: Vec<H2Setting>) -> Self {
167        self.h2_settings_order = Some(order);
168        self
169    }
170
171    pub fn with_header_priority(mut self, priority: PriorityParam) -> Self {
172        self.header_priority = Some(priority);
173        self
174    }
175
176    pub fn with_ja3_string(mut self, ja3: String) -> Self {
177        self.ja3_string = Some(ja3);
178        self
179    }
180
181    pub fn with_key_share_curves(mut self, curves: Vec<KeyShareCurve>) -> Self {
182        self.key_share_curves = Some(curves);
183        self
184    }
185
186    pub fn with_priority_frames(mut self, frames: Vec<PriorityFrames>) -> Self {
187        self.priority_frames = Some(frames);
188        self
189    }
190
191    pub fn with_alpn_protocols(mut self, protocols: Vec<String>) -> Self {
192        self.alpn_protocols = Some(protocols);
193        self
194    }
195
196    pub fn with_alps_protocols(mut self, protocols: Vec<String>) -> Self {
197        self.alps_protocols = Some(protocols);
198        self
199    }
200
201    pub fn with_ech_candidate_cipher_suites(mut self, suites: Vec<CandidateCipherSuite>) -> Self {
202        self.ech_candidate_cipher_suites = Some(suites);
203        self
204    }
205
206    pub fn with_ech_candidate_payloads(mut self, payloads: u16) -> Self {
207        self.ech_candidate_payloads = Some(payloads);
208        self
209    }
210
211    pub fn with_pseudo_header_order(mut self, order: Vec<String>) -> Self {
212        self.pseudo_header_order = Some(order);
213        self
214    }
215
216    pub fn with_supported_delegated_credentials_algorithms(mut self, algorithms: Vec<DelegatedCredential>) -> Self {
217        self.supported_delegated_credentials_algorithms = Some(algorithms);
218        self
219    }
220
221    pub fn with_supported_signature_algorithms(mut self, algorithms: Vec<SignatureAlgorithm>) -> Self {
222        self.supported_signature_algorithms = Some(algorithms);
223        self
224    }
225
226    pub fn with_supported_versions(mut self, versions: Vec<SupportedVersion>) -> Self {
227        self.supported_versions = Some(versions);
228        self
229    }
230}
231
232#[derive(Debug, Serialize, Deserialize, Clone)]
233#[serde(rename_all = "camelCase")]
234pub struct CandidateCipherSuite {
235    kdf_id: KdfId,
236    aead_id: AeadId,
237}
238
239#[derive(Debug, Serialize, Deserialize, Clone)]
240#[serde(rename_all = "camelCase")]
241pub struct PriorityFrames {
242    #[serde(rename = "streamID")]
243    stream_id: u32,
244    frame_payload: PriorityParam,
245}
246
247#[derive(Debug, Serialize, Deserialize, Clone)]
248#[serde(rename_all = "camelCase")]
249pub struct PriorityParam {
250    stream_dep: u32,
251    exclusive: bool,
252    weight: u32,
253}
254
255
256mod tests {
257    #[cfg(test)]
258    use std::io::Read;
259    #[cfg(test)]
260    use reqwest::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CONTENT_TYPE, USER_AGENT, HeaderMap, HeaderValue};
261    #[cfg(test)]
262    use super::*;
263
264    #[test]
265    fn test_get() {
266        let mut hm = HeaderMap::new();
267        hm.insert(ACCEPT, HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"));
268        hm.insert(USER_AGENT, HeaderValue::from_static("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"));
269        hm.insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip, deflate, br"));
270        hm.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"));
271
272        let client = TlsClient::new(ClientIdentifier::Chrome105, false);
273
274        let req = &client.get("https://microsoft.com")
275            .headers(hm)
276            .send()
277            .unwrap();
278
279        assert_eq!(req.get_status(), 200)
280    }
281    
282    #[test]
283    fn test_post() {
284        let mut hm = HeaderMap::new();
285        hm.insert(ACCEPT, HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"));
286        hm.insert(USER_AGENT, HeaderValue::from_static("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"));
287        hm.insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip, deflate, br"));
288        hm.insert(CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
289        hm.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"));
290
291        let client = TlsClient::new(ClientIdentifier::Chrome105, false);
292        
293        let req = client.post("https://www.toptal.com/developers/postbin/1711461823032-1978890220634")
294            .headers(hm)
295            .body("foo=bar&baz=foo")
296            .send()
297            .unwrap();
298
299        assert_eq!(req.get_status(), 200)
300    }
301    
302    #[test]
303    fn test_get_image() {
304        let mut hm = HeaderMap::new();
305        hm.insert(ACCEPT, HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"));
306        hm.insert(USER_AGENT, HeaderValue::from_static("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"));
307        hm.insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip, deflate, br"));
308        hm.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"));
309
310        let client = TlsClient::new(ClientIdentifier::Chrome105, false);
311        
312        let req = client.get("https://avatars.githubusercontent.com/u/17678241?v=4")
313            .headers(hm)
314            .byte_response() // Set response type to bytes
315            .send()
316            .unwrap();
317
318
319
320        assert_eq!(req.get_status(), 200)
321    }
322
323    #[test]
324    fn test_post_image() {
325        let mut hm = HeaderMap::new();
326        hm.insert(ACCEPT, HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"));
327        hm.insert(USER_AGENT, HeaderValue::from_static("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"));
328        hm.insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip, deflate, br"));
329        hm.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7"));
330
331        let client = TlsClient::new(ClientIdentifier::Chrome105, false);
332        
333        // Load image and convert to base64 string
334        let mut img_file = std::fs::File::open("src/cb_example.png").unwrap();
335        let mut buffer = Vec::new();
336        img_file.read_to_end(&mut buffer).unwrap();
337        let b64_file = base64::encode(&buffer);
338        println!("{}", b64_file);
339
340        let req = client.post("https://www.toptal.com/developers/postbin/1711492583368-7330834681633")
341            .headers(hm)
342            .body(b64_file)
343            .byte_request()
344            .send()
345            .unwrap();
346        
347        assert_eq!(serde_json::to_string(&req).unwrap(), "xx".to_string())
348
349    }
350
351    #[test]
352    fn make_custom_client() {
353
354    }
355
356}
357