cloudscraper_rs/challenges/core/
executor.rs1use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::Duration;
10
11use async_trait::async_trait;
12use http::Method;
13use http::header::{HeaderMap, HeaderName, HeaderValue, LOCATION, REFERER};
14use thiserror::Error;
15use tokio::time::sleep;
16use url::Url;
17
18use super::types::ChallengeSubmission;
19
20#[async_trait]
25pub trait ChallengeHttpClient: Send + Sync {
26 async fn send_form(
27 &self,
28 method: &Method,
29 url: &Url,
30 headers: &HeaderMap,
31 form_fields: &HashMap<String, String>,
32 allow_redirects: bool,
33 ) -> Result<ChallengeHttpResponse, ChallengeHttpClientError>;
34
35 async fn send_with_body(
36 &self,
37 method: &Method,
38 url: &Url,
39 headers: &HeaderMap,
40 body: Option<&[u8]>,
41 allow_redirects: bool,
42 ) -> Result<ChallengeHttpResponse, ChallengeHttpClientError>;
43}
44
45#[derive(Debug, Clone)]
47pub struct ChallengeHttpResponse {
48 pub status: u16,
49 pub headers: HeaderMap,
50 pub body: Vec<u8>,
51 pub url: Url,
52 pub is_redirect: bool,
53}
54
55impl ChallengeHttpResponse {
56 pub fn location(&self) -> Option<&str> {
57 self.headers
58 .get(LOCATION)
59 .and_then(|value| value.to_str().ok())
60 }
61}
62
63#[derive(Debug, Error)]
64pub enum ChallengeHttpClientError {
65 #[error("http transport error: {0}")]
66 Transport(String),
67}
68
69#[derive(Debug, Error)]
71pub enum ChallengeExecutionError {
72 #[error("failed to convert header '{0}'")]
73 InvalidHeader(String),
74 #[error("invalid challenge answer detected")]
75 InvalidAnswer,
76 #[error("http client error: {0}")]
77 Client(#[from] ChallengeHttpClientError),
78}
79
80#[derive(Debug, Clone)]
82pub struct OriginalRequest {
83 pub method: Method,
84 pub url: Url,
85 pub headers: HeaderMap,
86 pub body: Option<Vec<u8>>,
87}
88
89impl OriginalRequest {
90 pub fn new(method: Method, url: Url) -> Self {
91 Self {
92 method,
93 url,
94 headers: HeaderMap::new(),
95 body: None,
96 }
97 }
98
99 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
100 self.headers = headers;
101 self
102 }
103
104 pub fn with_body(mut self, body: Option<Vec<u8>>) -> Self {
105 self.body = body;
106 self
107 }
108}
109
110pub async fn execute_challenge_submission(
118 client: Arc<dyn ChallengeHttpClient>,
119 submission: ChallengeSubmission,
120 original_request: OriginalRequest,
121) -> Result<ChallengeHttpResponse, ChallengeExecutionError> {
122 if submission.wait > Duration::from_millis(0) {
123 sleep(submission.wait).await;
124 }
125
126 let submission_headers = convert_headers(&submission.headers)?;
127 let first_response = client
128 .send_form(
129 &submission.method,
130 &submission.url,
131 &submission_headers,
132 &submission.form_fields,
133 submission.allow_redirects,
134 )
135 .await?;
136
137 if first_response.status == 400 {
138 return Err(ChallengeExecutionError::InvalidAnswer);
139 }
140
141 if !first_response.is_redirect {
142 return Ok(first_response);
143 }
144
145 let redirect_target = resolve_redirect(&first_response, &original_request.url);
146 let mut follow_headers = original_request.headers.clone();
147 follow_headers.insert(
148 REFERER,
149 HeaderValue::from_str(first_response.url.as_str())
150 .map_err(|_| ChallengeExecutionError::InvalidHeader("referer".into()))?,
151 );
152
153 let follow_response = client
154 .send_with_body(
155 &original_request.method,
156 &redirect_target,
157 &follow_headers,
158 original_request.body.as_deref(),
159 true,
160 )
161 .await?;
162
163 Ok(follow_response)
164}
165
166fn convert_headers(
167 headers: &HashMap<String, String>,
168) -> Result<HeaderMap, ChallengeExecutionError> {
169 let mut map = HeaderMap::new();
170 for (name, value) in headers {
171 let header_name = HeaderName::from_bytes(name.as_bytes())
172 .map_err(|_| ChallengeExecutionError::InvalidHeader(name.clone()))?;
173 let header_value = HeaderValue::from_str(value)
174 .map_err(|_| ChallengeExecutionError::InvalidHeader(name.clone()))?;
175 map.insert(header_name, header_value);
176 }
177 Ok(map)
178}
179
180fn resolve_redirect(first_response: &ChallengeHttpResponse, original_url: &Url) -> Url {
181 if let Some(location) = first_response.location() {
182 if let Ok(absolute) = Url::parse(location)
183 && absolute.has_host()
184 {
185 return absolute;
186 }
187
188 if let Ok(joined) = first_response.url.join(location) {
189 return joined;
190 }
191 }
192
193 original_url.clone()
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use std::sync::Mutex;
200
201 struct StubClient {
202 responses: Mutex<Vec<ChallengeHttpResponse>>,
203 }
204
205 impl StubClient {
206 fn new(responses: Vec<ChallengeHttpResponse>) -> Self {
207 Self {
208 responses: Mutex::new(responses.into_iter().rev().collect()),
209 }
210 }
211
212 fn pop_response(&self) -> ChallengeHttpResponse {
213 self.responses
214 .lock()
215 .unwrap()
216 .pop()
217 .expect("no more stub responses")
218 }
219 }
220
221 #[async_trait]
222 impl ChallengeHttpClient for StubClient {
223 async fn send_form(
224 &self,
225 _method: &Method,
226 _url: &Url,
227 _headers: &HeaderMap,
228 _form_fields: &HashMap<String, String>,
229 _allow_redirects: bool,
230 ) -> Result<ChallengeHttpResponse, ChallengeHttpClientError> {
231 Ok(self.pop_response())
232 }
233
234 async fn send_with_body(
235 &self,
236 _method: &Method,
237 _url: &Url,
238 _headers: &HeaderMap,
239 _body: Option<&[u8]>,
240 _allow_redirects: bool,
241 ) -> Result<ChallengeHttpResponse, ChallengeHttpClientError> {
242 Ok(self.pop_response())
243 }
244 }
245
246 fn make_response(status: u16, url: &str, headers: HeaderMap) -> ChallengeHttpResponse {
247 ChallengeHttpResponse {
248 status,
249 headers,
250 body: vec![],
251 url: Url::parse(url).unwrap(),
252 is_redirect: (300..400).contains(&status),
253 }
254 }
255
256 #[tokio::test]
257 async fn returns_first_response_when_not_redirect() {
258 let submission = ChallengeSubmission::new(
259 Method::POST,
260 Url::parse("https://example.com/submit").unwrap(),
261 HashMap::from([(String::from("foo"), String::from("bar"))]),
262 HashMap::from([(String::from("referer"), String::from("https://example.com"))]),
263 Duration::from_millis(0),
264 );
265
266 let original =
267 OriginalRequest::new(Method::GET, Url::parse("https://example.com").unwrap());
268
269 let headers = HeaderMap::new();
270 let client = Arc::new(StubClient::new(vec![make_response(
271 200,
272 "https://example.com",
273 headers.clone(),
274 )]));
275
276 let response = execute_challenge_submission(client, submission, original)
277 .await
278 .unwrap();
279
280 assert_eq!(response.status, 200);
281 }
282
283 #[tokio::test]
284 async fn follows_redirect_and_returns_final_response() {
285 let submission = ChallengeSubmission::new(
286 Method::POST,
287 Url::parse("https://example.com/submit").unwrap(),
288 HashMap::from([(String::from("foo"), String::from("bar"))]),
289 HashMap::from([(String::from("referer"), String::from("https://example.com"))]),
290 Duration::from_millis(0),
291 );
292
293 let mut original_headers = HeaderMap::new();
294 original_headers.insert("user-agent", HeaderValue::from_static("test-agent"));
295
296 let original = OriginalRequest::new(
297 Method::GET,
298 Url::parse("https://example.com/protected").unwrap(),
299 )
300 .with_headers(original_headers.clone());
301
302 let mut redirect_headers = HeaderMap::new();
303 redirect_headers.insert(LOCATION, HeaderValue::from_static("/redirected"));
304
305 let client = Arc::new(StubClient::new(vec![
306 make_response(200, "https://example.com/redirected", HeaderMap::new()),
307 make_response(302, "https://example.com/submit", redirect_headers),
308 ]));
309
310 let response = execute_challenge_submission(client, submission, original)
311 .await
312 .unwrap();
313
314 assert_eq!(response.url.as_str(), "https://example.com/redirected");
315 }
316}