thirtyfour_stealth/
lib.rs

1pub use capabilities::DefaultCapabilitiesBuilder;
2use fetch_chromedriver::fetch_chromedriver;
3use patch_chromedriver::patch_chromedriver;
4use rand::Rng;
5use spawn_chromedriver::spawn_chromedriver;
6use std::{error::Error, fmt::Display, process::Child};
7pub use thirtyfour;
8use thirtyfour::WebDriver;
9mod capabilities;
10mod driver_ext;
11mod fetch_chromedriver;
12mod get_chrome_version;
13mod patch_chromedriver;
14mod spawn_chromedriver;
15pub use driver_ext::Chrome;
16
17/// Fetches a new ChromeDriver executable and patches it to prevent detection.
18/// Returns a WebDriver instance (with default capabilities) and handle to chromedriver process.
19pub async fn chrome() -> Result<(WebDriver, Child), Box<dyn std::error::Error + Send + Sync>> {
20    chrome_with_capabilities(DefaultCapabilitiesBuilder::new().into_chrome_caps()).await
21}
22
23/// Fetches a new ChromeDriver executable and patches it to prevent detection.
24/// Returns a WebDriver instance and handle to chromedriver process.
25/// If chromedriver fails to start 3 times new chromedriver is redownloaded.
26pub async fn chrome_with_capabilities(
27    capabilities: thirtyfour::ChromeCapabilities,
28) -> Result<(WebDriver, Child), Box<dyn std::error::Error + Send + Sync>> {
29    let res = try_start_chrome(capabilities.clone(), 3).await;
30    if res.is_ok() {
31        return res;
32    }
33    let os = std::env::consts::OS;
34    let chromedriver_executable = match os {
35        "linux" => "chromedriver",
36        "macos" => "chromedriver",
37        "windows" => "chromedriver.exe",
38        _ => return Err(UnsupportedOS.into()),
39    };
40    let _ = std::fs::remove_file(chromedriver_executable);
41    let chromedriver_executable = get_patched_chrome_driver_executable()?;
42    let _ = std::fs::remove_file(chromedriver_executable);
43    try_start_chrome(capabilities, 3).await
44}
45
46/// Fetches a new ChromeDriver executable and patches it to prevent detection.
47/// Returns a WebDriver instance and handle to chromedriver process.
48pub async fn try_start_chrome(
49    capabilities: thirtyfour::ChromeCapabilities,
50    num_attempts: u8,
51) -> Result<(WebDriver, Child), Box<dyn std::error::Error + Send + Sync>> {
52    if std::path::Path::new("chromedriver").exists()
53        || std::path::Path::new("chromedriver.exe").exists()
54    {
55        tracing::info!("ChromeDriver already exists!");
56    } else {
57        tracing::info!("ChromeDriver does not exist! Fetching...");
58        fetch_chromedriver().await?;
59    }
60
61    // Ensure existing driver matches host architecture on macOS.
62    // If not, remove and re-download the correct one.
63    if !is_existing_chromedriver_compatible()? {
64        tracing::warn!(
65            "Existing ChromeDriver appears incompatible with host arch. Re-downloading..."
66        );
67        let _ = std::fs::remove_file("chromedriver");
68        let _ = std::fs::remove_file("chromedriver.exe");
69        let _ = std::fs::remove_file("chromedriver_PATCHED");
70        let _ = std::fs::remove_file("chromedriver_PATCHED.exe");
71        fetch_chromedriver().await?;
72    }
73    let chromedriver_executable = get_patched_chrome_driver_executable()?;
74    if std::path::Path::new(chromedriver_executable).exists() {
75        tracing::info!("Detected patched chromedriver executable!");
76    } else {
77        patch_chromedriver(chromedriver_executable)?;
78    }
79    tracing::info!("Starting chromedriver...");
80    let port: u16 = rand::rng().random_range(2000..5000);
81    let mut chrome_driver_handle = spawn_chromedriver(chromedriver_executable, port)?;
82    let mut driver = None;
83    let mut attempt: u8 = 0u8;
84    while driver.is_none() && attempt < num_attempts {
85        attempt += 1;
86        match WebDriver::new(&format!("http://127.0.0.1:{}", port), capabilities.clone()).await {
87            Ok(d) => driver = Some(d),
88            Err(_) => tokio::time::sleep(std::time::Duration::from_millis(250)).await,
89        }
90    }
91    let driver = driver.ok_or_else(|| {
92        let _ = chrome_driver_handle.kill();
93        let _ = chrome_driver_handle.wait();
94        DriverCreationFailed
95    })?;
96    Ok((driver, chrome_driver_handle))
97}
98
99fn is_existing_chromedriver_compatible() -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
100    #[cfg(target_os = "macos")]
101    {
102        let arch = std::env::consts::ARCH; // "aarch64" or "x86_64"
103        let candidate = if std::path::Path::new("chromedriver_PATCHED").exists() {
104            "chromedriver_PATCHED"
105        } else if std::path::Path::new("chromedriver").exists() {
106            "chromedriver"
107        } else {
108            return Ok(true);
109        };
110        let output = std::process::Command::new("/usr/bin/file")
111            .arg(candidate)
112            .output();
113        if let Ok(out) = output {
114            let stdout = String::from_utf8_lossy(&out.stdout).to_string();
115            // Heuristic: ensure the Mach-O header mentions the expected cpu subtype
116            let is_arm_expected = arch == "aarch64";
117            let mentions_arm64 = stdout.contains("arm64");
118            let mentions_x86_64 = stdout.contains("x86_64");
119            if is_arm_expected && mentions_x86_64 {
120                return Ok(false);
121            }
122            if !is_arm_expected && mentions_arm64 {
123                return Ok(false);
124            }
125        }
126        return Ok(true);
127    }
128    #[cfg(not(target_os = "macos"))]
129    {
130        Ok(true)
131    }
132}
133
134fn get_patched_chrome_driver_executable(
135) -> Result<&'static str, Box<dyn std::error::Error + Send + Sync>> {
136    let os = std::env::consts::OS;
137    let chromedriver_executable = match os {
138        "linux" => "chromedriver_PATCHED",
139        "macos" => "chromedriver_PATCHED",
140        "windows" => "chromedriver_PATCHED.exe",
141        _ => return Err(UnsupportedOS.into()),
142    };
143    Ok(chromedriver_executable)
144}
145
146#[derive(Debug)]
147struct DriverCreationFailed;
148
149impl Display for DriverCreationFailed {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        write!(f, "Driver creation failed.")
152    }
153}
154impl Error for DriverCreationFailed {}
155
156#[derive(Debug)]
157struct UnsupportedOS;
158
159impl Display for UnsupportedOS {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        write!(f, "Your OS is not supported.")
162    }
163}
164impl Error for UnsupportedOS {}