mod browser;
mod config;
mod solver;
pub use browser::BrowserManager;
pub use config::ChaserConfig;
use crate::error::{ChaserError, ChaserResult};
use crate::models::{ProxyConfig, WafSession};
use std::sync::Arc;
use tokio::sync::RwLock;
pub struct ChaserCF {
config: ChaserConfig,
browser: Arc<RwLock<Option<BrowserManager>>>,
initialized: Arc<RwLock<bool>>,
}
impl ChaserCF {
pub async fn new(config: ChaserConfig) -> ChaserResult<Self> {
let suite = Self {
config: config.clone(),
browser: Arc::new(RwLock::new(None)),
initialized: Arc::new(RwLock::new(false)),
};
if !config.lazy_init {
suite.init().await?;
}
Ok(suite)
}
pub async fn init(&self) -> ChaserResult<()> {
let mut initialized = self.initialized.write().await;
if *initialized {
return Ok(());
}
tracing::info!("Initializing chaser-cf browser...");
let manager = BrowserManager::new(&self.config).await?;
let mut browser = self.browser.write().await;
*browser = Some(manager);
*initialized = true;
tracing::info!("chaser-cf browser initialized");
Ok(())
}
async fn ensure_init(&self) -> ChaserResult<()> {
if !*self.initialized.read().await {
self.init().await?;
}
Ok(())
}
async fn browser(
&self,
) -> ChaserResult<tokio::sync::RwLockReadGuard<'_, Option<BrowserManager>>> {
self.ensure_init().await?;
let guard = self.browser.read().await;
if guard.is_none() {
return Err(ChaserError::NotInitialized);
}
Ok(guard)
}
pub async fn shutdown(&self) {
let mut browser = self.browser.write().await;
if let Some(manager) = browser.take() {
manager.shutdown().await;
}
*self.initialized.write().await = false;
tracing::info!("chaser-cf shutdown complete");
}
pub async fn is_ready(&self) -> bool {
let initialized = *self.initialized.read().await;
if !initialized {
return false;
}
let browser = self.browser.read().await;
browser.as_ref().map(|b| b.is_healthy()).unwrap_or(false)
}
pub async fn get_source(&self, url: &str, proxy: Option<ProxyConfig>) -> ChaserResult<String> {
let browser = self.browser().await?;
let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
tokio::time::timeout(
self.config.timeout(),
solver::get_source(manager, url, proxy),
)
.await
.map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
}
pub async fn solve_waf_session(
&self,
url: &str,
proxy: Option<ProxyConfig>,
) -> ChaserResult<WafSession> {
let browser = self.browser().await?;
let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
tokio::time::timeout(
self.config.timeout(),
solver::solve_waf_session(manager, url, proxy),
)
.await
.map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
}
pub async fn solve_turnstile(
&self,
url: &str,
proxy: Option<ProxyConfig>,
) -> ChaserResult<String> {
let browser = self.browser().await?;
let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
tokio::time::timeout(
self.config.timeout(),
solver::solve_turnstile_max(manager, url, proxy),
)
.await
.map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
}
pub async fn solve_turnstile_min(
&self,
url: &str,
site_key: &str,
proxy: Option<ProxyConfig>,
) -> ChaserResult<String> {
let browser = self.browser().await?;
let manager = browser.as_ref().ok_or(ChaserError::NotInitialized)?;
tokio::time::timeout(
self.config.timeout(),
solver::solve_turnstile_min(manager, url, site_key, proxy),
)
.await
.map_err(|_| ChaserError::Timeout(self.config.timeout_ms))?
}
pub fn config(&self) -> &ChaserConfig {
&self.config
}
}
impl Drop for ChaserCF {
fn drop(&mut self) {
if let Ok(guard) = self.browser.try_read() {
if guard.is_some() {
tracing::warn!(
"ChaserCF dropped without explicit shutdown(). \
Call shutdown() for clean resource release."
);
}
}
}
}