1use std::future::Future;
2use std::io;
3
4use futures::SinkExt;
5use futures::channel::mpsc::{Sender, channel, unbounded};
6use futures::channel::oneshot::channel as oneshot_channel;
7use futures::select;
8
9use chromiumoxide_cdp::cdp::browser_protocol::browser::{
10 BrowserContextId, CloseReturns, GetVersionParams, GetVersionReturns,
11};
12use chromiumoxide_cdp::cdp::browser_protocol::network::{Cookie, CookieParam};
13use chromiumoxide_cdp::cdp::browser_protocol::storage::{
14 ClearCookiesParams, GetCookiesParams, SetCookiesParams,
15};
16use chromiumoxide_cdp::cdp::browser_protocol::target::{
17 CreateBrowserContextParams, CreateTargetParams, DisposeBrowserContextParams, TargetId,
18 TargetInfo,
19};
20use chromiumoxide_cdp::cdp::{CdpEventMessage, IntoEventKind};
21use chromiumoxide_types::*;
22
23pub use self::config::{BrowserConfig, BrowserConfigBuilder, LAUNCH_TIMEOUT};
24use crate::async_process::{Child, ExitStatus};
25use crate::cmd::{CommandMessage, to_command_response};
26use crate::conn::Connection;
27use crate::error::{BrowserStderr, CdpError, Result};
28use crate::handler::browser::BrowserContext;
29use crate::handler::{Handler, HandlerConfig, HandlerMessage};
30use crate::listeners::{EventListenerRequest, EventStream};
31use crate::page::Page;
32use crate::utils;
33
34mod argument;
35mod config;
36
37#[derive(Debug)]
39pub struct Browser {
40 sender: Sender<HandlerMessage>,
43 config: Option<BrowserConfig>,
45 child: Option<Child>,
47 debug_ws_url: String,
49 browser_context: BrowserContext,
51}
52
53#[derive(serde::Deserialize, Debug, Default)]
55pub struct BrowserConnection {
56 #[serde(rename = "Browser")]
57 pub browser: String,
59 #[serde(rename = "Protocol-Version")]
60 pub protocol_version: String,
62 #[serde(rename = "User-Agent")]
63 pub user_agent: String,
65 #[serde(rename = "V8-Version")]
66 pub v8_version: String,
68 #[serde(rename = "WebKit-Version")]
69 pub webkit_version: String,
71 #[serde(rename = "webSocketDebuggerUrl")]
72 pub web_socket_debugger_url: String,
74}
75
76impl Browser {
77 pub async fn connect(url: impl Into<String>) -> Result<(Self, Handler)> {
81 Self::connect_with_config(url, HandlerConfig::default()).await
82 }
83
84 pub async fn connect_with_config(
88 url: impl Into<String>,
89 config: HandlerConfig,
90 ) -> Result<(Self, Handler)> {
91 let mut debug_ws_url = url.into();
92
93 if debug_ws_url.starts_with("http") {
94 match reqwest::Client::new()
95 .get(
96 if debug_ws_url.ends_with("/json/version")
97 || debug_ws_url.ends_with("/json/version/")
98 {
99 debug_ws_url.clone()
100 } else {
101 format!(
102 "{}{}json/version",
103 &debug_ws_url,
104 if debug_ws_url.ends_with('/') { "" } else { "/" }
105 )
106 },
107 )
108 .header("content-type", "application/json")
109 .send()
110 .await
111 {
112 Ok(req) => {
113 let socketaddr = req.remote_addr().unwrap();
114 let connection: BrowserConnection =
115 serde_json::from_slice(&req.bytes().await.unwrap_or_default())
116 .unwrap_or_default();
117
118 if !connection.web_socket_debugger_url.is_empty() {
119 debug_ws_url = connection
121 .web_socket_debugger_url
122 .replace("127.0.0.1", &socketaddr.ip().to_string());
123 }
124 }
125 Err(_) => return Err(CdpError::NoResponse),
126 }
127 }
128
129 let conn = Connection::<CdpEventMessage>::connect(&debug_ws_url).await?;
130
131 let (tx, rx) = channel(1);
132
133 let fut = Handler::new(conn, rx, config);
134 let browser_context = fut.default_browser_context().clone();
135
136 let browser = Self {
137 sender: tx,
138 config: None,
139 child: None,
140 debug_ws_url,
141 browser_context,
142 };
143 Ok((browser, fut))
144 }
145
146 pub async fn launch(mut config: BrowserConfig) -> Result<(Self, Handler)> {
155 config.executable = utils::canonicalize_except_snap(config.executable).await?;
157
158 let mut child = config.launch()?;
160
161 async fn with_child(
166 config: &BrowserConfig,
167 child: &mut Child,
168 ) -> Result<(String, Connection<CdpEventMessage>)> {
169 let dur = config.launch_timeout;
170 let timeout_fut = Box::pin(tokio::time::sleep(dur));
171
172 let debug_ws_url = ws_url_from_output(child, timeout_fut).await?;
174 let conn = Connection::<CdpEventMessage>::connect(&debug_ws_url).await?;
175 Ok((debug_ws_url, conn))
176 }
177
178 let (debug_ws_url, conn) = match with_child(&config, &mut child).await {
179 Ok(conn) => conn,
180 Err(e) => {
181 if let Ok(Some(_)) = child.try_wait() {
183 } else {
185 child.kill().await.expect("`Browser::launch` failed but could not clean-up the child process (`kill`)");
187 child.wait().await.expect("`Browser::launch` failed but could not clean-up the child process (`wait`)");
188 }
189 return Err(e);
190 }
191 };
192
193 let (tx, rx) = channel(1);
197
198 let handler_config = HandlerConfig {
199 ignore_https_errors: config.ignore_https_errors,
200 ignore_invalid_messages: config.ignore_invalid_messages,
201 viewport: config.viewport.clone(),
202 context_ids: Vec::new(),
203 request_timeout: config.request_timeout,
204 request_intercept: config.request_intercept,
205 cache_enabled: config.cache_enabled,
206 };
207
208 let fut = Handler::new(conn, rx, handler_config);
209 let browser_context = fut.default_browser_context().clone();
210
211 let browser = Self {
212 sender: tx,
213 config: Some(config),
214 child: Some(child),
215 debug_ws_url,
216 browser_context,
217 };
218
219 Ok((browser, fut))
220 }
221
222 pub async fn fetch_targets(&mut self) -> Result<Vec<TargetInfo>> {
232 let (tx, rx) = oneshot_channel();
233
234 self.sender
235 .clone()
236 .send(HandlerMessage::FetchTargets(tx))
237 .await?;
238
239 rx.await?
240 }
241
242 pub async fn close(&mut self) -> Result<CloseReturns> {
249 let (tx, rx) = oneshot_channel();
250
251 self.sender
252 .clone()
253 .send(HandlerMessage::CloseBrowser(tx))
254 .await?;
255
256 rx.await?
257 }
258
259 pub async fn wait(&mut self) -> io::Result<Option<ExitStatus>> {
268 if let Some(child) = self.child.as_mut() {
269 Ok(Some(child.wait().await?))
270 } else {
271 Ok(None)
272 }
273 }
274
275 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
284 if let Some(child) = self.child.as_mut() {
285 child.try_wait()
286 } else {
287 Ok(None)
288 }
289 }
290
291 pub fn get_mut_child(&mut self) -> Option<&mut Child> {
302 self.child.as_mut()
303 }
304
305 pub async fn kill(&mut self) -> Option<io::Result<()>> {
316 match self.child.as_mut() {
317 Some(child) => Some(child.kill().await),
318 None => None,
319 }
320 }
321
322 pub async fn start_incognito_context(&mut self) -> Result<&mut Self> {
328 if !self.is_incognito_configured() {
329 let browser_context_id = self
330 .create_browser_context(CreateBrowserContextParams::default())
331 .await?;
332 self.browser_context = BrowserContext::from(browser_context_id);
333 self.sender
334 .clone()
335 .send(HandlerMessage::InsertContext(self.browser_context.clone()))
336 .await?;
337 }
338
339 Ok(self)
340 }
341
342 pub async fn quit_incognito_context(&mut self) -> Result<&mut Self> {
348 if let Some(id) = self.browser_context.take() {
349 self.dispose_browser_context(id.clone()).await?;
350 self.sender
351 .clone()
352 .send(HandlerMessage::DisposeContext(BrowserContext::from(id)))
353 .await?;
354 }
355 Ok(self)
356 }
357
358 fn is_incognito_configured(&self) -> bool {
360 self.config
361 .as_ref()
362 .map(|c| c.incognito)
363 .unwrap_or_default()
364 }
365
366 pub fn websocket_address(&self) -> &String {
368 &self.debug_ws_url
369 }
370
371 pub fn is_incognito(&self) -> bool {
373 self.is_incognito_configured() || self.browser_context.is_incognito()
374 }
375
376 pub fn config(&self) -> Option<&BrowserConfig> {
378 self.config.as_ref()
379 }
380
381 pub async fn new_page(&self, params: impl Into<CreateTargetParams>) -> Result<Page> {
383 let (tx, rx) = oneshot_channel();
384 let mut params = params.into();
385 if let Some(id) = self.browser_context.id() {
386 if params.browser_context_id.is_none() {
387 params.browser_context_id = Some(id.clone());
388 }
389 }
390
391 self.sender
392 .clone()
393 .send(HandlerMessage::CreatePage(params, tx))
394 .await?;
395
396 rx.await?
397 }
398
399 pub async fn version(&self) -> Result<GetVersionReturns> {
401 Ok(self.execute(GetVersionParams::default()).await?.result)
402 }
403
404 pub async fn user_agent(&self) -> Result<String> {
406 Ok(self.version().await?.user_agent)
407 }
408
409 pub async fn execute<T: Command>(&self, cmd: T) -> Result<CommandResponse<T::Response>> {
411 let (tx, rx) = oneshot_channel();
412 let method = cmd.identifier();
413 let msg = CommandMessage::new(cmd, tx)?;
414
415 self.sender
416 .clone()
417 .send(HandlerMessage::Command(msg))
418 .await?;
419 let resp = rx.await??;
420 to_command_response::<T>(resp, method)
421 }
422
423 pub async fn pages(&self) -> Result<Vec<Page>> {
425 let (tx, rx) = oneshot_channel();
426 self.sender
427 .clone()
428 .send(HandlerMessage::GetPages(tx))
429 .await?;
430 Ok(rx.await?)
431 }
432
433 pub async fn get_page(&self, target_id: TargetId) -> Result<Page> {
435 let (tx, rx) = oneshot_channel();
436 self.sender
437 .clone()
438 .send(HandlerMessage::GetPage(target_id, tx))
439 .await?;
440 rx.await?.ok_or(CdpError::NotFound)
441 }
442
443 pub async fn event_listener<T: IntoEventKind>(&self) -> Result<EventStream<T>> {
445 let (tx, rx) = unbounded();
446 self.sender
447 .clone()
448 .send(HandlerMessage::AddEventListener(
449 EventListenerRequest::new::<T>(tx),
450 ))
451 .await?;
452
453 Ok(EventStream::new(rx))
454 }
455
456 pub async fn create_browser_context(
458 &self,
459 params: CreateBrowserContextParams,
460 ) -> Result<BrowserContextId> {
461 let response = self.execute(params).await?;
462 Ok(response.result.browser_context_id)
463 }
464
465 pub async fn dispose_browser_context(
467 &self,
468 browser_context_id: impl Into<BrowserContextId>,
469 ) -> Result<()> {
470 self.execute(DisposeBrowserContextParams::new(browser_context_id))
471 .await?;
472
473 Ok(())
474 }
475
476 pub async fn create_incognito_context_with_proxy(
482 &self,
483 proxy_server: impl Into<String>,
484 ) -> Result<BrowserContextId> {
485 let params = CreateBrowserContextParams::builder()
486 .proxy_server(proxy_server)
487 .build();
488 self.create_browser_context(params).await
489 }
490
491 pub async fn clear_cookies(&self) -> Result<()> {
493 self.execute(ClearCookiesParams::default()).await?;
494 Ok(())
495 }
496
497 pub async fn get_cookies(&self) -> Result<Vec<Cookie>> {
499 Ok(self
500 .execute(GetCookiesParams::default())
501 .await?
502 .result
503 .cookies)
504 }
505
506 pub async fn set_cookies(&self, mut cookies: Vec<CookieParam>) -> Result<&Self> {
508 for cookie in &mut cookies {
509 if let Some(url) = cookie.url.as_ref() {
510 crate::page::validate_cookie_url(url)?;
511 }
512 }
513
514 self.execute(SetCookiesParams::new(cookies)).await?;
515 Ok(self)
516 }
517}
518
519impl Drop for Browser {
520 fn drop(&mut self) {
521 if let Some(child) = self.child.as_mut() {
522 if let Ok(Some(_)) = child.try_wait() {
523 } else {
525 tracing::warn!(
533 "Browser was not closed manually, it will be killed automatically in the background"
534 );
535 }
536 }
537 }
538}
539
540async fn ws_url_from_output(
550 child_process: &mut Child,
551 timeout_fut: impl Future<Output = ()> + Unpin,
552) -> Result<String> {
553 use futures::{AsyncBufReadExt, FutureExt};
554 let mut timeout_fut = timeout_fut.fuse();
555 let stderr = child_process.stderr.take().expect("no stderror");
556 let mut stderr_bytes = Vec::<u8>::new();
557 let mut exit_status_fut = Box::pin(child_process.wait()).fuse();
558 let mut buf = futures::io::BufReader::new(stderr);
559 loop {
560 select! {
561 _ = timeout_fut => return Err(CdpError::LaunchTimeout(BrowserStderr::new(stderr_bytes))),
562 exit_status = exit_status_fut => {
563 return Err(match exit_status {
564 Err(e) => CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)),
565 Ok(exit_status) => CdpError::LaunchExit(exit_status, BrowserStderr::new(stderr_bytes)),
566 })
567 },
568 read_res = buf.read_until(b'\n', &mut stderr_bytes).fuse() => {
569 match read_res {
570 Err(e) => return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes))),
571 Ok(byte_count) => {
572 if byte_count == 0 {
573 let e = io::Error::new(io::ErrorKind::UnexpectedEof, "unexpected end of stream");
574 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
575 }
576 let start_offset = stderr_bytes.len() - byte_count;
577 let new_bytes = &stderr_bytes[start_offset..];
578 match std::str::from_utf8(new_bytes) {
579 Err(_) => {
580 let e = io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8");
581 return Err(CdpError::LaunchIo(e, BrowserStderr::new(stderr_bytes)));
582 }
583 Ok(line) => {
584 if let Some((_, ws)) = line.rsplit_once("listening on ") {
585 if ws.starts_with("ws") && ws.contains("devtools/browser") {
586 return Ok(ws.trim().to_string());
587 }
588 }
589 }
590 }
591 }
592 }
593 }
594 }
595 }
596}