cloudscraper_rs/challenges/core/
reqwest_client.rs

1//! Reqwest-based implementation of the `ChallengeHttpClient` trait.
2//!
3//! Provides a thin adapter around `reqwest::Client` that converts between the
4//! shared HTTP representations used by the solver core and the concrete
5//! transport.
6
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use http::{
11    HeaderMap as HttpHeaderMap, HeaderName as HttpHeaderName, HeaderValue as HttpHeaderValue,
12    Method as HttpMethod,
13};
14use reqwest::{Client, Method, header::HeaderMap, redirect::Policy};
15use url::Url;
16
17use super::{
18    ChallengeExecutionError, ChallengeHttpClient, ChallengeHttpClientError, ChallengeHttpResponse,
19};
20
21/// Reqwest-backed HTTP client used during challenge replay.
22pub struct ReqwestChallengeHttpClient {
23    client: Client,
24}
25
26impl ReqwestChallengeHttpClient {
27    /// Creates a new client with redirects disabled so the executor can inspect
28    /// redirect responses explicitly.
29    pub fn new() -> Result<Self, ChallengeExecutionError> {
30        let client = Client::builder()
31            .redirect(Policy::none())
32            .cookie_store(true)
33            .build()
34            .map_err(|err| {
35                ChallengeExecutionError::Client(ChallengeHttpClientError::Transport(
36                    err.to_string(),
37                ))
38            })?;
39
40        Ok(Self { client })
41    }
42
43    /// Wrap an existing reqwest client. The client should already have
44    /// redirects disabled; otherwise redirects will be followed automatically
45    /// and the executor will not observe the intermediate 30x response.
46    pub fn from_client(client: Client) -> Self {
47        Self { client }
48    }
49}
50
51impl Default for ReqwestChallengeHttpClient {
52    fn default() -> Self {
53        Self::new().expect("failed to create reqwest challenge client")
54    }
55}
56
57#[async_trait]
58impl ChallengeHttpClient for ReqwestChallengeHttpClient {
59    async fn send_form(
60        &self,
61        method: &HttpMethod,
62        url: &Url,
63        headers: &HttpHeaderMap,
64        form_fields: &std::collections::HashMap<String, String>,
65        _allow_redirects: bool,
66    ) -> Result<ChallengeHttpResponse, ChallengeHttpClientError> {
67        let req_method = map_method(method)?;
68        let req_headers = convert_headers(headers)?;
69
70        let response = self
71            .client
72            .request(req_method, url.as_str())
73            .headers(req_headers)
74            .form(form_fields)
75            .send()
76            .await
77            .map_err(|err| ChallengeHttpClientError::Transport(err.to_string()))?;
78
79        Ok(to_challenge_response(response).await?)
80    }
81
82    async fn send_with_body(
83        &self,
84        method: &HttpMethod,
85        url: &Url,
86        headers: &HttpHeaderMap,
87        body: Option<&[u8]>,
88        _allow_redirects: bool,
89    ) -> Result<ChallengeHttpResponse, ChallengeHttpClientError> {
90        let req_method = map_method(method)?;
91        let req_headers = convert_headers(headers)?;
92
93        let mut builder = self
94            .client
95            .request(req_method, url.as_str())
96            .headers(req_headers);
97
98        if let Some(data) = body {
99            builder = builder.body(data.to_vec());
100        }
101
102        let response = builder
103            .send()
104            .await
105            .map_err(|err| ChallengeHttpClientError::Transport(err.to_string()))?;
106
107        Ok(to_challenge_response(response).await?)
108    }
109}
110
111fn map_method(method: &HttpMethod) -> Result<Method, ChallengeHttpClientError> {
112    Method::from_bytes(method.as_str().as_bytes())
113        .map_err(|err| ChallengeHttpClientError::Transport(err.to_string()))
114}
115
116fn convert_headers(headers: &HttpHeaderMap) -> Result<HeaderMap, ChallengeHttpClientError> {
117    let mut map = HeaderMap::new();
118    for (name, value) in headers.iter() {
119        let name = reqwest::header::HeaderName::from_bytes(name.as_str().as_bytes())
120            .map_err(|err| ChallengeHttpClientError::Transport(err.to_string()))?;
121        let value = reqwest::header::HeaderValue::from_bytes(value.as_bytes())
122            .map_err(|err| ChallengeHttpClientError::Transport(err.to_string()))?;
123        map.insert(name, value);
124    }
125    Ok(map)
126}
127
128async fn to_challenge_response(
129    response: reqwest::Response,
130) -> Result<ChallengeHttpResponse, ChallengeHttpClientError> {
131    let status = response.status().as_u16();
132    let headers = convert_back_headers(response.headers())?;
133    let url = response.url().clone();
134    let is_redirect = response.status().is_redirection();
135    let body = response
136        .bytes()
137        .await
138        .map_err(|err| ChallengeHttpClientError::Transport(err.to_string()))?
139        .to_vec();
140
141    Ok(ChallengeHttpResponse {
142        status,
143        headers,
144        body,
145        url,
146        is_redirect,
147    })
148}
149
150fn convert_back_headers(map: &HeaderMap) -> Result<HttpHeaderMap, ChallengeHttpClientError> {
151    let mut headers = HttpHeaderMap::new();
152    for (name, value) in map.iter() {
153        let http_name = HttpHeaderName::from_bytes(name.as_str().as_bytes())
154            .map_err(|err| ChallengeHttpClientError::Transport(err.to_string()))?;
155        let http_value = HttpHeaderValue::from_bytes(value.as_bytes())
156            .map_err(|err| ChallengeHttpClientError::Transport(err.to_string()))?;
157        headers.insert(http_name, http_value);
158    }
159    Ok(headers)
160}
161
162type _AssertSync = Arc<ReqwestChallengeHttpClient>;