Skip to main content

chromiumoxide/handler/
mod.rs

1use crate::listeners::{EventListenerRequest, EventListeners};
2use chromiumoxide_cdp::cdp::browser_protocol::browser::*;
3use chromiumoxide_cdp::cdp::browser_protocol::target::*;
4use chromiumoxide_cdp::cdp::events::CdpEvent;
5use chromiumoxide_cdp::cdp::events::CdpEventMessage;
6use chromiumoxide_types::{CallId, Message, Method, Response};
7use chromiumoxide_types::{MethodId, Request as CdpRequest};
8use fnv::FnvHashMap;
9use futures_util::Stream;
10use hashbrown::{HashMap, HashSet};
11use spider_network_blocker::intercept_manager::NetworkInterceptManager;
12use std::pin::Pin;
13use std::task::{Context, Poll};
14use std::time::{Duration, Instant};
15use tokio::sync::mpsc::Receiver;
16use tokio::sync::oneshot::Sender as OneshotSender;
17use tokio_tungstenite::tungstenite::error::ProtocolError;
18use tokio_tungstenite::tungstenite::Error;
19
20use crate::cmd::{to_command_response, CommandMessage};
21use crate::conn::Connection;
22use crate::error::{CdpError, Result};
23use crate::handler::browser::BrowserContext;
24use crate::handler::frame::FrameRequestedNavigation;
25use crate::handler::frame::{NavigationError, NavigationId, NavigationOk};
26use crate::handler::job::PeriodicJob;
27use crate::handler::session::Session;
28use crate::handler::target::TargetEvent;
29use crate::handler::target::{Target, TargetConfig};
30use crate::handler::viewport::Viewport;
31use crate::page::Page;
32pub(crate) use page::PageInner;
33
34/// Standard timeout in MS
35pub const REQUEST_TIMEOUT: u64 = 30_000;
36
37pub mod blockers;
38pub mod browser;
39pub mod commandfuture;
40pub mod domworld;
41pub mod emulation;
42pub mod frame;
43pub mod http;
44pub mod httpfuture;
45mod job;
46pub mod network;
47pub mod network_utils;
48pub mod page;
49mod session;
50pub mod target;
51pub mod target_message_future;
52pub mod viewport;
53
54/// The handler that monitors the state of the chromium browser and drives all
55/// the requests and events.
56#[must_use = "streams do nothing unless polled"]
57#[derive(Debug)]
58pub struct Handler {
59    pub default_browser_context: BrowserContext,
60    pub browser_contexts: HashSet<BrowserContext>,
61    /// Commands that are being processed and awaiting a response from the
62    /// chromium instance together with the timestamp when the request
63    /// started.
64    pending_commands: FnvHashMap<CallId, (PendingRequest, MethodId, Instant)>,
65    /// Connection to the browser instance
66    from_browser: Receiver<HandlerMessage>,
67    /// Used to loop over all targets in a consistent manner
68    target_ids: Vec<TargetId>,
69    /// The created and attached targets
70    targets: HashMap<TargetId, Target>,
71    /// Currently queued in navigations for targets
72    navigations: FnvHashMap<NavigationId, NavigationRequest>,
73    /// Keeps track of all the current active sessions
74    ///
75    /// There can be multiple sessions per target.
76    sessions: HashMap<SessionId, Session>,
77    /// The websocket connection to the chromium instance
78    conn: Connection<CdpEventMessage>,
79    /// Evicts timed out requests periodically
80    evict_command_timeout: PeriodicJob,
81    /// The internal identifier for a specific navigation
82    next_navigation_id: usize,
83    /// How this handler will configure targets etc,
84    config: HandlerConfig,
85    /// All registered event subscriptions
86    event_listeners: EventListeners,
87    /// Keeps track is the browser is closing
88    closing: bool,
89    /// Track the bytes remainder until network request will be blocked.
90    remaining_bytes: Option<u64>,
91    /// The budget is exhausted.
92    budget_exhausted: bool,
93    /// Tracks which targets we've already attached to, to avoid multiple sessions per target.
94    attached_targets: HashSet<TargetId>,
95}
96
97lazy_static::lazy_static! {
98    /// Set the discovery ID target.
99    static ref DISCOVER_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
100        let discover = SetDiscoverTargetsParams::new(true);
101        (discover.identifier(), serde_json::to_value(discover).expect("valid discover target params"))
102    };
103    /// Targets params id.
104    static ref TARGET_PARAMS_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
105        let msg = GetTargetsParams { filter: None };
106        (msg.identifier(), serde_json::to_value(msg).expect("valid paramtarget"))
107    };
108    /// Set the close targets.
109    static ref CLOSE_PARAMS_ID: (std::borrow::Cow<'static, str>, serde_json::Value) = {
110        let close_msg = CloseParams::default();
111        (close_msg.identifier(), serde_json::to_value(close_msg).expect("valid close params"))
112    };
113}
114
115fn maybe_store_attach_session_id(target: &mut Target, method: &MethodId, resp: &Response) {
116    if method.as_ref() != AttachToTargetParams::IDENTIFIER {
117        return;
118    }
119
120    if let Ok(resp) = to_command_response::<AttachToTargetParams>(resp.clone(), method.clone()) {
121        target.set_session_id(resp.result.session_id);
122    }
123}
124
125impl Handler {
126    /// Create a new `Handler` that drives the connection and listens for
127    /// messages on the receiver `rx`.
128    pub(crate) fn new(
129        mut conn: Connection<CdpEventMessage>,
130        rx: Receiver<HandlerMessage>,
131        config: HandlerConfig,
132    ) -> Self {
133        let discover = DISCOVER_ID.clone();
134        let _ = conn.submit_command(discover.0, None, discover.1);
135
136        let browser_contexts = config
137            .context_ids
138            .iter()
139            .map(|id| BrowserContext::from(id.clone()))
140            .collect();
141
142        Self {
143            pending_commands: Default::default(),
144            from_browser: rx,
145            default_browser_context: Default::default(),
146            browser_contexts,
147            target_ids: Default::default(),
148            targets: Default::default(),
149            navigations: Default::default(),
150            sessions: Default::default(),
151            conn,
152            evict_command_timeout: PeriodicJob::new(config.request_timeout),
153            next_navigation_id: 0,
154            config,
155            event_listeners: Default::default(),
156            closing: false,
157            remaining_bytes: None,
158            budget_exhausted: false,
159            attached_targets: Default::default(),
160        }
161    }
162
163    /// Return the target with the matching `target_id`
164    pub fn get_target(&self, target_id: &TargetId) -> Option<&Target> {
165        self.targets.get(target_id)
166    }
167
168    /// Iterator over all currently attached targets
169    pub fn targets(&self) -> impl Iterator<Item = &Target> + '_ {
170        self.targets.values()
171    }
172
173    /// The default Browser context
174    pub fn default_browser_context(&self) -> &BrowserContext {
175        &self.default_browser_context
176    }
177
178    /// Iterator over all currently available browser contexts
179    pub fn browser_contexts(&self) -> impl Iterator<Item = &BrowserContext> + '_ {
180        self.browser_contexts.iter()
181    }
182
183    /// received a response to a navigation request like `Page.navigate`
184    fn on_navigation_response(&mut self, id: NavigationId, resp: Response) {
185        if let Some(nav) = self.navigations.remove(&id) {
186            match nav {
187                NavigationRequest::Navigate(mut nav) => {
188                    if nav.navigated {
189                        let _ = nav.tx.send(Ok(resp));
190                    } else {
191                        nav.set_response(resp);
192                        self.navigations
193                            .insert(id, NavigationRequest::Navigate(nav));
194                    }
195                }
196            }
197        }
198    }
199
200    /// A navigation has finished.
201    fn on_navigation_lifecycle_completed(&mut self, res: Result<NavigationOk, NavigationError>) {
202        match res {
203            Ok(ok) => {
204                let id = *ok.navigation_id();
205                if let Some(nav) = self.navigations.remove(&id) {
206                    match nav {
207                        NavigationRequest::Navigate(mut nav) => {
208                            if let Some(resp) = nav.response.take() {
209                                let _ = nav.tx.send(Ok(resp));
210                            } else {
211                                nav.set_navigated();
212                                self.navigations
213                                    .insert(id, NavigationRequest::Navigate(nav));
214                            }
215                        }
216                    }
217                }
218            }
219            Err(err) => {
220                if let Some(nav) = self.navigations.remove(err.navigation_id()) {
221                    match nav {
222                        NavigationRequest::Navigate(nav) => {
223                            let _ = nav.tx.send(Err(err.into()));
224                        }
225                    }
226                }
227            }
228        }
229    }
230
231    /// Received a response to a request.
232    fn on_response(&mut self, resp: Response) {
233        if let Some((req, method, _)) = self.pending_commands.remove(&resp.id) {
234            match req {
235                PendingRequest::CreateTarget(tx) => {
236                    match to_command_response::<CreateTargetParams>(resp, method) {
237                        Ok(resp) => {
238                            if let Some(target) = self.targets.get_mut(&resp.target_id) {
239                                target.set_initiator(tx);
240                            } else {
241                                let _ = tx.send(Err(CdpError::NotFound)).ok();
242                            }
243                        }
244                        Err(err) => {
245                            let _ = tx.send(Err(err)).ok();
246                        }
247                    }
248                }
249                PendingRequest::GetTargets(tx) => {
250                    match to_command_response::<GetTargetsParams>(resp, method) {
251                        Ok(resp) => {
252                            let targets = resp.result.target_infos;
253                            let results = targets.clone();
254
255                            for target_info in targets {
256                                let event: EventTargetCreated = EventTargetCreated { target_info };
257                                self.on_target_created(event);
258                            }
259
260                            let _ = tx.send(Ok(results)).ok();
261                        }
262                        Err(err) => {
263                            let _ = tx.send(Err(err)).ok();
264                        }
265                    }
266                }
267                PendingRequest::Navigate(id) => {
268                    self.on_navigation_response(id, resp);
269                    if self.config.only_html && !self.config.created_first_target {
270                        self.config.created_first_target = true;
271                    }
272                }
273                PendingRequest::ExternalCommand(tx) => {
274                    let _ = tx.send(Ok(resp)).ok();
275                }
276                PendingRequest::InternalCommand(target_id) => {
277                    if let Some(target) = self.targets.get_mut(&target_id) {
278                        maybe_store_attach_session_id(target, &method, &resp);
279                        target.on_response(resp, method.as_ref());
280                    }
281                }
282                PendingRequest::CloseBrowser(tx) => {
283                    self.closing = true;
284                    let _ = tx.send(Ok(CloseReturns {})).ok();
285                }
286            }
287        }
288    }
289
290    /// Submit a command initiated via channel
291    pub(crate) fn submit_external_command(
292        &mut self,
293        msg: CommandMessage,
294        now: Instant,
295    ) -> Result<()> {
296        let call_id = self
297            .conn
298            .submit_command(msg.method.clone(), msg.session_id, msg.params)?;
299        self.pending_commands.insert(
300            call_id,
301            (PendingRequest::ExternalCommand(msg.sender), msg.method, now),
302        );
303        Ok(())
304    }
305
306    pub(crate) fn submit_internal_command(
307        &mut self,
308        target_id: TargetId,
309        req: CdpRequest,
310        now: Instant,
311    ) -> Result<()> {
312        let call_id = self.conn.submit_command(
313            req.method.clone(),
314            req.session_id.map(Into::into),
315            req.params,
316        )?;
317        self.pending_commands.insert(
318            call_id,
319            (PendingRequest::InternalCommand(target_id), req.method, now),
320        );
321        Ok(())
322    }
323
324    fn submit_fetch_targets(&mut self, tx: OneshotSender<Result<Vec<TargetInfo>>>, now: Instant) {
325        let msg = TARGET_PARAMS_ID.clone();
326
327        if let Ok(call_id) = self.conn.submit_command(msg.0.clone(), None, msg.1) {
328            self.pending_commands
329                .insert(call_id, (PendingRequest::GetTargets(tx), msg.0, now));
330        }
331    }
332
333    /// Send the Request over to the server and store its identifier to handle
334    /// the response once received.
335    fn submit_navigation(&mut self, id: NavigationId, req: CdpRequest, now: Instant) {
336        if let Ok(call_id) = self.conn.submit_command(
337            req.method.clone(),
338            req.session_id.map(Into::into),
339            req.params,
340        ) {
341            self.pending_commands
342                .insert(call_id, (PendingRequest::Navigate(id), req.method, now));
343        }
344    }
345
346    fn submit_close(&mut self, tx: OneshotSender<Result<CloseReturns>>, now: Instant) {
347        let close_msg = CLOSE_PARAMS_ID.clone();
348
349        if let Ok(call_id) = self
350            .conn
351            .submit_command(close_msg.0.clone(), None, close_msg.1)
352        {
353            self.pending_commands.insert(
354                call_id,
355                (PendingRequest::CloseBrowser(tx), close_msg.0, now),
356            );
357        }
358    }
359
360    /// Process a message received by the target's page via channel
361    fn on_target_message(&mut self, target: &mut Target, msg: CommandMessage, now: Instant) {
362        if msg.is_navigation() {
363            let (req, tx) = msg.split();
364            let id = self.next_navigation_id();
365
366            target.goto(FrameRequestedNavigation::new(
367                id,
368                req,
369                self.config.request_timeout,
370            ));
371
372            self.navigations.insert(
373                id,
374                NavigationRequest::Navigate(NavigationInProgress::new(tx)),
375            );
376        } else {
377            let _ = self.submit_external_command(msg, now);
378        }
379    }
380
381    /// An identifier for queued `NavigationRequest`s.
382    fn next_navigation_id(&mut self) -> NavigationId {
383        let id = NavigationId(self.next_navigation_id);
384        self.next_navigation_id = self.next_navigation_id.wrapping_add(1);
385        id
386    }
387
388    /// Create a new page and send it to the receiver when ready
389    ///
390    /// First a `CreateTargetParams` is send to the server, this will trigger
391    /// `EventTargetCreated` which results in a new `Target` being created.
392    /// Once the response to the request is received the initialization process
393    /// of the target kicks in. This triggers a queue of initialization requests
394    /// of the `Target`, once those are all processed and the `url` fo the
395    /// `CreateTargetParams` has finished loading (The `Target`'s `Page` is
396    /// ready and idle), the `Target` sends its newly created `Page` as response
397    /// to the initiator (`tx`) of the `CreateTargetParams` request.
398    fn create_page(&mut self, params: CreateTargetParams, tx: OneshotSender<Result<Page>>) {
399        let about_blank = params.url == "about:blank";
400        let http_check =
401            !about_blank && params.url.starts_with("http") || params.url.starts_with("file://");
402
403        if about_blank || http_check {
404            let method = params.identifier();
405
406            match serde_json::to_value(params) {
407                Ok(params) => match self.conn.submit_command(method.clone(), None, params) {
408                    Ok(call_id) => {
409                        self.pending_commands.insert(
410                            call_id,
411                            (PendingRequest::CreateTarget(tx), method, Instant::now()),
412                        );
413                    }
414                    Err(err) => {
415                        let _ = tx.send(Err(err.into())).ok();
416                    }
417                },
418                Err(err) => {
419                    let _ = tx.send(Err(err.into())).ok();
420                }
421            }
422        } else {
423            let _ = tx.send(Err(CdpError::NotFound)).ok();
424        }
425    }
426
427    /// Process an incoming event read from the websocket
428    fn on_event(&mut self, event: CdpEventMessage) {
429        if let Some(session_id) = &event.session_id {
430            if let Some(session) = self.sessions.get(session_id.as_str()) {
431                if let Some(target) = self.targets.get_mut(session.target_id()) {
432                    return target.on_event(event);
433                }
434            }
435        }
436        let CdpEventMessage { params, method, .. } = event;
437
438        match params {
439            CdpEvent::TargetTargetCreated(ref ev) => self.on_target_created((**ev).clone()),
440            CdpEvent::TargetAttachedToTarget(ref ev) => self.on_attached_to_target(ev.clone()),
441            CdpEvent::TargetTargetDestroyed(ref ev) => self.on_target_destroyed(ev.clone()),
442            CdpEvent::TargetDetachedFromTarget(ref ev) => self.on_detached_from_target(ev.clone()),
443            _ => {}
444        }
445
446        chromiumoxide_cdp::consume_event!(match params {
447            |ev| self.event_listeners.start_send(ev),
448            |json| { let _ = self.event_listeners.try_send_custom(&method, json);}
449        });
450    }
451
452    /// Fired when a new target was created on the chromium instance
453    ///
454    /// Creates a new `Target` instance and keeps track of it
455    fn on_target_created(&mut self, event: EventTargetCreated) {
456        if !self.browser_contexts.is_empty() {
457            if let Some(ref context_id) = event.target_info.browser_context_id {
458                let bc = BrowserContext {
459                    id: Some(context_id.clone()),
460                };
461                if !self.browser_contexts.contains(&bc) {
462                    return;
463                }
464            }
465        }
466        let browser_ctx = event
467            .target_info
468            .browser_context_id
469            .clone()
470            .map(BrowserContext::from)
471            .unwrap_or_else(|| self.default_browser_context.clone());
472        let target = Target::new(
473            event.target_info,
474            TargetConfig {
475                ignore_https_errors: self.config.ignore_https_errors,
476                request_timeout: self.config.request_timeout,
477                viewport: self.config.viewport.clone(),
478                request_intercept: self.config.request_intercept,
479                cache_enabled: self.config.cache_enabled,
480                service_worker_enabled: self.config.service_worker_enabled,
481                ignore_visuals: self.config.ignore_visuals,
482                ignore_stylesheets: self.config.ignore_stylesheets,
483                ignore_javascript: self.config.ignore_javascript,
484                ignore_analytics: self.config.ignore_analytics,
485                ignore_prefetch: self.config.ignore_prefetch,
486                extra_headers: self.config.extra_headers.clone(),
487                only_html: self.config.only_html && self.config.created_first_target,
488                intercept_manager: self.config.intercept_manager,
489                max_bytes_allowed: self.config.max_bytes_allowed,
490                whitelist_patterns: self.config.whitelist_patterns.clone(),
491                blacklist_patterns: self.config.blacklist_patterns.clone(),
492                #[cfg(feature = "adblock")]
493                adblock_filter_rules: self.config.adblock_filter_rules.clone(),
494            },
495            browser_ctx,
496        );
497
498        self.target_ids.push(target.target_id().clone());
499        self.targets.insert(target.target_id().clone(), target);
500    }
501
502    /// A new session is attached to a target
503    fn on_attached_to_target(&mut self, event: Box<EventAttachedToTarget>) {
504        let session = Session::new(event.session_id.clone(), event.target_info.target_id);
505        if let Some(target) = self.targets.get_mut(session.target_id()) {
506            target.set_session_id(session.session_id().clone())
507        }
508        self.sessions.insert(event.session_id, session);
509    }
510
511    /// The session was detached from target.
512    /// Can be issued multiple times per target if multiple session have been
513    /// attached to it.
514    fn on_detached_from_target(&mut self, event: EventDetachedFromTarget) {
515        // remove the session
516        if let Some(session) = self.sessions.remove(&event.session_id) {
517            if let Some(target) = self.targets.get_mut(session.target_id()) {
518                target.session_id_mut().take();
519            }
520        }
521    }
522
523    /// Fired when the target was destroyed in the browser
524    fn on_target_destroyed(&mut self, event: EventTargetDestroyed) {
525        self.attached_targets.remove(&event.target_id);
526
527        if let Some(target) = self.targets.remove(&event.target_id) {
528            // TODO shutdown?
529            if let Some(session) = target.session_id() {
530                self.sessions.remove(session);
531            }
532        }
533    }
534
535    /// House keeping of commands
536    ///
537    /// Remove all commands where `now` > `timestamp of command starting point +
538    /// request timeout` and notify the senders that their request timed out.
539    fn evict_timed_out_commands(&mut self, now: Instant) {
540        let deadline = match now.checked_sub(self.config.request_timeout) {
541            Some(d) => d,
542            None => return,
543        };
544
545        let timed_out: Vec<_> = self
546            .pending_commands
547            .iter()
548            .filter(|(_, (_, _, timestamp))| *timestamp < deadline)
549            .map(|(k, _)| *k)
550            .collect();
551
552        for call in timed_out {
553            if let Some((req, _, _)) = self.pending_commands.remove(&call) {
554                match req {
555                    PendingRequest::CreateTarget(tx) => {
556                        let _ = tx.send(Err(CdpError::Timeout));
557                    }
558                    PendingRequest::GetTargets(tx) => {
559                        let _ = tx.send(Err(CdpError::Timeout));
560                    }
561                    PendingRequest::Navigate(nav) => {
562                        if let Some(nav) = self.navigations.remove(&nav) {
563                            match nav {
564                                NavigationRequest::Navigate(nav) => {
565                                    let _ = nav.tx.send(Err(CdpError::Timeout));
566                                }
567                            }
568                        }
569                    }
570                    PendingRequest::ExternalCommand(tx) => {
571                        let _ = tx.send(Err(CdpError::Timeout));
572                    }
573                    PendingRequest::InternalCommand(_) => {}
574                    PendingRequest::CloseBrowser(tx) => {
575                        let _ = tx.send(Err(CdpError::Timeout));
576                    }
577                }
578            }
579        }
580    }
581
582    pub fn event_listeners_mut(&mut self) -> &mut EventListeners {
583        &mut self.event_listeners
584    }
585}
586
587impl Stream for Handler {
588    type Item = Result<()>;
589
590    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
591        let pin = self.get_mut();
592
593        let mut dispose = false;
594
595        // Per-section iteration cap to ensure fairness between browser
596        // messages, target polling, and websocket events.  When a cap is
597        // hit we break out, re-wake, and let other sections run.
598        const POLL_BUDGET: usize = 256;
599
600        loop {
601            let now = Instant::now();
602            let mut budget_hit = false;
603
604            // temporary pinning of the browser receiver should be safe as we are pinning
605            // through the already pinned self. with the receivers we can also
606            // safely ignore exhaustion as those are fused.
607            let mut browser_msgs = 0usize;
608            while let Poll::Ready(Some(msg)) = pin.from_browser.poll_recv(cx) {
609                match msg {
610                    HandlerMessage::Command(cmd) => {
611                        pin.submit_external_command(cmd, now)?;
612                    }
613                    HandlerMessage::FetchTargets(tx) => {
614                        pin.submit_fetch_targets(tx, now);
615                    }
616                    HandlerMessage::CloseBrowser(tx) => {
617                        pin.submit_close(tx, now);
618                    }
619                    HandlerMessage::CreatePage(params, tx) => {
620                        if let Some(ref id) = params.browser_context_id {
621                            pin.browser_contexts
622                                .insert(BrowserContext::from(id.clone()));
623                        }
624                        pin.create_page(params, tx);
625                    }
626                    HandlerMessage::GetPages(tx) => {
627                        let pages: Vec<_> = pin
628                            .targets
629                            .values_mut()
630                            .filter(|p: &&mut Target| p.is_page())
631                            .filter_map(|target| target.get_or_create_page())
632                            .map(|page| Page::from(page.clone()))
633                            .collect();
634                        let _ = tx.send(pages);
635                    }
636                    HandlerMessage::InsertContext(ctx) => {
637                        if pin.default_browser_context.id().is_none() {
638                            pin.default_browser_context = ctx.clone();
639                        }
640                        pin.browser_contexts.insert(ctx);
641                    }
642                    HandlerMessage::DisposeContext(ctx) => {
643                        pin.browser_contexts.remove(&ctx);
644                        pin.attached_targets.retain(|tid| {
645                            pin.targets
646                                .get(tid)
647                                .and_then(|t| t.browser_context_id()) // however you expose it
648                                .map(|id| Some(id) != ctx.id())
649                                .unwrap_or(true)
650                        });
651                        pin.closing = true;
652                        dispose = true;
653                    }
654                    HandlerMessage::GetPage(target_id, tx) => {
655                        let page = pin
656                            .targets
657                            .get_mut(&target_id)
658                            .and_then(|target| target.get_or_create_page())
659                            .map(|page| Page::from(page.clone()));
660                        let _ = tx.send(page);
661                    }
662                    HandlerMessage::AddEventListener(req) => {
663                        pin.event_listeners.add_listener(req);
664                    }
665                }
666                browser_msgs += 1;
667                if browser_msgs >= POLL_BUDGET {
668                    budget_hit = true;
669                    break;
670                }
671            }
672
673            for n in (0..pin.target_ids.len()).rev() {
674                let target_id = pin.target_ids.swap_remove(n);
675
676                if let Some((id, mut target)) = pin.targets.remove_entry(&target_id) {
677                    let mut target_events = 0usize;
678                    while let Some(event) = target.poll(cx, now) {
679                        match event {
680                            TargetEvent::Request(req) => {
681                                let _ = pin.submit_internal_command(
682                                    target.target_id().clone(),
683                                    req,
684                                    now,
685                                );
686                            }
687                            TargetEvent::Command(msg) => {
688                                pin.on_target_message(&mut target, msg, now);
689                            }
690                            TargetEvent::NavigationRequest(id, req) => {
691                                pin.submit_navigation(id, req, now);
692                            }
693                            TargetEvent::NavigationResult(res) => {
694                                pin.on_navigation_lifecycle_completed(res)
695                            }
696                            TargetEvent::BytesConsumed(n) => {
697                                if let Some(rem) = pin.remaining_bytes.as_mut() {
698                                    *rem = rem.saturating_sub(n);
699                                    if *rem == 0 {
700                                        pin.budget_exhausted = true;
701                                    }
702                                }
703                            }
704                        }
705                        target_events += 1;
706                        if target_events >= POLL_BUDGET {
707                            budget_hit = true;
708                            break;
709                        }
710                    }
711
712                    // poll the target's event listeners
713                    target.event_listeners_mut().poll(cx);
714                    // poll the handler's event listeners
715                    pin.event_listeners_mut().poll(cx);
716
717                    pin.targets.insert(id, target);
718                    pin.target_ids.push(target_id);
719                }
720            }
721
722            let mut done = true;
723            let mut ws_msgs = 0usize;
724
725            while let Poll::Ready(Some(ev)) = Pin::new(&mut pin.conn).poll_next(cx) {
726                match ev {
727                    Ok(boxed_msg) => match *boxed_msg {
728                        Message::Response(resp) => {
729                            pin.on_response(resp);
730                            if pin.closing {
731                                return Poll::Ready(None);
732                            }
733                        }
734                        Message::Event(ev) => {
735                            pin.on_event(ev);
736                        }
737                    },
738                    Err(err) => {
739                        tracing::error!("WS Connection error: {:?}", err);
740                        if let CdpError::Ws(ref ws_error) = err {
741                            match ws_error {
742                                Error::AlreadyClosed => {
743                                    pin.closing = true;
744                                    dispose = true;
745                                    break;
746                                }
747                                Error::Protocol(detail)
748                                    if detail == &ProtocolError::ResetWithoutClosingHandshake =>
749                                {
750                                    pin.closing = true;
751                                    dispose = true;
752                                    break;
753                                }
754                                _ => {}
755                            }
756                        }
757                        return Poll::Ready(Some(Err(err)));
758                    }
759                }
760                done = false;
761                ws_msgs += 1;
762                if ws_msgs >= POLL_BUDGET {
763                    budget_hit = true;
764                    break;
765                }
766            }
767
768            if pin.evict_command_timeout.poll_ready(cx) {
769                // evict all commands that timed out
770                pin.evict_timed_out_commands(now);
771                // evict stale network race-condition buffers
772                for t in pin.targets.values_mut() {
773                    t.network_manager.evict_stale_entries();
774                }
775            }
776
777            if pin.budget_exhausted {
778                for t in pin.targets.values_mut() {
779                    t.network_manager.set_block_all(true);
780                }
781            }
782
783            if dispose {
784                return Poll::Ready(None);
785            }
786
787            if budget_hit {
788                // We hit a per-section cap — yield to the executor so
789                // other tasks get CPU time, then resume.
790                cx.waker().wake_by_ref();
791                return Poll::Pending;
792            }
793
794            if done {
795                // no events/responses were read from the websocket
796                return Poll::Pending;
797            }
798        }
799    }
800}
801
802/// How to configure the handler
803#[derive(Debug, Clone)]
804pub struct HandlerConfig {
805    /// Whether the `NetworkManager`s should ignore https errors
806    pub ignore_https_errors: bool,
807    /// Window and device settings
808    pub viewport: Option<Viewport>,
809    /// Context ids to set from the get go
810    pub context_ids: Vec<BrowserContextId>,
811    /// default request timeout to use
812    pub request_timeout: Duration,
813    /// Whether to enable request interception
814    pub request_intercept: bool,
815    /// Whether to enable cache
816    pub cache_enabled: bool,
817    /// Whether to enable Service Workers
818    pub service_worker_enabled: bool,
819    /// Whether to ignore visuals.
820    pub ignore_visuals: bool,
821    /// Whether to ignore stylesheets.
822    pub ignore_stylesheets: bool,
823    /// Whether to ignore Javascript only allowing critical framework or lib based rendering.
824    pub ignore_javascript: bool,
825    /// Whether to ignore analytics.
826    pub ignore_analytics: bool,
827    /// Ignore prefetch request. Defaults to true.
828    pub ignore_prefetch: bool,
829    /// Whether to ignore ads.
830    pub ignore_ads: bool,
831    /// Extra headers.
832    pub extra_headers: Option<std::collections::HashMap<String, String>>,
833    /// Only Html.
834    pub only_html: bool,
835    /// Created the first target.
836    pub created_first_target: bool,
837    /// The network intercept manager.
838    pub intercept_manager: NetworkInterceptManager,
839    /// The max bytes to receive.
840    pub max_bytes_allowed: Option<u64>,
841    /// Optional per-run/per-site whitelist of URL substrings (scripts/resources).
842    pub whitelist_patterns: Option<Vec<String>>,
843    /// Optional per-run/per-site blacklist of URL substrings (scripts/resources).
844    pub blacklist_patterns: Option<Vec<String>>,
845    /// Extra ABP/uBO filter rules for the adblock engine.
846    #[cfg(feature = "adblock")]
847    pub adblock_filter_rules: Option<Vec<String>>,
848    /// Capacity of the channel between browser handle and handler.
849    /// Defaults to 1000.
850    pub channel_capacity: usize,
851    /// Number of WebSocket connection retry attempts with exponential backoff.
852    /// Defaults to 4.
853    pub connection_retries: u32,
854}
855
856impl Default for HandlerConfig {
857    fn default() -> Self {
858        Self {
859            ignore_https_errors: true,
860            viewport: Default::default(),
861            context_ids: Vec::new(),
862            request_timeout: Duration::from_millis(REQUEST_TIMEOUT),
863            request_intercept: false,
864            cache_enabled: true,
865            service_worker_enabled: true,
866            ignore_visuals: false,
867            ignore_stylesheets: false,
868            ignore_ads: false,
869            ignore_javascript: false,
870            ignore_analytics: true,
871            ignore_prefetch: true,
872            only_html: false,
873            extra_headers: Default::default(),
874            created_first_target: false,
875            intercept_manager: NetworkInterceptManager::Unknown,
876            max_bytes_allowed: None,
877            whitelist_patterns: None,
878            blacklist_patterns: None,
879            #[cfg(feature = "adblock")]
880            adblock_filter_rules: None,
881            channel_capacity: 1000,
882            connection_retries: crate::conn::DEFAULT_CONNECTION_RETRIES,
883        }
884    }
885}
886
887/// Wraps the sender half of the channel who requested a navigation
888#[derive(Debug)]
889pub struct NavigationInProgress<T> {
890    /// Marker to indicate whether a navigation lifecycle has completed
891    navigated: bool,
892    /// The response of the issued navigation request
893    response: Option<Response>,
894    /// Sender who initiated the navigation request
895    tx: OneshotSender<T>,
896}
897
898impl<T> NavigationInProgress<T> {
899    fn new(tx: OneshotSender<T>) -> Self {
900        Self {
901            navigated: false,
902            response: None,
903            tx,
904        }
905    }
906
907    /// The response to the cdp request has arrived
908    fn set_response(&mut self, resp: Response) {
909        self.response = Some(resp);
910    }
911
912    /// The navigation process has finished, the page finished loading.
913    fn set_navigated(&mut self) {
914        self.navigated = true;
915    }
916}
917
918/// Request type for navigation
919#[derive(Debug)]
920enum NavigationRequest {
921    /// Represents a simple `NavigateParams` ("Page.navigate")
922    Navigate(NavigationInProgress<Result<Response>>),
923    // TODO are there more?
924}
925
926/// Different kind of submitted request submitted from the  `Handler` to the
927/// `Connection` and being waited on for the response.
928#[derive(Debug)]
929enum PendingRequest {
930    /// A Request to create a new `Target` that results in the creation of a
931    /// `Page` that represents a browser page.
932    CreateTarget(OneshotSender<Result<Page>>),
933    /// A Request to fetch old `Target`s created before connection
934    GetTargets(OneshotSender<Result<Vec<TargetInfo>>>),
935    /// A Request to navigate a specific `Target`.
936    ///
937    /// Navigation requests are not automatically completed once the response to
938    /// the raw cdp navigation request (like `NavigateParams`) arrives, but only
939    /// after the `Target` notifies the `Handler` that the `Page` has finished
940    /// loading, which comes after the response.
941    Navigate(NavigationId),
942    /// A common request received via a channel (`Page`).
943    ExternalCommand(OneshotSender<Result<Response>>),
944    /// Requests that are initiated directly from a `Target` (all the
945    /// initialization commands).
946    InternalCommand(TargetId),
947    // A Request to close the browser.
948    CloseBrowser(OneshotSender<Result<CloseReturns>>),
949}
950
951/// Events used internally to communicate with the handler, which are executed
952/// in the background
953// TODO rename to BrowserMessage
954#[derive(Debug)]
955pub(crate) enum HandlerMessage {
956    CreatePage(CreateTargetParams, OneshotSender<Result<Page>>),
957    FetchTargets(OneshotSender<Result<Vec<TargetInfo>>>),
958    InsertContext(BrowserContext),
959    DisposeContext(BrowserContext),
960    GetPages(OneshotSender<Vec<Page>>),
961    Command(CommandMessage),
962    GetPage(TargetId, OneshotSender<Option<Page>>),
963    AddEventListener(EventListenerRequest),
964    CloseBrowser(OneshotSender<Result<CloseReturns>>),
965}
966
967#[cfg(test)]
968mod tests {
969    use super::*;
970    use chromiumoxide_cdp::cdp::browser_protocol::target::{AttachToTargetReturns, TargetInfo};
971
972    #[test]
973    fn attach_to_target_response_sets_session_id_before_event_arrives() {
974        let info = TargetInfo::builder()
975            .target_id("target-1".to_string())
976            .r#type("page")
977            .title("")
978            .url("about:blank")
979            .attached(false)
980            .can_access_opener(false)
981            .build()
982            .expect("target info");
983        let mut target = Target::new(info, TargetConfig::default(), BrowserContext::default());
984        let method: MethodId = AttachToTargetParams::IDENTIFIER.into();
985        let result = serde_json::to_value(AttachToTargetReturns::new("session-1".to_string()))
986            .expect("attach result");
987        let resp = Response {
988            id: CallId::new(1),
989            result: Some(result),
990            error: None,
991        };
992
993        maybe_store_attach_session_id(&mut target, &method, &resp);
994
995        assert_eq!(
996            target.session_id().map(AsRef::as_ref),
997            Some("session-1"),
998            "attach response should seed the flat session id even before Target.attachedToTarget"
999        );
1000    }
1001}