use anyhow::{anyhow, Result};
use chromiumoxide::cdp::js_protocol::runtime::EvaluateParams;
use chromiumoxide::Page;
fn escape_js_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("\\'"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\0' => out.push_str("\\0"),
c => out.push(c),
}
}
out
}
fn lookup_iframe_offset(
iframe_offsets: &[(usize, String, String, f64, f64)],
url: &str,
iframe_idx: i64,
) -> (f64, f64) {
if iframe_idx >= 0 {
iframe_offsets
.iter()
.find(|(idx, src, id, _, _)| *idx == iframe_idx as usize && (src == url || id == url))
.map(|(_, _, _, x, y)| (*x, *y))
} else {
iframe_offsets
.iter()
.find(|(_, src, id, _, _)| src == url || id == url)
.map(|(_, _, _, x, y)| (*x, *y))
}
.unwrap_or((0.0, 0.0))
}
pub async fn evaluate_in_all_frames<T>(page: &Page, expression: &str) -> Result<Vec<T>>
where
T: serde::de::DeserializeOwned,
{
let frame_ids = page.frames().await?;
let mut out = Vec::with_capacity(frame_ids.len());
for fid in frame_ids {
if let Some(ctx) = page.frame_execution_context(fid.clone()).await? {
let params = EvaluateParams::builder()
.expression(expression)
.context_id(ctx)
.build()
.map_err(|e| anyhow!("EvaluateParams build failed: {e}"))?;
let eval = page.evaluate_expression(params).await?;
if let Ok(v) = eval.into_value::<T>() {
out.push(v);
}
}
}
Ok(out)
}
pub async fn evaluate_in_frames_first<T, F>(
page: &Page,
expression: &str,
filter: F,
default: T,
) -> Result<T>
where
T: serde::de::DeserializeOwned + Clone,
F: Fn(&T) -> bool,
{
let all = evaluate_in_all_frames::<T>(page, expression).await?;
Ok(all.into_iter().find(filter).unwrap_or(default))
}
pub async fn find_element_centre_in_frames(
page: &Page,
selector: &str,
) -> Result<Option<(f64, f64)>> {
let frame_ids = page.frames().await?;
let main_frame = page.mainframe().await?;
let mut iframe_offsets: Vec<(usize, String, String, f64, f64)> = Vec::new();
if let Some(ref main) = main_frame {
let js = r#"
(function() {
const out = [];
const frames = document.querySelectorAll('iframe');
for (let i = 0; i < frames.length; i++) {
const f = frames[i];
const r = f.getBoundingClientRect();
out.push({ idx: i, src: f.src, id: f.id, x: r.left, y: r.top });
}
return out;
})()
"#;
if let Some(ctx) = page.frame_execution_context(main.clone()).await? {
let params = EvaluateParams::builder()
.expression(js)
.context_id(ctx)
.build()
.map_err(|e| anyhow!("EvaluateParams build failed: {e}"))?;
let eval = page.evaluate_expression(params).await?;
if let Ok(vals) = eval.into_value::<Vec<serde_json::Value>>() {
for v in vals {
if let (Some(idx), Some(x), Some(y)) =
(v["idx"].as_u64(), v["x"].as_f64(), v["y"].as_f64())
{
let src = v["src"].as_str().unwrap_or("").to_string();
let id = v["id"].as_str().unwrap_or("").to_string();
iframe_offsets.push((idx as usize, src, id, x, y));
}
}
}
}
}
let escaped = escape_js_string(selector);
let js = format!(
r#"(function() {{
const el = document.querySelector('{}');
if (!el) return null;
const r = el.getBoundingClientRect();
let iframeIdx = -1;
try {{
const frames = window.parent.frames;
for (let i = 0; i < frames.length; i++) {{
if (frames[i] === window) {{
iframeIdx = i;
break;
}}
}}
}} catch (e) {{}}
return {{ x: r.left + r.width / 2, y: r.top + r.height / 2, url: window.location.href, iframeIdx: iframeIdx }};
}})()"#,
escaped
);
for fid in frame_ids {
if let Some(ctx) = page.frame_execution_context(fid.clone()).await? {
let params = EvaluateParams::builder()
.expression(&js)
.context_id(ctx)
.build()
.map_err(|e| anyhow!("EvaluateParams build failed: {e}"))?;
let eval = page.evaluate_expression(params).await?;
if let Ok(val) = eval.into_value::<serde_json::Value>() {
if let (Some(x), Some(y)) = (val["x"].as_f64(), val["y"].as_f64()) {
let url = val["url"].as_str().unwrap_or("");
let iframe_idx = val["iframeIdx"].as_i64().unwrap_or(-1);
let (offset_x, offset_y) = if Some(&fid) == main_frame.as_ref() {
(0.0, 0.0)
} else {
lookup_iframe_offset(&iframe_offsets, url, iframe_idx)
};
return Ok(Some((x + offset_x, y + offset_y)));
}
}
}
}
Ok(None)
}
pub async fn verify_token_in_frames(page: &Page, token_input_name: &str) -> Result<bool> {
let escaped = escape_js_string(token_input_name);
let js = format!(
r#"!!document.querySelector('input[name="{}"][value]:not([value=""])')"#,
escaped
);
let results = evaluate_in_all_frames::<bool>(page, &js).await?;
Ok(results.into_iter().any(|v| v))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_js_string_all_special_chars() {
let input = "\\'\"\n\r\t\0";
assert_eq!(escape_js_string(input), "\\\\\\\'\\\"\\n\\r\\t\\0");
}
#[test]
fn escape_js_string_backslash() {
assert_eq!(escape_js_string(r"\"), "\\\\");
}
#[test]
fn escape_js_string_single_quote() {
assert_eq!(escape_js_string("'"), "\\'");
}
#[test]
fn escape_js_string_double_quote() {
assert_eq!(escape_js_string("\""), "\\\"");
}
#[test]
fn escape_js_string_newline() {
assert_eq!(escape_js_string("a\nb"), "a\\nb");
}
#[test]
fn escape_js_string_carriage_return() {
assert_eq!(escape_js_string("a\rb"), "a\\rb");
}
#[test]
fn escape_js_string_tab() {
assert_eq!(escape_js_string("a\tb"), "a\\tb");
}
#[test]
fn escape_js_string_null_byte() {
assert_eq!(escape_js_string("a\0b"), "a\\0b");
}
#[test]
fn escape_js_string_mixed() {
let input = "line1\nline2\tcol\0end\\\"'";
assert_eq!(
escape_js_string(input),
"line1\\nline2\\tcol\\0end\\\\\\\"\\'"
);
}
#[test]
fn escape_js_string_no_special_chars() {
assert_eq!(escape_js_string("#simple-id"), "#simple-id");
}
#[test]
fn lookup_iframe_offset_by_index_and_url() {
let offsets = vec![
(0, "a.html".into(), "".into(), 10.0, 20.0),
(1, "b.html".into(), "".into(), 30.0, 40.0),
];
assert_eq!(lookup_iframe_offset(&offsets, "a.html", 0), (10.0, 20.0));
assert_eq!(lookup_iframe_offset(&offsets, "b.html", 1), (30.0, 40.0));
}
#[test]
fn lookup_iframe_offset_fallback_when_index_missing() {
let offsets = vec![(0, "a.html".into(), "".into(), 10.0, 20.0)];
assert_eq!(lookup_iframe_offset(&offsets, "a.html", -1), (10.0, 20.0));
}
#[test]
fn lookup_iframe_offset_disambiguates_duplicate_src() {
let offsets = vec![
(0, "same.html".into(), "".into(), 10.0, 20.0),
(1, "same.html".into(), "".into(), 30.0, 40.0),
];
assert_eq!(lookup_iframe_offset(&offsets, "same.html", 0), (10.0, 20.0));
assert_eq!(lookup_iframe_offset(&offsets, "same.html", 1), (30.0, 40.0));
assert_eq!(
lookup_iframe_offset(&offsets, "same.html", -1),
(10.0, 20.0)
);
}
#[test]
fn lookup_iframe_offset_empty_src_and_id() {
let offsets = vec![
(0, "".into(), "".into(), 5.0, 5.0),
(1, "".into(), "".into(), 15.0, 15.0),
];
assert_eq!(lookup_iframe_offset(&offsets, "", 0), (5.0, 5.0));
assert_eq!(lookup_iframe_offset(&offsets, "", 1), (15.0, 15.0));
}
#[test]
fn lookup_iframe_offset_no_match() {
let offsets = vec![(0, "a.html".into(), "".into(), 10.0, 20.0)];
assert_eq!(
lookup_iframe_offset(&offsets, "missing.html", -1),
(0.0, 0.0)
);
}
#[test]
fn find_element_js_contains_query_selector() {
let selector = "#btn";
let escaped = escape_js_string(selector);
let js = format!(
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 }}; }})()"#,
escaped
);
assert!(js.contains("document.querySelector"));
assert!(js.contains("getBoundingClientRect"));
}
#[test]
fn verify_token_js_contains_input_selector() {
let name = "g-recaptcha-response";
let escaped = escape_js_string(name);
let js = format!(
r#"!!document.querySelector('input[name="{}"][value]:not([value=""])')"#,
escaped
);
assert!(js.contains("input[name="));
assert!(js.contains("value]:not([value=\"\"])"));
}
#[test]
fn verify_token_escapes_quotes() {
let name = r#"token"value"#;
let escaped = escape_js_string(name);
assert!(escaped.contains("\\\""));
for (i, ch) in escaped.char_indices() {
if ch == '"' {
assert!(
i > 0 && escaped.as_bytes()[i - 1] == b'\\',
"quote at {} not escaped",
i
);
}
}
}
#[test]
fn verify_token_escapes_null_and_newline() {
let name = "token\0value\n";
let escaped = escape_js_string(name);
assert!(escaped.contains("\\0"));
assert!(escaped.contains("\\n"));
assert!(!escaped.contains('\0'));
assert!(!escaped.contains('\n'));
}
}