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}