1use anyhow::{anyhow, Result};
12use chromiumoxide::cdp::js_protocol::runtime::EvaluateParams;
13use chromiumoxide::Page;
14
15fn escape_js_string(s: &str) -> String {
27 let mut out = String::with_capacity(s.len());
28 for ch in s.chars() {
29 match ch {
30 '\\' => out.push_str("\\\\"),
31 '\'' => out.push_str("\\'"),
32 '"' => out.push_str("\\\""),
33 '\n' => out.push_str("\\n"),
34 '\r' => out.push_str("\\r"),
35 '\t' => out.push_str("\\t"),
36 '\0' => out.push_str("\\0"),
37 c => out.push(c),
38 }
39 }
40 out
41}
42
43fn lookup_iframe_offset(
49 iframe_offsets: &[(usize, String, String, f64, f64)],
50 url: &str,
51 iframe_idx: i64,
52) -> (f64, f64) {
53 if iframe_idx >= 0 {
54 iframe_offsets
55 .iter()
56 .find(|(idx, src, id, _, _)| *idx == iframe_idx as usize && (src == url || id == url))
57 .map(|(_, _, _, x, y)| (*x, *y))
58 } else {
59 iframe_offsets
60 .iter()
61 .find(|(_, src, id, _, _)| src == url || id == url)
62 .map(|(_, _, _, x, y)| (*x, *y))
63 }
64 .unwrap_or((0.0, 0.0))
65}
66
67pub async fn evaluate_in_all_frames<T>(page: &Page, expression: &str) -> Result<Vec<T>>
83where
84 T: serde::de::DeserializeOwned,
85{
86 let frame_ids = page.frames().await?;
87 let mut out = Vec::with_capacity(frame_ids.len());
88 for fid in frame_ids {
89 if let Some(ctx) = page.frame_execution_context(fid.clone()).await? {
90 let params = EvaluateParams::builder()
91 .expression(expression)
92 .context_id(ctx)
93 .build()
94 .map_err(|e| anyhow!("EvaluateParams build failed: {e}"))?;
95 let eval = page.evaluate_expression(params).await?;
96 if let Ok(v) = eval.into_value::<T>() {
97 out.push(v);
98 }
99 }
100 }
101 Ok(out)
102}
103
104pub async fn evaluate_in_frames_first<T, F>(
108 page: &Page,
109 expression: &str,
110 filter: F,
111 default: T,
112) -> Result<T>
113where
114 T: serde::de::DeserializeOwned + Clone,
115 F: Fn(&T) -> bool,
116{
117 let all = evaluate_in_all_frames::<T>(page, expression).await?;
118 Ok(all.into_iter().find(filter).unwrap_or(default))
119}
120
121pub async fn find_element_centre_in_frames(
140 page: &Page,
141 selector: &str,
142) -> Result<Option<(f64, f64)>> {
143 let frame_ids = page.frames().await?;
144 let main_frame = page.mainframe().await?;
145
146 let mut iframe_offsets: Vec<(usize, String, String, f64, f64)> = Vec::new();
150 if let Some(ref main) = main_frame {
151 let js = r#"
152 (function() {
153 const out = [];
154 const frames = document.querySelectorAll('iframe');
155 for (let i = 0; i < frames.length; i++) {
156 const f = frames[i];
157 const r = f.getBoundingClientRect();
158 out.push({ idx: i, src: f.src, id: f.id, x: r.left, y: r.top });
159 }
160 return out;
161 })()
162 "#;
163 if let Some(ctx) = page.frame_execution_context(main.clone()).await? {
164 let params = EvaluateParams::builder()
165 .expression(js)
166 .context_id(ctx)
167 .build()
168 .map_err(|e| anyhow!("EvaluateParams build failed: {e}"))?;
169 let eval = page.evaluate_expression(params).await?;
170 if let Ok(vals) = eval.into_value::<Vec<serde_json::Value>>() {
171 for v in vals {
172 if let (Some(idx), Some(x), Some(y)) =
173 (v["idx"].as_u64(), v["x"].as_f64(), v["y"].as_f64())
174 {
175 let src = v["src"].as_str().unwrap_or("").to_string();
176 let id = v["id"].as_str().unwrap_or("").to_string();
177 iframe_offsets.push((idx as usize, src, id, x, y));
178 }
179 }
180 }
181 }
182 }
183
184 let escaped = escape_js_string(selector);
185 let js = format!(
186 r#"(function() {{
187 const el = document.querySelector('{}');
188 if (!el) return null;
189 const r = el.getBoundingClientRect();
190 let iframeIdx = -1;
191 try {{
192 const frames = window.parent.frames;
193 for (let i = 0; i < frames.length; i++) {{
194 if (frames[i] === window) {{
195 iframeIdx = i;
196 break;
197 }}
198 }}
199 }} catch (e) {{}}
200 return {{ x: r.left + r.width / 2, y: r.top + r.height / 2, url: window.location.href, iframeIdx: iframeIdx }};
201 }})()"#,
202 escaped
203 );
204
205 for fid in frame_ids {
206 if let Some(ctx) = page.frame_execution_context(fid.clone()).await? {
207 let params = EvaluateParams::builder()
208 .expression(&js)
209 .context_id(ctx)
210 .build()
211 .map_err(|e| anyhow!("EvaluateParams build failed: {e}"))?;
212 let eval = page.evaluate_expression(params).await?;
213 if let Ok(val) = eval.into_value::<serde_json::Value>() {
214 if let (Some(x), Some(y)) = (val["x"].as_f64(), val["y"].as_f64()) {
215 let url = val["url"].as_str().unwrap_or("");
216 let iframe_idx = val["iframeIdx"].as_i64().unwrap_or(-1);
217 let (offset_x, offset_y) = if Some(&fid) == main_frame.as_ref() {
218 (0.0, 0.0)
219 } else {
220 lookup_iframe_offset(&iframe_offsets, url, iframe_idx)
221 };
222 return Ok(Some((x + offset_x, y + offset_y)));
223 }
224 }
225 }
226 }
227 Ok(None)
228}
229
230pub async fn verify_token_in_frames(page: &Page, token_input_name: &str) -> Result<bool> {
244 let escaped = escape_js_string(token_input_name);
245 let js = format!(
246 r#"!!document.querySelector('input[name="{}"][value]:not([value=""])')"#,
247 escaped
248 );
249 let results = evaluate_in_all_frames::<bool>(page, &js).await?;
250 Ok(results.into_iter().any(|v| v))
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn escape_js_string_all_special_chars() {
259 let input = "\\'\"\n\r\t\0";
260 assert_eq!(escape_js_string(input), "\\\\\\\'\\\"\\n\\r\\t\\0");
261 }
262
263 #[test]
264 fn escape_js_string_backslash() {
265 assert_eq!(escape_js_string(r"\"), "\\\\");
266 }
267
268 #[test]
269 fn escape_js_string_single_quote() {
270 assert_eq!(escape_js_string("'"), "\\'");
271 }
272
273 #[test]
274 fn escape_js_string_double_quote() {
275 assert_eq!(escape_js_string("\""), "\\\"");
276 }
277
278 #[test]
279 fn escape_js_string_newline() {
280 assert_eq!(escape_js_string("a\nb"), "a\\nb");
281 }
282
283 #[test]
284 fn escape_js_string_carriage_return() {
285 assert_eq!(escape_js_string("a\rb"), "a\\rb");
286 }
287
288 #[test]
289 fn escape_js_string_tab() {
290 assert_eq!(escape_js_string("a\tb"), "a\\tb");
291 }
292
293 #[test]
294 fn escape_js_string_null_byte() {
295 assert_eq!(escape_js_string("a\0b"), "a\\0b");
296 }
297
298 #[test]
299 fn escape_js_string_mixed() {
300 let input = "line1\nline2\tcol\0end\\\"'";
301 assert_eq!(
302 escape_js_string(input),
303 "line1\\nline2\\tcol\\0end\\\\\\\"\\'"
304 );
305 }
306
307 #[test]
308 fn escape_js_string_no_special_chars() {
309 assert_eq!(escape_js_string("#simple-id"), "#simple-id");
310 }
311
312 #[test]
313 fn lookup_iframe_offset_by_index_and_url() {
314 let offsets = vec![
315 (0, "a.html".into(), "".into(), 10.0, 20.0),
316 (1, "b.html".into(), "".into(), 30.0, 40.0),
317 ];
318 assert_eq!(lookup_iframe_offset(&offsets, "a.html", 0), (10.0, 20.0));
319 assert_eq!(lookup_iframe_offset(&offsets, "b.html", 1), (30.0, 40.0));
320 }
321
322 #[test]
323 fn lookup_iframe_offset_fallback_when_index_missing() {
324 let offsets = vec![(0, "a.html".into(), "".into(), 10.0, 20.0)];
325 assert_eq!(lookup_iframe_offset(&offsets, "a.html", -1), (10.0, 20.0));
326 }
327
328 #[test]
329 fn lookup_iframe_offset_disambiguates_duplicate_src() {
330 let offsets = vec![
331 (0, "same.html".into(), "".into(), 10.0, 20.0),
332 (1, "same.html".into(), "".into(), 30.0, 40.0),
333 ];
334 assert_eq!(lookup_iframe_offset(&offsets, "same.html", 0), (10.0, 20.0));
336 assert_eq!(lookup_iframe_offset(&offsets, "same.html", 1), (30.0, 40.0));
337 assert_eq!(
339 lookup_iframe_offset(&offsets, "same.html", -1),
340 (10.0, 20.0)
341 );
342 }
343
344 #[test]
345 fn lookup_iframe_offset_empty_src_and_id() {
346 let offsets = vec![
347 (0, "".into(), "".into(), 5.0, 5.0),
348 (1, "".into(), "".into(), 15.0, 15.0),
349 ];
350 assert_eq!(lookup_iframe_offset(&offsets, "", 0), (5.0, 5.0));
351 assert_eq!(lookup_iframe_offset(&offsets, "", 1), (15.0, 15.0));
352 }
353
354 #[test]
355 fn lookup_iframe_offset_no_match() {
356 let offsets = vec![(0, "a.html".into(), "".into(), 10.0, 20.0)];
357 assert_eq!(
358 lookup_iframe_offset(&offsets, "missing.html", -1),
359 (0.0, 0.0)
360 );
361 }
362
363 #[test]
364 fn find_element_js_contains_query_selector() {
365 let selector = "#btn";
366 let escaped = escape_js_string(selector);
367 let js = format!(
368 r#"(function() {{ const el = document.querySelector('{}'); if (!el) return null; const r = el.getBoundingClientRect(); return {{ x: r.left + r.width / 2, y: r.top + r.height / 2, url: window.location.href }}; }})()"#,
369 escaped
370 );
371 assert!(js.contains("document.querySelector"));
372 assert!(js.contains("getBoundingClientRect"));
373 }
374
375 #[test]
376 fn verify_token_js_contains_input_selector() {
377 let name = "g-recaptcha-response";
378 let escaped = escape_js_string(name);
379 let js = format!(
380 r#"!!document.querySelector('input[name="{}"][value]:not([value=""])')"#,
381 escaped
382 );
383 assert!(js.contains("input[name="));
384 assert!(js.contains("value]:not([value=\"\"])"));
385 }
386
387 #[test]
388 fn verify_token_escapes_quotes() {
389 let name = r#"token"value"#;
390 let escaped = escape_js_string(name);
391 assert!(escaped.contains("\\\""));
392 for (i, ch) in escaped.char_indices() {
393 if ch == '"' {
394 assert!(
395 i > 0 && escaped.as_bytes()[i - 1] == b'\\',
396 "quote at {} not escaped",
397 i
398 );
399 }
400 }
401 }
402
403 #[test]
404 fn verify_token_escapes_null_and_newline() {
405 let name = "token\0value\n";
406 let escaped = escape_js_string(name);
407 assert!(escaped.contains("\\0"));
408 assert!(escaped.contains("\\n"));
409 assert!(!escaped.contains('\0'));
410 assert!(!escaped.contains('\n'));
411 }
412}