Skip to main content

eoka/
lib.rs

1//! # Eoka
2//!
3//! Stealth browser automation for Rust — a Puppeteer/Playwright alternative that bypasses bot detection.
4//!
5//! Eoka is a minimal, fast headless Chrome library built from scratch. Unlike Selenium or
6//! chromiumoxide, it uses a custom CDP implementation with stealth filtering to avoid detection
7//! by Cloudflare, DataDome, PerimeterX, and other anti-bot systems.
8//!
9//! ## Features
10//!
11//! - **Stealth by Default** - Binary patching, 15 JS evasions, human-like mouse/typing
12//! - **Puppeteer-like API** - `click()`, `type()`, `wait_for()`, `screenshot()`, etc.
13//! - **Minimal Dependencies** - 11 direct crates, no chromiumoxide/puppeteer-extra bloat
14//! - **AI-Agent Ready** - PageState introspection, element indexing, text extraction
15//! - **Fast** - Lazy evasion scripts, mmap patching, stack-allocated paths
16//!
17//! ## Quick Start
18//!
19//! ```rust,no_run
20//! use eoka::Browser;
21//!
22//! #[tokio::main]
23//! async fn main() -> eoka::Result<()> {
24//!     // Launch browser (patches Chrome, applies evasions)
25//!     let browser = Browser::launch().await?;
26//!
27//!     // Create page and navigate
28//!     let page = browser.new_page("https://example.com").await?;
29//!
30//!     // Human-like interactions
31//!     page.human_click("#button").await?;
32//!     page.human_type("#input", "hello world").await?;
33//!
34//!     // Screenshot
35//!     let png = page.screenshot().await?;
36//!     std::fs::write("screenshot.png", png)?;
37//!
38//!     browser.close().await?;
39//!     Ok(())
40//! }
41//! ```
42//!
43//! ## Configuration
44//!
45//! ```rust,no_run
46//! use eoka::Browser;
47//!
48//! # #[tokio::main]
49//! # async fn main() -> eoka::Result<()> {
50//! // The default launch path is stealthy and headless.
51//! let browser = Browser::launch().await?;
52//! browser.close().await?;
53//!
54//! // For one-off tweaks, mutate the default stealth config inline.
55//! let browser = Browser::launch_with(|config| {
56//!     config.proxy = Some("http://127.0.0.1:8080".into());
57//! }).await?;
58//! browser.close().await?;
59//! # Ok(())
60//! # }
61//! ```
62
63#![deny(missing_docs)]
64
65pub mod browser;
66pub mod cdp;
67pub mod element;
68pub mod error;
69pub mod fetch;
70mod keyboard;
71pub mod network;
72pub mod page;
73pub mod session;
74pub mod stealth;
75
76// Static assertions: ensure core types are Send + Sync for use across async tasks
77#[allow(dead_code)]
78const _: () = {
79    fn assert_send_sync<T: Send + Sync>() {}
80    fn _assertions() {
81        assert_send_sync::<browser::Browser>();
82        assert_send_sync::<page::Page>();
83    }
84};
85
86// Re-exports
87pub use browser::{Browser, TabInfo};
88pub use element::{BoundingBox, Element};
89pub use error::{Error, Result};
90pub use fetch::{BrowserFetchOutcome, BrowserFetchRequest, BrowserFetchResponse};
91pub use network::{NetworkEvent, NetworkWatcher};
92pub use page::{CapturedRequest, FrameInfo, MouseButton, Page, PageState, ResponseBody, TextMatch};
93pub use session::{BrowserSession, BrowserState, SessionCookie};
94pub use stealth::{Fingerprint, HumanSpeed};
95
96/// Configuration for stealth features
97#[derive(Debug, Clone)]
98pub struct StealthConfig {
99    /// Spoof WebGL renderer/vendor
100    pub webgl_spoof: bool,
101    /// Spoof canvas fingerprint
102    pub canvas_spoof: bool,
103    /// Spoof audio fingerprint
104    pub audio_spoof: bool,
105    /// Use human-like mouse movements
106    pub human_mouse: bool,
107    /// Use human-like typing
108    pub human_typing: bool,
109    /// Custom user agent (None = random realistic)
110    pub user_agent: Option<String>,
111    /// Headless mode
112    pub headless: bool,
113    /// Path to Chrome/Chromium binary
114    pub chrome_path: Option<String>,
115    /// Patch Chrome binary to bypass detection
116    pub patch_binary: bool,
117    /// Viewport width
118    pub viewport_width: u32,
119    /// Viewport height
120    pub viewport_height: u32,
121    /// Debug mode - log actions and save screenshots on error
122    pub debug: bool,
123    /// Directory for debug screenshots (defaults to current directory)
124    pub debug_dir: Option<String>,
125    /// Proxy server URL (e.g. "http://host:port")
126    pub proxy: Option<String>,
127    /// Durable Chrome user-data directory. When set, Eoka preserves the
128    /// profile and its full fingerprint identity across browser launches.
129    ///
130    /// If a Chrome instance is already running on this directory, Eoka
131    /// attaches to it instead of spawning a second process (which Chrome's
132    /// own singleton lock would silently hand off to and exit).
133    pub user_data_dir: Option<String>,
134    /// Named profile within `user_data_dir` (Chrome's `--profile-directory`,
135    /// e.g. "Profile 1"). Only meaningful when `user_data_dir` is set;
136    /// defaults to Chrome's implicit "Default" profile when `None`.
137    ///
138    /// Chrome's singleton lock (and thus Eoka's reuse detection above) is
139    /// scoped to the whole `user_data_dir`, not the individual profile —
140    /// launching a second `profile_dir` while a Chrome is already running
141    /// on the same `user_data_dir` attaches to that same running instance
142    /// rather than opening a separate one, matching Chrome's own behavior.
143    pub profile_dir: Option<String>,
144    /// Proxy username for authenticated proxies
145    pub proxy_username: Option<String>,
146    /// Proxy password for authenticated proxies
147    pub proxy_password: Option<String>,
148    /// CDP command timeout in seconds (default: 30, increase for slow proxies)
149    pub cdp_timeout: u64,
150    /// IANA timezone (default: random from common US/EU timezones).
151    /// Set to a specific value like "America/New_York" to control the timezone.
152    pub timezone: Option<String>,
153    /// Extra Chrome command-line arguments appended after standard stealth args.
154    /// E.g. vec!["--use-fake-ui-for-media-stream".into()] to auto-grant camera.
155    pub extra_args: Vec<String>,
156    /// Enable legacy, invasive CDP-marker hiding that proxies `window.document`
157    /// and patches Object reflection APIs. It can break modern SPAs, so it is
158    /// disabled by default. Prefer the normal WebDriver and fingerprint evasions.
159    pub aggressive_cdp_evasion: bool,
160    /// Treat this as a session attached to a user-owned browser. When true,
161    /// `Browser::new_page`/`new_blank_page`/`attach_page` skip injecting the
162    /// evasion script. Defaults to false (set automatically by `Browser::connect*`).
163    pub live_session: bool,
164    /// Drop "detectable" CDP commands like `Runtime.enable` silently.
165    /// Defaults to true. Set to false in connect mode to get full DevTools-like control.
166    pub filter_cdp: bool,
167    /// Ignore TLS certificate errors on every page from creation, before the
168    /// first navigation. `Page::ignore_cert_errors` is applied too late for
169    /// `new_page`'s own initial navigate — this covers self-signed/expired
170    /// certs (e.g. .onion services) hit on the very first load. Defaults to
171    /// false.
172    pub ignore_cert_errors: bool,
173    /// Strip Chromium's `X-Client-Data` request header via CDP Fetch
174    /// interception. Chrome sends this header with field-trial variation IDs on
175    /// requests to Google-owned properties; a fresh automation profile carries
176    /// a distinctive (often empty) variation set that server-side bot scoring
177    /// can read before any JavaScript runs. Also launches Chrome with
178    /// `--disable-field-trial-config`. Defaults to true for spawned stealth
179    /// browsers; ignored (never applied) for attached live sessions.
180    pub strip_x_client_data: bool,
181    /// Resolve timezone and interface language from the browser's apparent
182    /// public IP via a one-shot navigation to Cloudflare's trace endpoint
183    /// before any real page is created. Keeps `Date`/`Intl` and the injected
184    /// `navigator.languages` consistent with IP geolocation instead of a
185    /// random timezone. Ignored when `timezone` is set explicitly and when
186    /// attaching to an existing browser. Defaults to false.
187    pub geo_align: bool,
188}
189
190impl Default for StealthConfig {
191    fn default() -> Self {
192        Self {
193            webgl_spoof: true,
194            canvas_spoof: true,
195            audio_spoof: true,
196            human_mouse: true,
197            human_typing: true,
198            user_agent: None,
199            headless: true,
200            chrome_path: None,
201            patch_binary: true,
202            viewport_width: 1920,
203            viewport_height: 1080,
204            debug: false,
205            debug_dir: None,
206            proxy: None,
207            user_data_dir: None,
208            profile_dir: None,
209            proxy_username: None,
210            proxy_password: None,
211            cdp_timeout: 30,
212            timezone: None, // Random from common timezones
213            extra_args: Vec::new(),
214            aggressive_cdp_evasion: false,
215            live_session: false,
216            filter_cdp: true,
217            ignore_cert_errors: false,
218            strip_x_client_data: true,
219            geo_align: false,
220        }
221    }
222}
223
224impl StealthConfig {
225    /// Create a minimal config (no spoofing, no patching)
226    pub fn minimal() -> Self {
227        Self {
228            webgl_spoof: false,
229            canvas_spoof: false,
230            audio_spoof: false,
231            human_mouse: false,
232            human_typing: false,
233            headless: false,
234            patch_binary: false,
235            ..Default::default()
236        }
237    }
238
239    /// Create a visible (non-headless) config
240    pub fn visible() -> Self {
241        Self {
242            headless: false,
243            ..Default::default()
244        }
245    }
246
247    /// Create a debug config (visible, with logging)
248    pub fn debug() -> Self {
249        Self {
250            headless: false,
251            debug: true,
252            ..Default::default()
253        }
254    }
255
256    /// Config tuned for attaching to a user-owned browser via `Browser::connect*`.
257    ///
258    /// Disables anything that touches the user's Chrome state:
259    /// - `live_session` — no evasion script injection on attached tabs
260    /// - `filter_cdp = false` — full DevTools-equivalent CDP access
261    /// - `patch_binary = false` — we don't manage the binary
262    /// - all spoofing off — the user's browser already has its own fingerprint
263    pub fn live() -> Self {
264        Self {
265            live_session: true,
266            filter_cdp: false,
267            patch_binary: false,
268            webgl_spoof: false,
269            canvas_spoof: false,
270            audio_spoof: false,
271            human_mouse: true,
272            human_typing: true,
273            ..Default::default()
274        }
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::StealthConfig;
281
282    #[test]
283    fn default_config_is_headless() {
284        assert!(StealthConfig::default().headless);
285    }
286
287    #[test]
288    fn visible_configs_are_explicitly_not_headless() {
289        assert!(!StealthConfig::visible().headless);
290        assert!(!StealthConfig::debug().headless);
291    }
292}