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::{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        let browser = self.browser().await?;
137        let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
138
139        tokio::time::timeout(
140            self.config.timeout(),
141            solver::get_source(manager, url, proxy),
142        )
143        .await
144        .map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
145    }
146
147    /// Create a WAF session with cookies and headers for authenticated requests
148    ///
149    /// # Arguments
150    ///
151    /// * `url` - Target URL to create session for
152    /// * `proxy` - Optional proxy configuration
153    ///
154    /// # Returns
155    ///
156    /// A `WafSession` containing cookies and headers that can be used for
157    /// subsequent requests to the same site.
158    pub async fn solve_waf_session(
159        &self,
160        url: &str,
161        proxy: Option<ProxyConfig>,
162    ) -> ChaserResult<WafSession> {
163        let browser = self.browser().await?;
164        let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
165
166        tokio::time::timeout(
167            self.config.timeout(),
168            solver::solve_waf_session(manager, url, proxy),
169        )
170        .await
171        .map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
172    }
173
174    /// Solve a Turnstile captcha with full page load
175    ///
176    /// # Arguments
177    ///
178    /// * `url` - URL containing the Turnstile widget
179    /// * `proxy` - Optional proxy configuration
180    ///
181    /// # Returns
182    ///
183    /// The Turnstile token string.
184    pub async fn solve_turnstile(
185        &self,
186        url: &str,
187        proxy: Option<ProxyConfig>,
188    ) -> ChaserResult<String> {
189        let browser = self.browser().await?;
190        let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
191
192        tokio::time::timeout(
193            self.config.timeout(),
194            solver::solve_turnstile_max(manager, url, proxy),
195        )
196        .await
197        .map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
198    }
199
200    /// Solve a Turnstile captcha with minimal resource usage
201    ///
202    /// This mode intercepts the page request and serves a minimal HTML page
203    /// that only loads the Turnstile widget. Requires the site key.
204    ///
205    /// # Arguments
206    ///
207    /// * `url` - URL to use as the Turnstile origin
208    /// * `site_key` - The Turnstile site key
209    /// * `proxy` - Optional proxy configuration
210    ///
211    /// # Returns
212    ///
213    /// The Turnstile token string.
214    pub async fn solve_turnstile_min(
215        &self,
216        url: &str,
217        site_key: &str,
218        proxy: Option<ProxyConfig>,
219    ) -> ChaserResult<String> {
220        let browser = self.browser().await?;
221        let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
222
223        tokio::time::timeout(
224            self.config.timeout(),
225            solver::solve_turnstile_min(manager, url, site_key, proxy),
226        )
227        .await
228        .map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
229    }
230
231    /// Get configuration
232    pub fn config(&self) -> &ChaserConfig {
233        &self.config
234    }
235}
236
237impl Drop for ChaserCF {
238    fn drop(&mut self) {
239        // Note: async drop is not supported, so we just log a warning
240        // if the browser wasn't explicitly shut down
241        if let Ok(guard) = self.browser.try_read() {
242            if guard.is_some() {
243                tracing::warn!(
244                    "ChaserCF dropped without explicit shutdown(). \
245                     Call shutdown() for clean resource release."
246                );
247            }
248        }
249    }
250}