use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
static BROWSER_COUNTER: AtomicU64 = AtomicU64::new(0);
use crate::cdp::transport::launch_chrome;
use crate::cdp::{Connection, Transport};
use crate::error::{Error, Result};
use crate::page::Page;
use crate::stealth::{build_evasion_script, find_chrome, random_user_agent, ChromePatcher};
use crate::StealthConfig;
fn stealth_args(config: &StealthConfig) -> Vec<String> {
let mut args = vec![
"--disable-blink-features=AutomationControlled".into(),
"--disable-automation".into(),
"--disable-features=IsolateOrigins,site-per-process,AutomationControlled,EnableAutomation"
.into(),
"--enable-features=NetworkService,NetworkServiceInProcess".into(),
"--disable-infobars".into(),
"--disable-dev-shm-usage".into(),
"--disable-ipc-flooding-protection".into(),
"--disable-renderer-backgrounding".into(),
"--disable-background-timer-throttling".into(),
"--disable-backgrounding-occluded-windows".into(),
"--no-first-run".into(),
"--no-default-browser-check".into(),
"--no-sandbox".into(),
"--disable-extensions-except=".into(),
"--disable-default-apps".into(),
"--disable-component-extensions-with-background-pages".into(),
"--disable-hang-monitor".into(),
"--disable-popup-blocking".into(),
"--disable-prompt-on-repost".into(),
"--disable-sync".into(),
"--disable-translate".into(),
"--metrics-recording-only".into(),
"--safebrowsing-disable-auto-update".into(),
"--disable-client-side-phishing-detection".into(),
"--password-store=basic".into(),
"--use-mock-keychain".into(),
"--excludeSwitches=enable-automation".into(),
format!(
"--window-size={},{}",
config.viewport_width, config.viewport_height
),
];
let user_agent = config.user_agent.clone().unwrap_or_else(random_user_agent);
args.push(format!("--user-agent={}", user_agent));
if config.headless {
args.push("--headless=new".into());
}
if let Some(ref proxy) = config.proxy {
args.push(format!("--proxy-server={}", proxy));
}
for arg in &config.extra_args {
args.push(arg.clone());
}
args
}
#[derive(Debug, Clone)]
pub struct TabInfo {
pub id: String,
pub title: String,
pub url: String,
}
pub struct Browser {
connection: Connection,
config: Arc<StealthConfig>,
user_data_dir: Option<PathBuf>,
patched_dir: Option<PathBuf>,
evasion_script: String,
}
impl Browser {
pub async fn launch() -> Result<Self> {
Self::launch_with_config(StealthConfig::default()).await
}
pub async fn launch_with_config(config: StealthConfig) -> Result<Self> {
let config = Arc::new(config);
let instance_id = BROWSER_COUNTER.fetch_add(1, Ordering::Relaxed);
let user_data_dir = std::env::temp_dir().join(format!(
"eoka-browser-{}-{}",
std::process::id(),
instance_id
));
let _ = std::fs::remove_dir_all(&user_data_dir);
std::fs::create_dir_all(&user_data_dir)?;
let chrome_path = match &config.chrome_path {
Some(p) => PathBuf::from(p),
None => find_chrome()?,
};
let (chrome_path, patched_dir) = if config.patch_binary {
let patcher = ChromePatcher::new(&chrome_path)?;
let patched = patcher.get_patched_path()?;
let dir = patched.parent().map(|p| p.to_path_buf());
(patched, dir)
} else {
(chrome_path, None)
};
let mut args = stealth_args(&config);
args.push(format!("--user-data-dir={}", user_data_dir.display()));
tracing::info!("Launching Chrome from {:?}", chrome_path);
let (child, ws_url) = launch_chrome(&chrome_path, &args)?;
let proxy_auth = match (&config.proxy_username, &config.proxy_password) {
(Some(u), Some(p)) => Some((u.clone(), p.clone())),
_ => None,
};
let transport =
Transport::new_with_options(child, &ws_url, proxy_auth, config.cdp_timeout)?;
let connection = Connection::new(transport);
let version = connection.version().await?;
tracing::info!("Connected to Chrome: {}", version.product);
let evasion_script = build_evasion_script(&config);
Ok(Self {
connection,
config,
user_data_dir: Some(user_data_dir),
patched_dir,
evasion_script,
})
}
pub async fn connect(ws_url: &str) -> Result<Self> {
Self::connect_with_config(ws_url, StealthConfig::live()).await
}
pub async fn connect_with_config(ws_url: &str, config: StealthConfig) -> Result<Self> {
let config = Arc::new(config);
let transport = crate::cdp::transport::Transport::connect_with_options(
ws_url,
config.cdp_timeout,
config.filter_cdp,
)?;
let connection = Connection::new(transport);
let version = connection.version().await?;
tracing::info!("Connected to Chrome: {}", version.product);
let evasion_script = if config.live_session {
String::new()
} else {
build_evasion_script(&config)
};
Ok(Self {
connection,
config,
user_data_dir: None,
patched_dir: None,
evasion_script,
})
}
pub async fn connect_port(port: u16) -> Result<Self> {
let ws_url = crate::cdp::discover::discover_browser_ws("127.0.0.1", port)?;
Self::connect(&ws_url).await
}
pub async fn connect_port_with_config(port: u16, config: StealthConfig) -> Result<Self> {
let ws_url = crate::cdp::discover::discover_browser_ws("127.0.0.1", port)?;
Self::connect_with_config(&ws_url, config).await
}
async fn setup_session(&self, session: &crate::cdp::Session) -> Result<()> {
session.page_enable().await?;
if self.config.proxy_username.is_some() && self.config.proxy_password.is_some() {
session.fetch_enable(true).await?;
}
if !self.config.live_session {
session
.add_script_to_evaluate_on_new_document(&self.evasion_script)
.await?;
}
Ok(())
}
pub async fn new_page(&self, url: &str) -> Result<Page> {
let target_id = self
.connection
.create_target("about:blank", None, None)
.await?;
let session = self.connection.attach_to_target(&target_id).await?;
self.setup_session(&session).await?;
let nav_result = session.navigate(url, None).await?;
if let Some(error) = nav_result.error_text {
return Err(Error::Navigation(error));
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(Page::new(session, Arc::clone(&self.config)))
}
pub async fn new_blank_page(&self) -> Result<Page> {
let target_id = self
.connection
.create_target("about:blank", None, None)
.await?;
let session = self.connection.attach_to_target(&target_id).await?;
self.setup_session(&session).await?;
Ok(Page::new(session, Arc::clone(&self.config)))
}
pub async fn version(&self) -> Result<String> {
let v = self.connection.version().await?;
Ok(v.product)
}
pub async fn tabs(&self) -> Result<Vec<TabInfo>> {
let targets = self.connection.get_targets().await?;
Ok(targets
.into_iter()
.filter(|t| t.r#type == "page")
.map(|t| TabInfo {
id: t.target_id,
title: t.title,
url: t.url,
})
.collect())
}
pub async fn attach_page(&self, target_id: &str) -> Result<Page> {
let session = self.connection.attach_to_target(target_id).await?;
self.setup_session(&session).await?;
Ok(Page::new(session, Arc::clone(&self.config)))
}
pub async fn activate_tab(&self, target_id: &str) -> Result<()> {
self.connection.activate_target(target_id).await
}
pub async fn close_tab(&self, target_id: &str) -> Result<()> {
self.connection.close_target(target_id).await?;
Ok(())
}
pub async fn close(self) -> Result<()> {
if self.config.live_session {
self.connection.transport().close().await?;
} else {
self.connection.close().await?;
}
if let Some(ref dir) = self.user_data_dir {
let _ = std::fs::remove_dir_all(dir);
}
if let Some(ref dir) = self.patched_dir {
let _ = std::fs::remove_dir_all(dir);
}
Ok(())
}
pub async fn disconnect(self) -> Result<()> {
self.connection.transport().close().await
}
}
impl Drop for Browser {
fn drop(&mut self) {
if let Some(ref dir) = self.user_data_dir {
let _ = std::fs::remove_dir_all(dir);
}
if let Some(ref dir) = self.patched_dir {
let _ = std::fs::remove_dir_all(dir);
}
}
}