chaser_cf/core/mod.rs
1//! Core chaser-cf implementation
2//!
3//! This module provides the main `ChaserCF` API for browser automation
4//! with stealth capabilities.
5
6mod browser;
7mod config;
8mod solver;
9
10pub use browser::BrowserManager;
11pub use config::ChaserConfig;
12
13use crate::error::{ChaserError, ChaserResult};
14use crate::models::{Profile, ProxyConfig, WafSession};
15
16use std::sync::Arc;
17use tokio::sync::RwLock;
18
19/// chaser-cf - High-level API for Cloudflare bypass operations
20///
21/// # Example
22///
23/// ```rust,no_run
24/// use chaser_cf::{ChaserCF, ChaserConfig};
25///
26/// #[tokio::main]
27/// async fn main() -> anyhow::Result<()> {
28/// let chaser = ChaserCF::new(ChaserConfig::default()).await?;
29///
30/// let session = chaser.solve_waf_session("https://example.com", None).await?;
31/// println!("Got {} cookies", session.cookies.len());
32///
33/// chaser.shutdown().await;
34/// Ok(())
35/// }
36/// ```
37pub struct ChaserCF {
38 config: ChaserConfig,
39 browser: Arc<RwLock<Option<BrowserManager>>>,
40 initialized: Arc<RwLock<bool>>,
41}
42
43impl ChaserCF {
44 /// Create a new ChaserCF instance with the given configuration.
45 ///
46 /// This will initialize the browser immediately unless `lazy_init` is enabled
47 /// in the configuration.
48 pub async fn new(config: ChaserConfig) -> ChaserResult<Self> {
49 let suite = Self {
50 config: config.clone(),
51 browser: Arc::new(RwLock::new(None)),
52 initialized: Arc::new(RwLock::new(false)),
53 };
54
55 if !config.lazy_init {
56 suite.init().await?;
57 }
58
59 Ok(suite)
60 }
61
62 /// Initialize the browser explicitly.
63 ///
64 /// This is called automatically on first use if `lazy_init` is enabled,
65 /// or during construction if `lazy_init` is disabled.
66 pub async fn init(&self) -> ChaserResult<()> {
67 let mut initialized = self.initialized.write().await;
68 if *initialized {
69 return Ok(());
70 }
71
72 tracing::info!("Initializing chaser-cf browser...");
73
74 let manager = BrowserManager::new(&self.config).await?;
75
76 let mut browser = self.browser.write().await;
77 *browser = Some(manager);
78 *initialized = true;
79
80 tracing::info!("chaser-cf browser initialized");
81 Ok(())
82 }
83
84 /// Ensure browser is initialized (for lazy init)
85 async fn ensure_init(&self) -> ChaserResult<()> {
86 if !*self.initialized.read().await {
87 self.init().await?;
88 }
89 Ok(())
90 }
91
92 /// Get browser manager, initializing if needed
93 async fn browser(
94 &self,
95 ) -> ChaserResult<tokio::sync::RwLockReadGuard<'_, Option<BrowserManager>>> {
96 self.ensure_init().await?;
97 let guard = self.browser.read().await;
98 if guard.is_none() {
99 return Err(ChaserError::NotInitialized);
100 }
101 Ok(guard)
102 }
103
104 /// Shutdown the browser and release resources.
105 pub async fn shutdown(&self) {
106 let mut browser = self.browser.write().await;
107 if let Some(manager) = browser.take() {
108 manager.shutdown().await;
109 }
110 *self.initialized.write().await = false;
111 tracing::info!("chaser-cf shutdown complete");
112 }
113
114 /// Check if the browser is initialized and healthy
115 pub async fn is_ready(&self) -> bool {
116 let initialized = *self.initialized.read().await;
117 if !initialized {
118 return false;
119 }
120
121 let browser = self.browser.read().await;
122 browser.as_ref().map(|b| b.is_healthy()).unwrap_or(false)
123 }
124
125 /// Get page source from a Cloudflare-protected URL
126 ///
127 /// # Arguments
128 ///
129 /// * `url` - Target URL to scrape
130 /// * `proxy` - Optional proxy configuration
131 ///
132 /// # Returns
133 ///
134 /// The HTML source of the page after bypassing Cloudflare protection.
135 pub async fn get_source(&self, url: &str, proxy: Option<ProxyConfig>) -> ChaserResult<String> {
136 self.get_source_with_profile(url, proxy, None).await
137 }
138
139 /// Get page source from a Cloudflare-protected URL with a specific profile
140 ///
141 /// # Arguments
142 ///
143 /// * `url` - Target URL to scrape
144 /// * `proxy` - Optional proxy configuration
145 /// * `profile` - Optional profile override (uses default if None)
146 ///
147 /// # Returns
148 ///
149 /// The HTML source of the page after bypassing Cloudflare protection.
150 pub async fn get_source_with_profile(
151 &self,
152 url: &str,
153 proxy: Option<ProxyConfig>,
154 profile: Option<Profile>,
155 ) -> ChaserResult<String> {
156 let browser = self.browser().await?;
157 let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
158 let profile = profile.unwrap_or(self.config.profile);
159
160 tokio::time::timeout(
161 self.config.timeout(),
162 solver::get_source(manager, url, proxy, profile),
163 )
164 .await
165 .map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
166 }
167
168 /// Create a WAF session with cookies and headers for authenticated requests
169 ///
170 /// # Arguments
171 ///
172 /// * `url` - Target URL to create session for
173 /// * `proxy` - Optional proxy configuration
174 ///
175 /// # Returns
176 ///
177 /// A `WafSession` containing cookies and headers that can be used for
178 /// subsequent requests to the same site.
179 pub async fn solve_waf_session(
180 &self,
181 url: &str,
182 proxy: Option<ProxyConfig>,
183 ) -> ChaserResult<WafSession> {
184 self.solve_waf_session_with_profile(url, proxy, None).await
185 }
186
187 /// Create a WAF session with a specific profile
188 ///
189 /// # Arguments
190 ///
191 /// * `url` - Target URL to create session for
192 /// * `proxy` - Optional proxy configuration
193 /// * `profile` - Optional profile override (uses default if None)
194 ///
195 /// # Returns
196 ///
197 /// A `WafSession` containing cookies and headers that can be used for
198 /// subsequent requests to the same site.
199 pub async fn solve_waf_session_with_profile(
200 &self,
201 url: &str,
202 proxy: Option<ProxyConfig>,
203 profile: Option<Profile>,
204 ) -> ChaserResult<WafSession> {
205 let browser = self.browser().await?;
206 let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
207 let profile = profile.unwrap_or(self.config.profile);
208
209 tokio::time::timeout(
210 self.config.timeout(),
211 solver::solve_waf_session(manager, url, proxy, profile),
212 )
213 .await
214 .map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
215 }
216
217 /// Solve a Turnstile captcha with full page load
218 ///
219 /// # Arguments
220 ///
221 /// * `url` - URL containing the Turnstile widget
222 /// * `proxy` - Optional proxy configuration
223 ///
224 /// # Returns
225 ///
226 /// The Turnstile token string.
227 pub async fn solve_turnstile(
228 &self,
229 url: &str,
230 proxy: Option<ProxyConfig>,
231 ) -> ChaserResult<String> {
232 self.solve_turnstile_with_profile(url, proxy, None).await
233 }
234
235 /// Solve a Turnstile captcha with a specific profile
236 ///
237 /// # Arguments
238 ///
239 /// * `url` - URL containing the Turnstile widget
240 /// * `proxy` - Optional proxy configuration
241 /// * `profile` - Optional profile override (uses default if None)
242 ///
243 /// # Returns
244 ///
245 /// The Turnstile token string.
246 pub async fn solve_turnstile_with_profile(
247 &self,
248 url: &str,
249 proxy: Option<ProxyConfig>,
250 profile: Option<Profile>,
251 ) -> ChaserResult<String> {
252 let browser = self.browser().await?;
253 let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
254 let profile = profile.unwrap_or(self.config.profile);
255
256 tokio::time::timeout(
257 self.config.timeout(),
258 solver::solve_turnstile_max(manager, url, proxy, profile),
259 )
260 .await
261 .map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
262 }
263
264 /// Solve a Turnstile captcha with minimal resource usage
265 ///
266 /// This mode intercepts the page request and serves a minimal HTML page
267 /// that only loads the Turnstile widget. Requires the site key.
268 ///
269 /// # Arguments
270 ///
271 /// * `url` - URL to use as the Turnstile origin
272 /// * `site_key` - The Turnstile site key
273 /// * `proxy` - Optional proxy configuration
274 ///
275 /// # Returns
276 ///
277 /// The Turnstile token string.
278 pub async fn solve_turnstile_min(
279 &self,
280 url: &str,
281 site_key: &str,
282 proxy: Option<ProxyConfig>,
283 ) -> ChaserResult<String> {
284 self.solve_turnstile_min_with_profile(url, site_key, proxy, None)
285 .await
286 }
287
288 /// Solve a Turnstile captcha with minimal resource usage and a specific profile
289 ///
290 /// # Arguments
291 ///
292 /// * `url` - URL to use as the Turnstile origin
293 /// * `site_key` - The Turnstile site key
294 /// * `proxy` - Optional proxy configuration
295 /// * `profile` - Optional profile override (uses default if None)
296 ///
297 /// # Returns
298 ///
299 /// The Turnstile token string.
300 pub async fn solve_turnstile_min_with_profile(
301 &self,
302 url: &str,
303 site_key: &str,
304 proxy: Option<ProxyConfig>,
305 profile: Option<Profile>,
306 ) -> ChaserResult<String> {
307 let browser = self.browser().await?;
308 let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
309 let profile = profile.unwrap_or(self.config.profile);
310
311 tokio::time::timeout(
312 self.config.timeout(),
313 solver::solve_turnstile_min(manager, url, site_key, proxy, profile),
314 )
315 .await
316 .map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
317 }
318
319 /// Get configuration
320 pub fn config(&self) -> &ChaserConfig {
321 &self.config
322 }
323}
324
325impl Drop for ChaserCF {
326 fn drop(&mut self) {
327 // Note: async drop is not supported, so we just log a warning
328 // if the browser wasn't explicitly shut down
329 if let Ok(guard) = self.browser.try_read() {
330 if guard.is_some() {
331 tracing::warn!(
332 "ChaserCF dropped without explicit shutdown(). \
333 Call shutdown() for clean resource release."
334 );
335 }
336 }
337 }
338}