1use crate::{find_chrome, injections::COMPREHENSIVE_BOOTSTRAP, launch_patched_chromedriver};
4use color_eyre::Result;
5use fantoccini::ClientBuilder;
6use serde_json::json;
7use std::process::Child;
8use base64::Engine;
9
10pub struct OrbBrowser {
31 client: fantoccini::Client,
32 _chromedriver: Child,
33}
34
35impl OrbBrowser {
36 pub async fn new() -> Result<Self> {
43 Self::with_size(1920, 1080).await
44 }
45
46 pub async fn with_size(width: u32, height: u32) -> Result<Self> {
48 let chrome_path = find_chrome()
49 .ok_or_else(|| color_eyre::eyre::eyre!("Chrome/Chromium not found"))?;
50
51 let (webdriver_url, chromedriver_process) = launch_patched_chromedriver()?;
53
54 let mut caps = serde_json::Map::new();
56 caps.insert("browserName".to_string(), json!("chrome"));
57
58 let mut chrome_options = serde_json::Map::new();
59 chrome_options.insert("binary".to_string(), json!(chrome_path.to_str().unwrap()));
60
61 let window_size_arg = format!("--window-size={},{}", width, height);
62 let args = vec![
63 "--disable-blink-features=AutomationControlled",
64 "--disable-web-security",
65 "--disable-dev-shm-usage",
66 "--no-first-run",
67 "--disable-infobars",
68 "--disable-extensions",
69 "--disable-gpu",
70 "--no-sandbox",
71 "--disable-setuid-sandbox",
72 window_size_arg.as_str(),
73 "--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
74 ];
75 chrome_options.insert("args".to_string(), json!(args));
76
77 let excluded_switches = vec!["enable-automation", "enable-logging"];
79 chrome_options.insert("excludeSwitches".to_string(), json!(excluded_switches));
80
81 let prefs = json!({
83 "credentials_enable_service": false,
84 "profile.password_manager_enabled": false,
85 });
86 chrome_options.insert("prefs".to_string(), prefs);
87
88 caps.insert("goog:chromeOptions".to_string(), json!(chrome_options));
89
90 let client = ClientBuilder::native()
92 .capabilities(caps)
93 .connect(&webdriver_url)
94 .await
95 .map_err(|e| color_eyre::eyre::eyre!("Failed to connect to ChromeDriver: {}", e))?;
96
97 Ok(Self {
98 client,
99 _chromedriver: chromedriver_process,
100 })
101 }
102
103 pub async fn goto(&self, url: &str) -> Result<()> {
105 self.client
106 .goto(url)
107 .await
108 .map_err(|e| color_eyre::eyre::eyre!("Failed to navigate: {}", e))?;
109
110 if !url.starts_with("file://") {
112 let _ = self
113 .client
114 .execute(COMPREHENSIVE_BOOTSTRAP, vec![])
115 .await;
116 }
117
118 Ok(())
119 }
120
121 pub async fn capture(&self, url: &str, _width: u32, _height: u32) -> Result<Vec<u8>> {
125 if !url.starts_with("file://") {
127 let _ = self
128 .client
129 .execute(COMPREHENSIVE_BOOTSTRAP, vec![])
130 .await;
131 }
132
133 self.goto(url).await?;
134
135 tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
137
138 if !url.starts_with("file://") {
140 let _ = self
141 .client
142 .execute(COMPREHENSIVE_BOOTSTRAP, vec![])
143 .await;
144 }
145
146 let screenshot_b64 = self
148 .client
149 .screenshot()
150 .await
151 .map_err(|e| color_eyre::eyre::eyre!("Failed to capture screenshot: {}", e))?;
152
153 let screenshot_bytes = base64::prelude::BASE64_STANDARD
155 .decode(&screenshot_b64)
156 .map_err(|e| color_eyre::eyre::eyre!("Failed to decode screenshot: {}", e))?;
157
158 Ok(screenshot_bytes)
159 }
160
161 pub fn client(&self) -> &fantoccini::Client {
163 &self.client
164 }
165
166 pub async fn close(mut self) -> Result<()> {
168 let _ = self.client.close().await;
169 let _ = self._chromedriver.kill();
170 Ok(())
171 }
172}