1use crate::configuration::Configuration;
2use crate::features::webdriver_args::get_browser_args;
3use crate::features::webdriver_common::{WebDriverBrowser, WebDriverConfig};
4use std::sync::Arc;
5use std::time::Duration;
6use thirtyfour::common::capabilities::desiredcapabilities::Capabilities;
7use thirtyfour::prelude::*;
8use tokio::task::JoinHandle;
9
10#[cfg(feature = "webdriver_stealth")]
12pub use spider_fingerprint::spoofs::{
13 spoof_device_memory, CLEANUP_CDP_MARKERS, HIDE_SELENIUM_MARKERS, HIDE_WEBDRIVER,
14};
15
16#[cfg(all(feature = "webdriver_stealth", not(feature = "serde")))]
18pub const STEALTH_SCRIPT: &str = r#"
19Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
20Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
21Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
22window.chrome = { runtime: {} };
23"#;
24
25pub type WebDriverControl = (Arc<WebDriver>, Option<JoinHandle<()>>, Option<String>);
27
28pub struct WebDriverController {
30 pub driver: WebDriverControl,
32 pub closed: bool,
34}
35
36impl WebDriverController {
37 pub fn new(driver: WebDriverControl) -> Self {
39 Self {
40 driver,
41 closed: false,
42 }
43 }
44
45 pub fn driver(&self) -> &Arc<WebDriver> {
47 &self.driver.0
48 }
49
50 pub fn dispose(&mut self) {
52 if !self.closed {
53 self.closed = true;
54 if let Some(handler) = self.driver.1.take() {
55 handler.abort();
56 }
57 }
58 }
59}
60
61impl Drop for WebDriverController {
62 fn drop(&mut self) {
63 self.dispose();
64 }
65}
66
67pub async fn launch_driver(config: &Configuration) -> Option<WebDriverController> {
69 let webdriver_config = config.webdriver_config.as_ref()?;
70 launch_driver_base(webdriver_config, config).await
71}
72
73pub async fn launch_driver_base(
75 webdriver_config: &WebDriverConfig,
76 config: &Configuration,
77) -> Option<WebDriverController> {
78 let server_url = &webdriver_config.server_url;
79
80 let caps = build_capabilities(webdriver_config, config).await?;
81
82 let mut attempts = 0;
83 let max_retries = 10;
84 let mut driver: Option<WebDriver> = None;
85
86 while attempts <= max_retries {
87 match WebDriver::new(server_url, caps.clone()).await {
88 Ok(d) => {
89 driver = Some(d);
90 break;
91 }
92 Err(err) => {
93 log::error!("WebDriver connection error: {:?}", err);
94 attempts += 1;
95 if attempts > max_retries {
96 log::error!("Exceeded maximum retry attempts for WebDriver connection");
97 break;
98 }
99 tokio::time::sleep(Duration::from_millis(500)).await;
100 }
101 }
102 }
103
104 let driver = driver?;
105 let driver_arc = Arc::new(driver);
106
107 if let (Some(width), Some(height)) = (
109 webdriver_config.viewport_width,
110 webdriver_config.viewport_height,
111 ) {
112 if let Err(e) = driver_arc.set_window_rect(0, 0, width, height).await {
113 log::warn!("Failed to set viewport: {:?}", e);
114 }
115 }
116
117 if let Some(timeout) = webdriver_config.timeout {
119 let timeouts = TimeoutConfiguration::new(Some(timeout), Some(timeout), Some(timeout));
120 if let Err(e) = driver_arc.update_timeouts(timeouts).await {
121 log::warn!("Failed to set timeouts: {:?}", e);
122 }
123 }
124
125 Some(WebDriverController::new((
126 driver_arc,
127 None,
128 Some(server_url.clone()),
129 )))
130}
131
132async fn build_capabilities(
134 webdriver_config: &WebDriverConfig,
135 config: &Configuration,
136) -> Option<Capabilities> {
137 match webdriver_config.browser {
138 WebDriverBrowser::Chrome => build_chrome_capabilities(webdriver_config, config).await,
139 WebDriverBrowser::Firefox => build_firefox_capabilities(webdriver_config, config).await,
140 WebDriverBrowser::Edge => build_edge_capabilities(webdriver_config, config).await,
141 }
142}
143
144async fn build_chrome_capabilities(
146 webdriver_config: &WebDriverConfig,
147 config: &Configuration,
148) -> Option<Capabilities> {
149 let mut caps = DesiredCapabilities::chrome();
150
151 if webdriver_config.accept_insecure_certs {
153 if let Err(e) = caps.accept_insecure_certs(true) {
154 log::warn!("Failed to set accept_insecure_certs: {:?}", e);
155 }
156 }
157
158 if let Some(ref strategy) = webdriver_config.page_load_strategy {
160 let strategy = match strategy.as_str() {
161 "eager" => thirtyfour::PageLoadStrategy::Eager,
162 "none" => thirtyfour::PageLoadStrategy::None,
163 _ => thirtyfour::PageLoadStrategy::Normal,
164 };
165 if let Err(e) = caps.set_page_load_strategy(strategy) {
166 log::warn!("Failed to set page_load_strategy: {:?}", e);
167 }
168 }
169
170 let mut args: Vec<String> = Vec::new();
172
173 let default_args = get_browser_args(&WebDriverBrowser::Chrome);
175 for arg in default_args {
176 args.push(arg.to_string());
177 }
178
179 if let Some(ref custom_args) = webdriver_config.browser_args {
181 args.extend(custom_args.clone());
182 }
183
184 if webdriver_config.headless && !args.iter().any(|a| a.contains("headless")) {
186 args.push("--headless".to_string());
187 }
188
189 if let Some(ref ua) = webdriver_config.user_agent {
191 args.push(format!("--user-agent={}", ua));
192 } else if let Some(ref ua) = config.user_agent {
193 args.push(format!("--user-agent={}", ua));
194 }
195
196 if let Some(ref proxy) = webdriver_config.proxy {
198 args.push(format!("--proxy-server={}", proxy));
199 }
200
201 if let (Some(width), Some(height)) = (
203 webdriver_config.viewport_width,
204 webdriver_config.viewport_height,
205 ) {
206 args.push(format!("--window-size={},{}", width, height));
207 }
208
209 for arg in args {
211 if let Err(e) = caps.add_arg(&arg) {
212 log::warn!("Failed to add Chrome arg '{}': {:?}", arg, e);
213 }
214 }
215
216 Some(caps.into())
217}
218
219async fn build_firefox_capabilities(
221 webdriver_config: &WebDriverConfig,
222 _config: &Configuration,
223) -> Option<Capabilities> {
224 let mut caps = DesiredCapabilities::firefox();
225
226 if webdriver_config.accept_insecure_certs {
228 if let Err(e) = caps.accept_insecure_certs(true) {
229 log::warn!("Failed to set accept_insecure_certs: {:?}", e);
230 }
231 }
232
233 if let Some(ref strategy) = webdriver_config.page_load_strategy {
235 let strategy = match strategy.as_str() {
236 "eager" => thirtyfour::PageLoadStrategy::Eager,
237 "none" => thirtyfour::PageLoadStrategy::None,
238 _ => thirtyfour::PageLoadStrategy::Normal,
239 };
240 if let Err(e) = caps.set_page_load_strategy(strategy) {
241 log::warn!("Failed to set page_load_strategy: {:?}", e);
242 }
243 }
244
245 let mut args: Vec<String> = Vec::new();
247
248 let default_args = get_browser_args(&WebDriverBrowser::Firefox);
250 for arg in default_args {
251 args.push(arg.to_string());
252 }
253
254 if let Some(ref custom_args) = webdriver_config.browser_args {
256 args.extend(custom_args.clone());
257 }
258
259 if webdriver_config.headless && !args.iter().any(|a| a.contains("headless")) {
261 args.push("-headless".to_string());
262 }
263
264 for arg in args {
266 if let Err(e) = caps.add_arg(&arg) {
267 log::warn!("Failed to add Firefox arg '{}': {:?}", arg, e);
268 }
269 }
270
271 Some(caps.into())
272}
273
274async fn build_edge_capabilities(
276 webdriver_config: &WebDriverConfig,
277 config: &Configuration,
278) -> Option<Capabilities> {
279 let mut caps = DesiredCapabilities::edge();
280
281 if webdriver_config.accept_insecure_certs {
283 if let Err(e) = caps.accept_insecure_certs(true) {
284 log::warn!("Failed to set accept_insecure_certs: {:?}", e);
285 }
286 }
287
288 if let Some(ref strategy) = webdriver_config.page_load_strategy {
290 let strategy = match strategy.as_str() {
291 "eager" => thirtyfour::PageLoadStrategy::Eager,
292 "none" => thirtyfour::PageLoadStrategy::None,
293 _ => thirtyfour::PageLoadStrategy::Normal,
294 };
295 if let Err(e) = caps.set_page_load_strategy(strategy) {
296 log::warn!("Failed to set page_load_strategy: {:?}", e);
297 }
298 }
299
300 let mut args: Vec<String> = Vec::new();
302
303 let default_args = get_browser_args(&WebDriverBrowser::Edge);
305 for arg in default_args {
306 args.push(arg.to_string());
307 }
308
309 if let Some(ref custom_args) = webdriver_config.browser_args {
311 args.extend(custom_args.clone());
312 }
313
314 if webdriver_config.headless && !args.iter().any(|a| a.contains("headless")) {
316 args.push("--headless".to_string());
317 }
318
319 if let Some(ref ua) = webdriver_config.user_agent {
321 args.push(format!("--user-agent={}", ua));
322 } else if let Some(ref ua) = config.user_agent {
323 args.push(format!("--user-agent={}", ua));
324 }
325
326 if let Some(ref proxy) = webdriver_config.proxy {
328 args.push(format!("--proxy-server={}", proxy));
329 }
330
331 if let (Some(width), Some(height)) = (
333 webdriver_config.viewport_width,
334 webdriver_config.viewport_height,
335 ) {
336 args.push(format!("--window-size={},{}", width, height));
337 }
338
339 for arg in args {
341 if let Err(e) = caps.add_arg(&arg) {
342 log::warn!("Failed to add Edge arg '{}': {:?}", arg, e);
343 }
344 }
345
346 Some(caps.into())
347}
348
349#[cfg(feature = "webdriver_stealth")]
356pub async fn setup_driver_events(driver: &WebDriver, _config: &Configuration) {
357 if let Err(e) = driver.execute(HIDE_WEBDRIVER, vec![]).await {
359 log::warn!("Failed to inject webdriver hiding script: {:?}", e);
360 }
361
362 let device_memory_script = spoof_device_memory(8);
365 if let Err(e) = driver.execute(&device_memory_script, vec![]).await {
366 log::warn!("Failed to inject device memory script: {:?}", e);
367 }
368
369 if let Err(e) = driver.execute(CLEANUP_CDP_MARKERS, vec![]).await {
371 log::warn!("Failed to inject CDP marker cleanup script: {:?}", e);
372 }
373
374 if let Err(e) = driver.execute(HIDE_SELENIUM_MARKERS, vec![]).await {
376 log::warn!("Failed to inject Selenium marker cleanup script: {:?}", e);
377 }
378}
379
380#[cfg(not(feature = "webdriver_stealth"))]
382pub async fn setup_driver_events(_driver: &WebDriver, _config: &Configuration) {
383 }
385
386pub async fn attempt_navigation(
388 url: &str,
389 driver: &WebDriver,
390 timeout: &Option<Duration>,
391) -> Result<(), WebDriverError> {
392 let nav_future = driver.goto(url);
393
394 match timeout {
395 Some(t) => match tokio::time::timeout(*t, nav_future).await {
396 Ok(result) => result,
397 Err(_) => Err(WebDriverError::Timeout("Navigation timeout".to_string())),
398 },
399 None => nav_future.await,
400 }
401}
402
403pub async fn get_page_content(driver: &WebDriver) -> Result<String, WebDriverError> {
405 driver.source().await
406}
407
408pub async fn get_current_url(driver: &WebDriver) -> Result<String, WebDriverError> {
410 driver.current_url().await.map(|u| u.to_string())
411}
412
413pub async fn get_page_title(driver: &WebDriver) -> Result<String, WebDriverError> {
415 driver.title().await
416}
417
418#[cfg(feature = "webdriver_screenshot")]
420pub async fn take_screenshot(driver: &WebDriver) -> Result<Vec<u8>, WebDriverError> {
421 driver.screenshot_as_png().await
422}
423
424#[cfg(not(feature = "webdriver_screenshot"))]
426pub async fn take_screenshot(_driver: &WebDriver) -> Result<Vec<u8>, WebDriverError> {
427 Err(WebDriverError::FatalError(
428 "Screenshot feature not enabled".to_string(),
429 ))
430}
431
432pub async fn execute_script(
434 driver: &WebDriver,
435 script: &str,
436) -> Result<serde_json::Value, WebDriverError> {
437 let result = driver.execute(script, vec![]).await?;
438 Ok(result.json().clone())
439}
440
441pub async fn wait_for_element(
443 driver: &WebDriver,
444 selector: &str,
445 timeout: Duration,
446) -> Result<WebElement, WebDriverError> {
447 driver
448 .query(By::Css(selector))
449 .wait(timeout, Duration::from_millis(100))
450 .first()
451 .await
452}
453
454pub async fn close_driver(driver: WebDriver) {
456 if let Err(e) = driver.quit().await {
457 log::warn!("Failed to close WebDriver session: {:?}", e);
458 }
459}
460
461#[cfg(feature = "real_browser")]
463pub fn get_random_webdriver_viewport() -> (u32, u32) {
464 use super::chrome_viewport::get_random_viewport;
465 let vp = get_random_viewport();
466 (vp.width, vp.height)
467}
468
469#[cfg(not(feature = "real_browser"))]
471pub fn get_random_webdriver_viewport() -> (u32, u32) {
472 (1920, 1080)
473}
474
475use crate::features::chrome_common::WebAutomation;
480
481pub async fn run_automation(driver: &WebDriver, action: &WebAutomation) -> bool {
483 let mut valid = false;
484
485 match action {
486 WebAutomation::Evaluate(js) => {
487 valid = driver.execute(js.as_str(), vec![]).await.is_ok();
488 }
489 WebAutomation::Click(selector) => {
490 if let Ok(ele) = driver.find(By::Css(selector)).await {
491 valid = ele.click().await.is_ok();
492 }
493 }
494 WebAutomation::ClickAll(selector) => {
495 if let Ok(eles) = driver.find_all(By::Css(selector)).await {
496 for ele in eles {
497 valid = ele.click().await.is_ok();
498 }
499 }
500 }
501 WebAutomation::ClickPoint { x, y } => {
502 let js = format!("document.elementFromPoint({}, {})?.click()", x, y);
504 valid = driver.execute(&js, vec![]).await.is_ok();
505 }
506 WebAutomation::ClickHold { selector, hold_ms } => {
507 let js = format!(
509 r#"
510 const el = document.querySelector('{}');
511 if (el) {{
512 const evt = new MouseEvent('mousedown', {{ bubbles: true }});
513 el.dispatchEvent(evt);
514 await new Promise(r => setTimeout(r, {}));
515 el.dispatchEvent(new MouseEvent('mouseup', {{ bubbles: true }}));
516 }}
517 "#,
518 selector.replace('\'', "\\'"),
519 hold_ms
520 );
521 valid = driver.execute(&js, vec![]).await.is_ok();
522 }
523 WebAutomation::ClickHoldPoint { x, y, hold_ms } => {
524 let js = format!(
525 r#"
526 const el = document.elementFromPoint({}, {});
527 if (el) {{
528 el.dispatchEvent(new MouseEvent('mousedown', {{ bubbles: true }}));
529 await new Promise(r => setTimeout(r, {}));
530 el.dispatchEvent(new MouseEvent('mouseup', {{ bubbles: true }}));
531 }}
532 "#,
533 x, y, hold_ms
534 );
535 valid = driver.execute(&js, vec![]).await.is_ok();
536 }
537 WebAutomation::ClickDrag {
538 from,
539 to,
540 modifier: _,
541 } => {
542 let js = format!(
544 r#"
545 const fromEl = document.querySelector('{}');
546 const toEl = document.querySelector('{}');
547 if (fromEl && toEl) {{
548 const fromRect = fromEl.getBoundingClientRect();
549 const toRect = toEl.getBoundingClientRect();
550 fromEl.dispatchEvent(new MouseEvent('mousedown', {{ bubbles: true, clientX: fromRect.x, clientY: fromRect.y }}));
551 toEl.dispatchEvent(new MouseEvent('mousemove', {{ bubbles: true, clientX: toRect.x, clientY: toRect.y }}));
552 toEl.dispatchEvent(new MouseEvent('mouseup', {{ bubbles: true, clientX: toRect.x, clientY: toRect.y }}));
553 }}
554 "#,
555 from.replace('\'', "\\'"),
556 to.replace('\'', "\\'")
557 );
558 valid = driver.execute(&js, vec![]).await.is_ok();
559 }
560 WebAutomation::ClickDragPoint {
561 from_x,
562 from_y,
563 to_x,
564 to_y,
565 modifier: _,
566 } => {
567 let js = format!(
568 r#"
569 const fromEl = document.elementFromPoint({}, {});
570 const toEl = document.elementFromPoint({}, {});
571 if (fromEl) {{
572 fromEl.dispatchEvent(new MouseEvent('mousedown', {{ bubbles: true, clientX: {}, clientY: {} }}));
573 (toEl || fromEl).dispatchEvent(new MouseEvent('mousemove', {{ bubbles: true, clientX: {}, clientY: {} }}));
574 (toEl || fromEl).dispatchEvent(new MouseEvent('mouseup', {{ bubbles: true, clientX: {}, clientY: {} }}));
575 }}
576 "#,
577 from_x, from_y, to_x, to_y, from_x, from_y, to_x, to_y, to_x, to_y
578 );
579 valid = driver.execute(&js, vec![]).await.is_ok();
580 }
581 WebAutomation::ClickAllClickable() => {
582 let clickable_selector =
583 "a, button, input[type='button'], input[type='submit'], [onclick], [role='button']";
584 if let Ok(eles) = driver.find_all(By::Css(clickable_selector)).await {
585 for ele in eles {
586 let _ = ele.click().await;
587 valid = true;
588 }
589 }
590 }
591 WebAutomation::Wait(ms) => {
592 tokio::time::sleep(Duration::from_millis(*ms)).await;
593 valid = true;
594 }
595 WebAutomation::WaitForNavigation => {
596 let js = r#"
598 return new Promise(resolve => {
599 if (document.readyState === 'complete') {
600 resolve(true);
601 } else {
602 window.addEventListener('load', () => resolve(true));
603 }
604 });
605 "#;
606 valid = driver.execute(js, vec![]).await.is_ok();
607 }
608 WebAutomation::WaitForDom { selector, timeout } => {
609 let timeout_duration = Duration::from_millis(*timeout as u64);
610 if let Some(sel) = selector {
611 valid = driver
612 .query(By::Css(sel))
613 .wait(timeout_duration, Duration::from_millis(100))
614 .first()
615 .await
616 .is_ok();
617 } else {
618 tokio::time::sleep(timeout_duration).await;
620 valid = true;
621 }
622 }
623 WebAutomation::WaitFor(selector) => {
624 valid = driver
625 .query(By::Css(selector))
626 .wait(Duration::from_secs(60), Duration::from_millis(100))
627 .first()
628 .await
629 .is_ok();
630 }
631 WebAutomation::WaitForWithTimeout { selector, timeout } => {
632 valid = driver
633 .query(By::Css(selector))
634 .wait(Duration::from_millis(*timeout), Duration::from_millis(100))
635 .first()
636 .await
637 .is_ok();
638 }
639 WebAutomation::WaitForAndClick(selector) => {
640 if let Ok(ele) = driver
641 .query(By::Css(selector))
642 .wait(Duration::from_secs(60), Duration::from_millis(100))
643 .first()
644 .await
645 {
646 valid = ele.click().await.is_ok();
647 }
648 }
649 WebAutomation::ScrollX(px) => {
650 let js = format!("window.scrollBy({}, 0)", px);
651 valid = driver.execute(&js, vec![]).await.is_ok();
652 }
653 WebAutomation::ScrollY(px) => {
654 let js = format!("window.scrollBy(0, {})", px);
655 valid = driver.execute(&js, vec![]).await.is_ok();
656 }
657 WebAutomation::InfiniteScroll(duration) => {
658 let timeout = (*duration).min(300); let js = format!(
660 r#"
661 const endTime = Date.now() + {} * 1000;
662 const scroll = () => {{
663 window.scrollBy(0, window.innerHeight);
664 if (Date.now() < endTime) {{
665 setTimeout(scroll, 500);
666 }}
667 }};
668 scroll();
669 "#,
670 timeout
671 );
672 valid = driver.execute(&js, vec![]).await.is_ok();
673 tokio::time::sleep(Duration::from_secs(timeout as u64)).await;
675 }
676 WebAutomation::Fill { selector, value } => {
677 if let Ok(ele) = driver.find(By::Css(selector)).await {
678 if ele.clear().await.is_ok() {
680 valid = ele.send_keys(value).await.is_ok();
681 }
682 }
683 }
684 WebAutomation::Type { value, modifier: _ } => {
685 let js = format!(
687 r#"
688 const el = document.activeElement;
689 if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable)) {{
690 el.value = (el.value || '') + '{}';
691 el.dispatchEvent(new Event('input', {{ bubbles: true }}));
692 }}
693 "#,
694 value.replace('\'', "\\'").replace('\n', "\\n")
695 );
696 valid = driver.execute(&js, vec![]).await.is_ok();
697 }
698 WebAutomation::Screenshot {
699 full_page: _,
700 omit_background: _,
701 output,
702 } => {
703 #[cfg(feature = "webdriver_screenshot")]
704 {
705 if let Ok(png_data) = driver.screenshot_as_png().await {
706 valid = crate::utils::uring_fs::write_file(output.clone(), png_data)
707 .await
708 .is_ok();
709 }
710 }
711 #[cfg(not(feature = "webdriver_screenshot"))]
712 {
713 let _ = output;
714 log::warn!("Screenshot feature not enabled");
715 }
716 }
717 WebAutomation::ValidateChain => {
718 valid = true;
720 }
721 }
722
723 valid
724}
725
726pub async fn run_automation_scripts(driver: &WebDriver, scripts: &[WebAutomation]) -> bool {
728 let mut valid = false;
729
730 for script in scripts {
731 if script == &WebAutomation::ValidateChain && !valid {
732 break;
733 }
734 match tokio::time::timeout(Duration::from_secs(60), run_automation(driver, script)).await {
735 Ok(result) => valid = result,
736 Err(_) => {
737 log::warn!("Automation script timed out: {:?}", script.name());
738 valid = false;
739 }
740 }
741 }
742
743 valid
744}
745
746pub async fn run_execution_scripts(
748 driver: &WebDriver,
749 url: &str,
750 execution_scripts: &Option<crate::features::chrome_common::ExecutionScripts>,
751) {
752 if let Some(scripts) = execution_scripts {
753 if let Some(js) = scripts.search(url) {
754 if let Err(e) = driver.execute(js.as_str(), vec![]).await {
755 log::warn!("Failed to execute script for {}: {:?}", url, e);
756 }
757 }
758 }
759}
760
761pub async fn run_url_automation_scripts(
763 driver: &WebDriver,
764 url: &str,
765 automation_scripts: &Option<crate::features::chrome_common::AutomationScripts>,
766) -> bool {
767 if let Some(scripts) = automation_scripts {
768 if let Some(actions) = scripts.search(url) {
769 return run_automation_scripts(driver, actions).await;
770 }
771 }
772 true
773}