undetected_chromedriver/
lib.rs

1use rand::Rng;
2#[cfg(any(target_os = "linux", target_os = "macos"))]
3use std::os::unix::fs::PermissionsExt;
4use std::process::Command;
5use thirtyfour::{DesiredCapabilities, WebDriver};
6
7/// Fetches a new ChromeDriver executable and patches it to prevent detection.
8/// Returns a WebDriver instance.
9pub async fn chrome() -> Result<WebDriver, Box<dyn std::error::Error>> {
10    let os = std::env::consts::OS;
11    if std::path::Path::new("chromedriver").exists()
12        || std::path::Path::new("chromedriver.exe").exists()
13    {
14        println!("ChromeDriver already exists!");
15    } else {
16        println!("ChromeDriver does not exist! Fetching...");
17        let client = reqwest::Client::new();
18        fetch_chromedriver(&client).await.unwrap();
19    }
20    let chromedriver_executable = match os {
21        "linux" => "chromedriver_PATCHED",
22        "macos" => "chromedriver_PATCHED",
23        "windows" => "chromedriver_PATCHED.exe",
24        _ => panic!("Unsupported OS!"),
25    };
26    match !std::path::Path::new(chromedriver_executable).exists() {
27        true => {
28            println!("Starting ChromeDriver executable patch...");
29            let file_name = if cfg!(windows) {
30                "chromedriver.exe"
31            } else {
32                "chromedriver"
33            };
34            let f = std::fs::read(file_name).unwrap();
35            let mut new_chromedriver_bytes = f.clone();
36            let mut total_cdc = String::from("");
37            let mut cdc_pos_list = Vec::new();
38            let mut is_cdc_present = false;
39            let mut patch_ct = 0;
40            for i in 0..f.len() - 3 {
41                if "cdc_"
42                    == format!(
43                        "{}{}{}{}",
44                        f[i] as char,
45                        f[i + 1] as char,
46                        f[i + 2] as char,
47                        f[i + 3] as char
48                    )
49                    .as_str()
50                {
51                    for x in i + 4..i + 22 {
52                        total_cdc.push_str(&(f[x] as char).to_string());
53                    }
54                    is_cdc_present = true;
55                    cdc_pos_list.push(i);
56                    total_cdc = String::from("");
57                }
58            }
59            match is_cdc_present {
60                true => println!("Found cdcs!"),
61                false => println!("No cdcs were found!"),
62            }
63            let get_random_char = || -> char {
64                "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
65                    .chars()
66                    .collect::<Vec<char>>()[rand::thread_rng().gen_range(0..48)]
67            };
68
69            for i in cdc_pos_list {
70                for x in i + 4..i + 22 {
71                    new_chromedriver_bytes[x] = get_random_char() as u8;
72                }
73                patch_ct += 1;
74            }
75            println!("Patched {} cdcs!", patch_ct);
76
77            println!("Starting to write to binary file...");
78            let _file = std::fs::File::create(chromedriver_executable).unwrap();
79            match std::fs::write(chromedriver_executable, new_chromedriver_bytes) {
80                Ok(_res) => {
81                    println!("Successfully wrote patched executable to 'chromedriver_PATCHED'!",)
82                }
83                Err(err) => println!("Error when writing patch to file! Error: {}", err),
84            };
85        }
86        false => {
87            println!("Detected patched chromedriver executable!");
88        }
89    }
90    #[cfg(any(target_os = "linux", target_os = "macos"))]
91    {
92        let mut perms = std::fs::metadata(chromedriver_executable)
93            .unwrap()
94            .permissions();
95        perms.set_mode(0o755);
96        std::fs::set_permissions(chromedriver_executable, perms).unwrap();
97    }
98    println!("Starting chromedriver...");
99    let port: usize = rand::thread_rng().gen_range(2000..5000);
100    Command::new(format!("./{}", chromedriver_executable))
101        .arg(format!("--port={}", port))
102        .spawn()
103        .expect("Failed to start chromedriver!");
104    let mut caps = DesiredCapabilities::chrome();
105    caps.set_no_sandbox().unwrap();
106    caps.set_disable_dev_shm_usage().unwrap();
107    caps.add_chrome_arg("--disable-blink-features=AutomationControlled")
108        .unwrap();
109    caps.add_chrome_arg("window-size=1920,1080").unwrap();
110    caps.add_chrome_arg("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36").unwrap();
111    caps.add_chrome_arg("disable-infobars").unwrap();
112    caps.add_chrome_option("excludeSwitches", ["enable-automation"])
113        .unwrap();
114    let mut driver = None;
115    let mut attempt = 0;
116    while driver.is_none() && attempt < 20 {
117        attempt += 1;
118        match WebDriver::new(&format!("http://localhost:{}", port), caps.clone()).await {
119            Ok(d) => driver = Some(d),
120            Err(_) => std::thread::sleep(std::time::Duration::from_millis(250)),
121        }
122    }
123    let driver = driver.unwrap();
124    Ok(driver)
125}
126
127async fn fetch_chromedriver(client: &reqwest::Client) -> Result<(), Box<dyn std::error::Error>> {
128    let os = std::env::consts::OS;
129
130    let installed_version = get_chrome_version(os).await?;
131    let chromedriver_url: String;
132    if installed_version.as_str() >= "114" {
133        // Fetch the correct version
134        let url = "https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone.json";
135        let resp = client.get(url).send().await?;
136        let body = resp.bytes().await?;
137        let json = serde_json::from_slice::<serde_json::Value>(&body)?;
138        let version = json["milestones"][installed_version]["version"]
139            .as_str()
140            .unwrap();
141
142        // Fetch the chromedriver binary
143        chromedriver_url = match os {
144            "linux" => format!(
145                "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/{}/{}/{}",
146                version, "linux64", "chromedriver-linux64.zip"
147            ),
148            "macos" => format!(
149                "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/{}/{}/{}",
150                version, "mac-x64", "chromedriver-mac-x64.zip"
151            ),
152            "windows" => format!(
153                "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/{}/{}/{}",
154                version, "win64", "chromedriver-win64.zip"
155            ),
156            _ => panic!("Unsupported OS!"),
157        };
158    } else {
159        let resp = client
160            .get(format!(
161                "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{}",
162                installed_version
163            ))
164            .send()
165            .await?;
166        let body = resp.text().await?;
167        chromedriver_url = match os {
168            "linux" => format!(
169                "https://chromedriver.storage.googleapis.com/{}/chromedriver_linux64.zip",
170                body
171            ),
172            "windows" => format!(
173                "https://chromedriver.storage.googleapis.com/{}/chromedriver_win32.zip",
174                body
175            ),
176            "macos" => format!(
177                "https://chromedriver.storage.googleapis.com/{}/chromedriver_mac64.zip",
178                body
179            ),
180            _ => panic!("Unsupported OS!"),
181        };
182    }
183
184    let resp = client.get(&chromedriver_url).send().await?;
185    let body = resp.bytes().await?;
186
187    let mut archive = zip::ZipArchive::new(std::io::Cursor::new(body))?;
188    for i in 0..archive.len() {
189        let mut file = archive.by_index(i)?;
190        let outpath = file.mangled_name();
191        if file.name().ends_with('/') {
192            std::fs::create_dir_all(&outpath)?;
193        } else {
194            let outpath_relative = outpath.file_name().unwrap();
195            let mut outfile = std::fs::File::create(outpath_relative)?;
196            std::io::copy(&mut file, &mut outfile)?;
197        }
198    }
199    Ok(())
200}
201
202async fn get_chrome_version(os: &str) -> Result<String, Box<dyn std::error::Error>> {
203    println!("Getting installed Chrome version...");
204    let command = match os {
205        "linux" => Command::new("/usr/bin/google-chrome")
206            .arg("--version")
207            .output()?,
208        "macos" => Command::new("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
209            .arg("--version")
210            .output()?,
211        "windows" => Command::new("powershell")
212            .arg("-c")
213            .arg("(Get-Item 'C:/Program Files/Google/Chrome/Application/chrome.exe').VersionInfo")
214            .output()?,
215        _ => panic!("Unsupported OS!"),
216    };
217    let output = String::from_utf8(command.stdout)?;
218    
219    let version = output
220    .lines()
221    .flat_map(|line| line.chars().filter(|&ch| ch.is_ascii_digit()))
222    .take(3)
223    .collect::<String>();
224
225    println!("Currently installed Chrome version: {}", version);
226    Ok(version)
227}