Skip to main content

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}