cloudscraper_rs/challenges/solvers/
javascript_v1.rs1use std::sync::Arc;
11use std::time::Duration;
12
13use once_cell::sync::Lazy;
14use regex::{Regex, RegexBuilder};
15use thiserror::Error;
16
17use crate::challenges::core::{
18 ChallengeExecutionError, ChallengeHttpClient, ChallengeHttpResponse, ChallengeParseError,
19 ChallengeResponse, ChallengeSubmission, OriginalRequest, execute_challenge_submission,
20 is_cloudflare_response, origin_from_url, parse_iuam_challenge,
21};
22use crate::external_deps::interpreters::{InterpreterError, JavascriptInterpreter};
23
24use super::ChallengeSolver;
25
26pub struct JavascriptV1Solver {
28 interpreter: Arc<dyn JavascriptInterpreter>,
29}
30
31impl JavascriptV1Solver {
32 pub fn new(interpreter: Arc<dyn JavascriptInterpreter>) -> Self {
33 Self { interpreter }
34 }
35
36 pub fn is_iuam_challenge(&self, response: &ChallengeResponse<'_>) -> bool {
38 is_cloudflare_response(response)
39 && matches!(response.status, 429 | 503)
40 && response.body.contains("/cdn-cgi/images/trace/jsch/")
41 && parse_iuam_challenge(response).is_ok()
42 }
43
44 pub fn is_new_iuam_challenge(&self, response: &ChallengeResponse<'_>) -> bool {
46 static NEW_IUAM_RE: Lazy<Regex> = Lazy::new(|| {
47 RegexBuilder::new(
48 r#"cpo\.src\s*=\s*['\"/]+cdn-cgi/challenge-platform/\S+orchestrate/jsch/v1"#,
49 )
50 .case_insensitive(true)
51 .build()
52 .unwrap()
53 });
54
55 self.is_iuam_challenge(response) && NEW_IUAM_RE.is_match(response.body)
56 }
57
58 pub fn is_captcha_challenge(&self, response: &ChallengeResponse<'_>) -> bool {
60 is_cloudflare_response(response)
61 && response.status == 403
62 && response.body.contains("__cf_chl_captcha_tk__")
63 && response.body.contains("data-sitekey")
64 }
65
66 pub fn is_firewall_blocked(&self, response: &ChallengeResponse<'_>) -> bool {
68 is_cloudflare_response(response)
69 && response.status == 403
70 && response
71 .body
72 .to_ascii_lowercase()
73 .contains("<span class=\"cf-error-code\">1020</span>")
74 }
75
76 pub fn solve(
78 &self,
79 response: &ChallengeResponse<'_>,
80 ) -> Result<ChallengeSubmission, JavascriptV1Error> {
81 if !self.is_iuam_challenge(response) {
82 return Err(JavascriptV1Error::NotAnIuamChallenge);
83 }
84
85 let base_url = response.url.clone();
86 let host = base_url.host_str().ok_or(JavascriptV1Error::MissingHost)?;
87
88 let blueprint = parse_iuam_challenge(response).map_err(JavascriptV1Error::Parse)?;
89
90 let answer = self
91 .interpreter
92 .solve_challenge(response.body, host)
93 .map_err(JavascriptV1Error::Interpreter)?;
94
95 let mut submission = blueprint
96 .to_submission(&base_url, vec![("jschl_answer".to_string(), answer)])
97 .map_err(JavascriptV1Error::Parse)?;
98
99 submission.wait = extract_delay(response.body)?;
100 submission
101 .headers
102 .insert("Referer".into(), response.url.as_str().to_string());
103 submission
104 .headers
105 .insert("Origin".into(), origin_from_url(&base_url));
106
107 Ok(submission)
108 }
109
110 pub async fn solve_and_submit(
112 &self,
113 client: Arc<dyn ChallengeHttpClient>,
114 response: &ChallengeResponse<'_>,
115 original_request: OriginalRequest,
116 ) -> Result<ChallengeHttpResponse, JavascriptV1Error> {
117 let submission = self.solve(response)?;
118 execute_challenge_submission(client, submission, original_request)
119 .await
120 .map_err(JavascriptV1Error::Submission)
121 }
122}
123
124impl ChallengeSolver for JavascriptV1Solver {
125 fn name(&self) -> &'static str {
126 "javascript_v1"
127 }
128}
129
130fn extract_delay(body: &str) -> Result<Duration, JavascriptV1Error> {
131 static DELAY_RE: Lazy<Regex> = Lazy::new(|| {
132 RegexBuilder::new(r#"submit\(\);\r?\n\s*},\s*([0-9]+)"#)
133 .case_insensitive(true)
134 .build()
135 .unwrap()
136 });
137
138 let captures = DELAY_RE
139 .captures(body)
140 .ok_or(JavascriptV1Error::DelayNotFound)?;
141
142 let millis = captures
143 .get(1)
144 .and_then(|m| m.as_str().parse::<u64>().ok())
145 .ok_or(JavascriptV1Error::DelayNotFound)?;
146
147 Ok(Duration::from_millis(millis))
148}
149
150#[derive(Debug, Error)]
152pub enum JavascriptV1Error {
153 #[error("response is not an IUAM challenge")]
154 NotAnIuamChallenge,
155 #[error("unable to determine challenge host")]
156 MissingHost,
157 #[error("missing Cloudflare delay value")]
158 DelayNotFound,
159 #[error("javascript interpreter error: {0}")]
160 Interpreter(InterpreterError),
161 #[error("challenge parsing error: {0}")]
162 Parse(ChallengeParseError),
163 #[error("challenge submission failed: {0}")]
164 Submission(ChallengeExecutionError),
165 #[error(
166 "cloudflare returned invalid response (status 400) - challenge answer may be incorrect"
167 )]
168 InvalidResponse,
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::challenges::core::ChallengeHttpClientError;
175 use async_trait::async_trait;
176 use http::{HeaderMap, Method, header::SERVER};
177 use std::sync::Mutex;
178 use url::Url;
179
180 struct StubInterpreter;
181
182 impl JavascriptInterpreter for StubInterpreter {
183 fn solve_challenge(
184 &self,
185 _page_html: &str,
186 _host: &str,
187 ) -> Result<String, InterpreterError> {
188 Ok("42".into())
189 }
190 }
191
192 struct ResponseFixture {
193 url: Url,
194 headers: HeaderMap,
195 method: Method,
196 body: String,
197 status: u16,
198 }
199
200 impl ResponseFixture {
201 fn new(body: &str, status: u16) -> Self {
202 let mut headers = HeaderMap::new();
203 headers.insert(SERVER, "cloudflare".parse().unwrap());
204 Self {
205 url: Url::parse("https://example.com/").unwrap(),
206 headers,
207 method: Method::GET,
208 body: body.to_string(),
209 status,
210 }
211 }
212
213 fn response(&self) -> ChallengeResponse<'_> {
214 ChallengeResponse {
215 url: &self.url,
216 status: self.status,
217 headers: &self.headers,
218 body: &self.body,
219 request_method: &self.method,
220 }
221 }
222
223 fn url(&self) -> &Url {
224 &self.url
225 }
226 }
227
228 #[test]
229 fn solve_extracts_payload() {
230 let html = r#"
231 <html>
232 <body>
233 <form id='challenge-form' action='/cdn-cgi/l/chk_jschl?__cf_chl_f_tk=foo' method='POST'>
234 <input type='hidden' name='r' value='abc'/>
235 <input type='hidden' name='jschl_vc' value='def'/>
236 <input type='hidden' name='pass' value='ghi'/>
237 </form>
238 <script>setTimeout(function(){ submit();
239 }, 4000);</script>
240 <script src='/cdn-cgi/images/trace/jsch/'></script>
241 </body>
242 </html>
243 "#;
244
245 let solver = JavascriptV1Solver::new(Arc::new(StubInterpreter));
246 let fixture = ResponseFixture::new(html, 503);
247 let resp = fixture.response();
248 assert!(solver.is_iuam_challenge(&resp));
249 let submission = solver.solve(&resp).unwrap();
250 assert_eq!(submission.method, Method::POST);
251 assert_eq!(
252 submission.form_fields.get("jschl_answer"),
253 Some(&"42".to_string())
254 );
255 assert_eq!(submission.wait, Duration::from_millis(4000));
256 }
257
258 struct StubClient {
259 responses: Mutex<Vec<ChallengeHttpResponse>>,
260 }
261
262 impl StubClient {
263 fn new(responses: Vec<ChallengeHttpResponse>) -> Self {
264 Self {
265 responses: Mutex::new(responses.into_iter().rev().collect()),
266 }
267 }
268
269 fn pop_response(&self) -> ChallengeHttpResponse {
270 self.responses
271 .lock()
272 .unwrap()
273 .pop()
274 .expect("no more stub responses")
275 }
276 }
277
278 #[async_trait]
279 impl ChallengeHttpClient for StubClient {
280 async fn send_form(
281 &self,
282 _method: &Method,
283 _url: &Url,
284 _headers: &http::HeaderMap,
285 _form_fields: &std::collections::HashMap<String, String>,
286 _allow_redirects: bool,
287 ) -> Result<ChallengeHttpResponse, ChallengeHttpClientError> {
288 Ok(self.pop_response())
289 }
290
291 async fn send_with_body(
292 &self,
293 _method: &Method,
294 _url: &Url,
295 _headers: &http::HeaderMap,
296 _body: Option<&[u8]>,
297 _allow_redirects: bool,
298 ) -> Result<ChallengeHttpResponse, ChallengeHttpClientError> {
299 Ok(self.pop_response())
300 }
301 }
302
303 #[tokio::test]
304 async fn solve_and_submit_executes_challenge() {
305 let solver = JavascriptV1Solver::new(Arc::new(StubInterpreter));
306 let html = r#"
307 <html>
308 <body>
309 <form id='challenge-form' action='/cdn-cgi/l/chk_jschl?__cf_chl_f_tk=foo' method='POST'>
310 <input type='hidden' name='r' value='abc'/>
311 <input type='hidden' name='jschl_vc' value='def'/>
312 <input type='hidden' name='pass' value='ghi'/>
313 </form>
314 <script>setTimeout(function(){ submit();
315 }, 0);</script>
316 <script src='/cdn-cgi/images/trace/jsch/'></script>
317 </body>
318 </html>
319 "#;
320 let fixture = ResponseFixture::new(html, 503);
321 let response = fixture.response();
322 let original = OriginalRequest::new(Method::GET, fixture.url().clone());
323
324 let client = Arc::new(StubClient::new(vec![ChallengeHttpResponse {
325 status: 200,
326 headers: HeaderMap::new(),
327 body: Vec::new(),
328 url: Url::parse("https://example.com/success").unwrap(),
329 is_redirect: false,
330 }]));
331
332 let result = solver
333 .solve_and_submit(client, &response, original)
334 .await
335 .unwrap();
336
337 assert_eq!(result.status, 200);
338 }
339
340 #[test]
341 fn detects_new_iuam_challenge() {
342 let html = r#"
343 <html>
344 <body>
345 <form id='challenge-form' action='/cdn-cgi/l/chk_jschl?__cf_chl_f_tk=foo' method='POST'>
346 <input type='hidden' name='r' value='abc'/>
347 <input type='hidden' name='jschl_vc' value='def'/>
348 <input type='hidden' name='pass' value='ghi'/>
349 </form>
350 <script>
351 cpo.src = '/cdn-cgi/challenge-platform/h/g/orchestrate/jsch/v1?ray=abc';
352 </script>
353 <script>setTimeout(function(){ submit();
354 }, 4000);</script>
355 <script src='/cdn-cgi/images/trace/jsch/'></script>
356 </body>
357 </html>
358 "#;
359
360 let solver = JavascriptV1Solver::new(Arc::new(StubInterpreter));
361 let fixture = ResponseFixture::new(html, 503);
362 let resp = fixture.response();
363
364 assert!(solver.is_iuam_challenge(&resp));
365 assert!(solver.is_new_iuam_challenge(&resp));
366 }
367
368 #[test]
369 fn detects_old_iuam_challenge_not_new() {
370 let html = r#"
371 <html>
372 <body>
373 <form id='challenge-form' action='/cdn-cgi/l/chk_jschl?__cf_chl_f_tk=foo' method='POST'>
374 <input type='hidden' name='r' value='abc'/>
375 <input type='hidden' name='jschl_vc' value='def'/>
376 <input type='hidden' name='pass' value='ghi'/>
377 </form>
378 <script>setTimeout(function(){ submit();
379 }, 4000);</script>
380 <script src='/cdn-cgi/images/trace/jsch/'></script>
381 </body>
382 </html>
383 "#;
384
385 let solver = JavascriptV1Solver::new(Arc::new(StubInterpreter));
386 let fixture = ResponseFixture::new(html, 503);
387 let resp = fixture.response();
388
389 assert!(solver.is_iuam_challenge(&resp));
390 assert!(!solver.is_new_iuam_challenge(&resp));
391 }
392}