use chaser_oxide::{Browser, BrowserConfig, ChaserPage, ChaserProfile, Gpu}; use chaser_oxide::handler::viewport::Viewport; use chaser_oxide::cdp::browser_protocol::network::{EnableParams, EventResponseReceived, GetResponseBodyParams}; use chaser_oxide::cdp::browser_protocol::emulation::{SetDeviceMetricsOverrideParams, SetGeolocationOverrideParams}; use chaser_oxide::cdp::browser_protocol::page::CaptureScreenshotParams; use futures::StreamExt; use serde::{Deserialize, Serialize}; use std::convert::Infallible; use std::env; use std::net::SocketAddr; use std::sync::Arc; use tokio::sync::Mutex; use warp::Filter; type SharedBrowser = Arc>; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct AdvancedAction { r#type: String, selector: Option, value: Option, file_name: Option, full: Option, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct Payload { width: Option, height: Option, sniff_network: Option, actions: Vec, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct StandardResponse { logs: Vec, result: serde_json::Value, error: Option, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct MemoryStats { total_system: String, free_system: String, allocated_by_api: String, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct StatsResponse { uptime: String, engine_version: String, system_load_average: String, active_tabs: usize, memory: MemoryStats, } #[derive(Serialize, Clone)] #[serde(rename_all = "camelCase")] struct SniffedActivity { url: String, method: String, response_status: i32, response_body: String, } fn format_uptime(secs: u64) -> String { let hours = secs / 3600; let minutes = (secs % 3600) / 60; let seconds = secs % 60; format!("{}h {}m {}s", hours, minutes, seconds) } fn format_size(bytes: u64) -> String { if bytes >= 1073741824 { format!("{:.2} GB", bytes as f64 / 1073741824.0) } else { format!("{:.2} MB", bytes as f64 / 1048576.0) } } fn get_current_memory_bytes() -> u64 { if let Ok(statm) = std::fs::read_to_string("/proc/self/statm") { if let Some(first_val) = statm.split_whitespace().next() { if let Ok(pages) = first_val.parse::() { return pages * 4096; } } } 0 } fn get_system_memory_bytes() -> (u64, u64) { let mut total = 0; let mut free = 0; if let Ok(meminfo) = std::fs::read_to_string("/proc/meminfo") { for line in meminfo.lines() { if line.starts_with("MemTotal:") { total = line.split_whitespace().nth(1).unwrap_or("0").parse::().unwrap_or(0) * 1024; } else if line.starts_with("MemAvailable:") || line.starts_with("MemFree:") { free = line.split_whitespace().nth(1).unwrap_or("0").parse::().unwrap_or(0) * 1024; if line.starts_with("MemAvailable:") { break; } } } } (total, free) } fn get_system_load() -> String { if let Ok(loadavg) = std::fs::read_to_string("/proc/loadavg") { let parts: Vec<&str> = loadavg.split_whitespace().take(3).collect(); if parts.len() == 3 { return format!("{} {} {}", parts[0], parts[1], parts[2]); } } "0.00 0.00 0.00".to_string() } #[tokio::main] async fn main() -> anyhow::Result<()> { let start_time = std::time::Instant::now(); let port_str = env::var("PORT").unwrap_or_else(|_| "7860".to_string()); let port: u16 = port_str.parse().unwrap_or(7860); let chrome_bin = env::var("CHROME_PATH").unwrap_or_else(|_| "/usr/bin/google-chrome-stable".to_string()); let config = BrowserConfig::builder() .chrome_executable(chrome_bin) .no_sandbox() .new_headless_mode() .arg("--disable-gpu") .arg("--disable-dev-shm-usage") .arg("--disable-gl-drawing-for-tests") .arg("--disable-software-rasterizer") .arg("--no-zygote") .arg("--single-process") .arg("--disable-blink-features=AutomationControlled") .arg("--user-data-dir=/tmp/isolated_chrome_profile") .arg("--disable-infobars") .arg("--password-store=basic") .arg("--use-mock-keychain") .viewport(Viewport { width: 1366, height: 768, ..Default::default() }) .build() .map_err(|e| anyhow::anyhow!(e))?; let (browser, mut handler) = Browser::launch(config).await?; tokio::spawn(async move { while let Some(_) = handler.next().await {} }); let shared_browser = Arc::new(Mutex::new(browser)); let execute_browser_filter = shared_browser.clone(); let execute_route = warp::post() .and(warp::path("execute")) .and(warp::body::json()) .and(warp::any().map(move || execute_browser_filter.clone())) .and_then(handle_execute); let stats_browser_filter = shared_browser.clone(); let stats_route = warp::get() .and(warp::path::end()) .and(warp::any().map(move || stats_browser_filter.clone())) .and_then(move |b| handle_stats(start_time, b)); let tmp_dir = env::temp_dir(); let files_route = warp::path("tmp").and(warp::fs::dir(tmp_dir.clone())); let routes = execute_route.or(stats_route).or(files_route); let addr: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap(); warp::serve(routes).run(addr).await; Ok(()) } async fn handle_stats(start_time: std::time::Instant, browser_info: SharedBrowser) -> Result { let (total_mem, free_mem) = get_system_memory_bytes(); let api_mem = get_current_memory_bytes(); let pkg_version = format!("rust-warp-v{}", env!("CARGO_PKG_VERSION")); let total_tabs = { let browser = browser_info.lock().await; if let Ok(pages) = browser.pages().await { pages.len() } else { 1 } }; Ok(warp::reply::json(&StatsResponse { uptime: format_uptime(start_time.elapsed().as_secs()), engine_version: pkg_version, system_load_average: get_system_load(), active_tabs: total_tabs, memory: MemoryStats { total_system: format_size(total_mem), free_system: format_size(free_mem), allocated_by_api: format_size(api_mem), }, })) } async fn handle_execute(payload: Payload, browser_info: SharedBrowser) -> Result { let mut logs = Vec::new(); let mut screenshots = Vec::new(); let mut evals = Vec::new(); let network_activities = Arc::new(Mutex::new(Vec::::new())); let width = payload.width.unwrap_or(1366); let height = payload.height.unwrap_or(768); let first_url = payload.actions.iter() .find(|act| act.r#type == "navigate") .and_then(|act| act.value.clone()) .unwrap_or_else(|| "about:blank".to_string()); let browser = browser_info.lock().await; let page = browser.new_page(&first_url).await.unwrap(); let _ = page.execute(SetDeviceMetricsOverrideParams::builder() .width(width as i32) .height(height as i32) .device_scale_factor(1.0) .mobile(false) .build() .unwrap() ).await; if payload.sniff_network.unwrap_or(false) { let _ = page.execute(EnableParams::default()).await; let mut response_rcvd_stream = page.event_listener::().await.unwrap(); let net_logs_clone = network_activities.clone(); let page_clone = page.clone(); tokio::spawn(async move { while let Some(ev) = response_rcvd_stream.next().await { let req_id = ev.request_id.clone(); let url = ev.response.url.clone(); let status = ev.response.status; if url.contains("json") || url.contains("api") { if let Ok(body_res) = page_clone.execute(GetResponseBodyParams { request_id: req_id }).await { let mut lock = net_logs_clone.lock().await; lock.push(SniffedActivity { url, method: "GET/POST".to_string(), response_status: status as i32, response_body: body_res.body.clone(), }); } } } }); } let chaser = ChaserPage::new(page.clone()); let profile = ChaserProfile::windows() .chrome_version(131) .gpu(Gpu::NvidiaRTX3080) .memory_gb(16) .cpu_cores(8) .locale("en-US") .timezone("America/New_York") .screen(width, height) .build(); chaser.apply_profile(&profile).await.unwrap(); let os_tmp_dir = env::temp_dir(); let mut turnstile_sitekey = String::new(); let mut turnstile_token = String::new(); for (i, act) in payload.actions.iter().enumerate() { let mut log_msg = format!("Step {}: [{}]", i + 1, act.r#type); if let Some(sel) = &act.selector { log_msg.push_str(&format!(" -> Target: {}", sel)); } gather_logs(&mut logs, log_msg); let inner_page = chaser.raw_page(); match act.r#type.as_str() { "navigate" => { if first_url != "about:blank" && i == 0 { continue; } if let Some(url) = &act.value { chaser.goto(url).await.unwrap(); } } "wait" => { if let Some(sel) = &act.selector { let _ = inner_page.find_element(sel).await.unwrap(); } } "click" => { if let Some(sel) = &act.selector { if let Ok(el) = inner_page.find_element(sel).await { el.click().await.unwrap(); } } } "type" => { if let Some(sel) = &act.selector { if let Some(val) = &act.value { if let Ok(el) = inner_page.find_element(sel).await { el.type_str(val).await.unwrap(); } } } } "clear" => { if let Some(sel) = &act.selector { let js_clear = format!("(() => {{ const el = document.querySelector('{}'); if(el) {{ el.value = ''; el.dispatchEvent(new Event('input', {{bubbles:true}})); }} }})()", sel); let _: Option = chaser.evaluate(&js_clear).await.unwrap(); } } "hover" => { if let Some(sel) = &act.selector { if let Ok(el) = inner_page.find_element(sel).await { el.hover().await.unwrap(); } } } "select" => { if let Some(sel) = &act.selector { if let Some(val) = &act.value { let js_select = format!("(() => {{ const el = document.querySelector('{}'); if(el) {{ el.value = '{}'; el.dispatchEvent(new Event('change', {{bubbles:true}})); }} }})()", sel, val); let _: Option = chaser.evaluate(&js_select).await.unwrap(); } } } "key" => { if let Some(val) = &act.value { let js_input = format!( "(() => {{ const activeEl = document.activeElement; if (activeEl) {{ activeEl.value += '{}'; activeEl.dispatchEvent(new Event('input', {{ bubbles: true }})); activeEl.dispatchEvent(new Event('change', {{ bubbles: true }})); }} }})()", val ); let _: Option = chaser.evaluate(&js_input).await.unwrap(); } } "scroll" => { if let Some(sel) = &act.selector { if let Ok(el) = inner_page.find_element(sel).await { el.scroll_into_view().await.unwrap(); } } else if let Some(val) = &act.value { if let Ok(val_int) = val.parse::() { let js_scroll = format!("window.scrollTo(0, {});", val_int); let _: Option = chaser.evaluate(&js_scroll).await.unwrap(); } } } "setLocation" => { if let Some(val) = &act.value { let coords: Vec<&str> = val.split(',').collect(); if coords.len() == 2 { if let (Ok(lat), Ok(long)) = (coords[0].parse::(), coords[1].parse::()) { let _ = inner_page.execute(SetGeolocationOverrideParams::builder() .latitude(lat) .longitude(long) .accuracy(100.0) .build() ).await; } } } } "extractTurnstile" => { let harvest_script = r#" (() => { const iframe = document.querySelector('iframe[src*="challenges.cloudflare.com"]'); let sitekey = ""; if (iframe) { const urlParams = new URLSearchParams(new URL(iframe.src).hash ? iframe.src : iframe.src.replace('#', '?')); sitekey = urlParams.get('k') || ""; } const input = document.querySelector('input[name="cf-turnstile-response"]') || document.getElementById('cf-turnstile-response'); return JSON.stringify({ sitekey: sitekey, token: input ? input.value : "" }); })() "#; if let Ok(Some(res_json)) = chaser.evaluate(harvest_script).await { if let Some(res_str) = res_json.as_str() { if let Ok(parsed) = serde_json::from_str::(res_str) { turnstile_sitekey = parsed["sitekey"].as_str().unwrap_or("").to_string(); turnstile_token = parsed["token"].as_str().unwrap_or("").to_string(); } } } } "evaluate" => { if let Some(js_code) = &act.value { if let Ok(Some(res)) = chaser.evaluate(js_code).await { evals.push(res); } } } "screenshot" => { let is_full_page = act.full.unwrap_or(false); let buf = inner_page.screenshot( CaptureScreenshotParams::builder() .from_surface(true) .capture_beyond_viewport(is_full_page) .build() ).await.unwrap(); let mut name = act.file_name.clone().unwrap_or_else(|| format!("ss-{}.png", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos())); name = name.replace(" ", "_"); let file_path = os_tmp_dir.join(&name); std::fs::write(file_path, buf).unwrap(); screenshots.push(format!("/tmp/{}", name)); } _ => {} } } let final_net_logs = network_activities.lock().await.clone(); let res_val = serde_json::json!({ "screenshots": screenshots, "evals": evals, "turnstile": { "sitekey": turnstile_sitekey, "token": turnstile_token }, "networkLogs": final_net_logs }); page.close().await.unwrap(); Ok(warp::reply::json(&StandardResponse { logs, result: res_val, error: None, })) } fn gather_logs(logs: &mut Vec, msg: String) { logs.push(msg); }