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