chromiumoxide/handler/
network.rs

1use super::blockers::{
2    ignore_script_embedded, ignore_script_xhr, ignore_script_xhr_media,
3    intercept_manager::NetworkInterceptManager,
4    scripts::{
5        URL_IGNORE_SCRIPT_BASE_PATHS, URL_IGNORE_SCRIPT_STYLES_PATHS, URL_IGNORE_TRIE,
6        URL_IGNORE_TRIE_PATHS,
7    },
8    xhr::IGNORE_XHR_ASSETS,
9};
10use crate::auth::Credentials;
11use crate::cmd::CommandChain;
12use crate::handler::http::HttpRequest;
13use aho_corasick::AhoCorasick;
14use case_insensitive_string::CaseInsensitiveString;
15use chromiumoxide_cdp::cdp::browser_protocol::network::{
16    EmulateNetworkConditionsParams, EventLoadingFailed, EventLoadingFinished,
17    EventRequestServedFromCache, EventRequestWillBeSent, EventResponseReceived, Headers,
18    InterceptionId, RequestId, ResourceType, Response, SetCacheDisabledParams,
19    SetExtraHttpHeadersParams,
20};
21use chromiumoxide_cdp::cdp::browser_protocol::{
22    fetch::{
23        self, AuthChallengeResponse, AuthChallengeResponseResponse, ContinueRequestParams,
24        ContinueWithAuthParams, DisableParams, EventAuthRequired, EventRequestPaused,
25        RequestPattern,
26    },
27    network::SetBypassServiceWorkerParams,
28};
29use chromiumoxide_cdp::cdp::browser_protocol::{
30    network::EnableParams, security::SetIgnoreCertificateErrorsParams,
31};
32use chromiumoxide_types::{Command, Method, MethodId};
33use hashbrown::{HashMap, HashSet};
34use lazy_static::lazy_static;
35use reqwest::header::PROXY_AUTHORIZATION;
36use std::collections::VecDeque;
37use std::time::Duration;
38
39lazy_static! {
40    /// General patterns for popular libraries and resources
41    static ref JS_FRAMEWORK_ALLOW: Vec<&'static str> = vec![
42        "jquery",           // Covers jquery.min.js, jquery.js, etc.
43        "angular",
44        "react",            // Covers all React-related patterns
45        "vue",              // Covers all Vue-related patterns
46        "bootstrap",
47        "d3",
48        "lodash",
49        "ajax",
50        "application",
51        "app",              // Covers general app scripts like app.js
52        "main",
53        "index",
54        "bundle",
55        "vendor",
56        "runtime",
57        "polyfill",
58        "scripts",
59        "/wp-content/js/",  // Covers Wordpress content
60        // Verified 3rd parties for request
61        "https://m.stripe.network/",
62        "https://challenges.cloudflare.com/",
63        "https://js.stripe.com/",
64        "https://cdn.prod.website-files.com/", // webflow cdn scripts
65        "https://cdnjs.cloudflare.com/",        // cloudflare cdn scripts
66        "https://code.jquery.com/jquery-"
67    ];
68
69    /// Determine if a script should be rendered in the browser by name.
70    pub static ref ALLOWED_MATCHER: AhoCorasick = AhoCorasick::new(JS_FRAMEWORK_ALLOW.iter()).unwrap();
71
72    /// path of a js framework
73    pub static ref JS_FRAMEWORK_PATH: phf::Set<&'static str> = {
74        phf::phf_set! {
75            // Add allowed assets from JS_FRAMEWORK_ASSETS except the excluded ones
76            "_next/static/", "_astro/",
77        }
78    };
79
80    /// Ignore the content types.
81    pub static ref IGNORE_CONTENT_TYPES: phf::Set<&'static str> = phf::phf_set! {
82        "application/pdf",
83        "application/zip",
84        "application/x-rar-compressed",
85        "application/x-tar",
86        "image/png",
87        "image/jpeg",
88        "image/gif",
89        "image/bmp",
90        "image/svg+xml",
91        "video/mp4",
92        "video/x-msvideo",
93        "video/x-matroska",
94        "video/webm",
95        "audio/mpeg",
96        "audio/ogg",
97        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
98        "application/vnd.ms-excel",
99        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
100        "application/vnd.ms-powerpoint",
101        "application/vnd.openxmlformats-officedocument.presentationml.presentation",
102        "application/x-7z-compressed",
103        "application/x-rpm",
104        "application/x-shockwave-flash",
105    };
106
107    /// Ignore the resources for visual content types.
108    pub static ref IGNORE_VISUAL_RESOURCE_MAP: phf::Set<&'static str> = phf::phf_set! {
109        "Image",
110        "Media",
111        "Font"
112    };
113
114    /// Ignore the resources for visual content types.
115    pub static ref IGNORE_NETWORKING_RESOURCE_MAP: phf::Set<&'static str> = phf::phf_set! {
116        "Prefetch",
117        "Ping",
118    };
119
120    /// Case insenstive css matching
121    pub static ref CSS_EXTENSION: CaseInsensitiveString = CaseInsensitiveString::from("css");
122
123}
124
125#[derive(Debug)]
126pub struct NetworkManager {
127    queued_events: VecDeque<NetworkEvent>,
128    ignore_httpserrors: bool,
129    requests: HashMap<RequestId, HttpRequest>,
130    // TODO put event in an Arc?
131    requests_will_be_sent: HashMap<RequestId, EventRequestWillBeSent>,
132    extra_headers: std::collections::HashMap<String, String>,
133    request_id_to_interception_id: HashMap<RequestId, InterceptionId>,
134    user_cache_disabled: bool,
135    attempted_authentications: HashSet<RequestId>,
136    credentials: Option<Credentials>,
137    user_request_interception_enabled: bool,
138    protocol_request_interception_enabled: bool,
139    offline: bool,
140    request_timeout: Duration,
141    // made_request: bool,
142    /// Ignore visuals (no pings, prefetching, and etc).
143    pub ignore_visuals: bool,
144    /// Block CSS stylesheets.
145    pub block_stylesheets: bool,
146    /// Block javascript that is not critical to rendering.
147    pub block_javascript: bool,
148    /// Block analytics from rendering
149    pub block_analytics: bool,
150    /// Only html from loading.
151    pub only_html: bool,
152    /// The custom intercept handle logic to run on the website.
153    pub intercept_manager: NetworkInterceptManager,
154}
155
156impl NetworkManager {
157    pub fn new(ignore_httpserrors: bool, request_timeout: Duration) -> Self {
158        Self {
159            queued_events: Default::default(),
160            ignore_httpserrors,
161            requests: Default::default(),
162            requests_will_be_sent: Default::default(),
163            extra_headers: Default::default(),
164            request_id_to_interception_id: Default::default(),
165            user_cache_disabled: false,
166            attempted_authentications: Default::default(),
167            credentials: None,
168            user_request_interception_enabled: false,
169            protocol_request_interception_enabled: false,
170            offline: false,
171            request_timeout,
172            ignore_visuals: false,
173            block_javascript: false,
174            block_stylesheets: false,
175            block_analytics: true,
176            only_html: false,
177            intercept_manager: NetworkInterceptManager::UNKNOWN,
178        }
179    }
180
181    pub fn init_commands(&self) -> CommandChain {
182        let enable = EnableParams::default();
183        let mut v = vec![];
184
185        if let Ok(c) = serde_json::to_value(&enable) {
186            v.push((enable.identifier(), c));
187        }
188
189        let cmds = if self.ignore_httpserrors {
190            let ignore = SetIgnoreCertificateErrorsParams::new(true);
191
192            if let Ok(ignored) = serde_json::to_value(&ignore) {
193                v.push((ignore.identifier(), ignored));
194            }
195
196            v
197        } else {
198            v
199        };
200
201        CommandChain::new(cmds, self.request_timeout)
202    }
203
204    fn push_cdp_request<T: Command>(&mut self, cmd: T) {
205        let method = cmd.identifier();
206        if let Ok(params) = serde_json::to_value(cmd) {
207            self.queued_events
208                .push_back(NetworkEvent::SendCdpRequest((method, params)));
209        }
210    }
211
212    /// The next event to handle
213    pub fn poll(&mut self) -> Option<NetworkEvent> {
214        self.queued_events.pop_front()
215    }
216
217    pub fn extra_headers(&self) -> &std::collections::HashMap<String, String> {
218        &self.extra_headers
219    }
220
221    pub fn set_extra_headers(&mut self, headers: std::collections::HashMap<String, String>) {
222        self.extra_headers = headers;
223        self.extra_headers.remove(PROXY_AUTHORIZATION.as_str());
224        if let Ok(headers) = serde_json::to_value(&self.extra_headers) {
225            self.push_cdp_request(SetExtraHttpHeadersParams::new(Headers::new(headers)));
226        }
227    }
228
229    pub fn set_service_worker_enabled(&mut self, bypass: bool) {
230        self.push_cdp_request(SetBypassServiceWorkerParams::new(bypass));
231    }
232
233    pub fn set_request_interception(&mut self, enabled: bool) {
234        self.user_request_interception_enabled = enabled;
235        self.update_protocol_request_interception();
236    }
237
238    pub fn set_cache_enabled(&mut self, enabled: bool) {
239        self.user_cache_disabled = !enabled;
240        self.update_protocol_cache_disabled();
241    }
242
243    pub fn update_protocol_cache_disabled(&mut self) {
244        self.push_cdp_request(SetCacheDisabledParams::new(
245            self.user_cache_disabled || self.protocol_request_interception_enabled,
246        ));
247    }
248
249    pub fn authenticate(&mut self, credentials: Credentials) {
250        self.credentials = Some(credentials);
251        self.update_protocol_request_interception()
252    }
253
254    fn update_protocol_request_interception(&mut self) {
255        let enabled = self.user_request_interception_enabled || self.credentials.is_some();
256
257        if enabled == self.protocol_request_interception_enabled {
258            return;
259        }
260        self.update_protocol_cache_disabled();
261
262        if enabled {
263            self.push_cdp_request(
264                fetch::EnableParams::builder()
265                    .handle_auth_requests(true)
266                    .pattern(RequestPattern::builder().url_pattern("*").build())
267                    .build(),
268            )
269        } else {
270            self.push_cdp_request(DisableParams::default())
271        }
272    }
273
274    /// Url matches analytics that we want to ignore or trackers.
275    pub(crate) fn ignore_script(
276        &self,
277        url: &str,
278        block_analytics: bool,
279        intercept_manager: NetworkInterceptManager,
280    ) -> bool {
281        let mut ignore_script = block_analytics && URL_IGNORE_TRIE.contains_prefix(url);
282
283        if !ignore_script {
284            if let Some(index) = url.find("//") {
285                let pos = index + 2;
286
287                // Ensure there is something after `//`
288                if pos < url.len() {
289                    // Find the first slash after the `//`
290                    if let Some(slash_index) = url[pos..].find('/') {
291                        let base_path_index = pos + slash_index + 1;
292
293                        if url.len() > base_path_index {
294                            let new_url: &str = &url[base_path_index..];
295
296                            ignore_script = URL_IGNORE_TRIE_PATHS.contains_prefix(new_url);
297
298                            // ignore assets we do not need for frameworks
299                            if !ignore_script
300                                && intercept_manager == NetworkInterceptManager::UNKNOWN
301                            {
302                                let hydration_file =
303                                    JS_FRAMEWORK_PATH.iter().any(|p| new_url.starts_with(p));
304
305                                // ignore astro paths
306                                if hydration_file && new_url.ends_with(".js") {
307                                    ignore_script = true;
308                                }
309                            }
310
311                            if !ignore_script
312                                && URL_IGNORE_SCRIPT_BASE_PATHS.contains_prefix(new_url)
313                            {
314                                ignore_script = true;
315                            }
316
317                            if !ignore_script
318                                && self.ignore_visuals
319                                && URL_IGNORE_SCRIPT_STYLES_PATHS.contains_prefix(new_url)
320                            {
321                                ignore_script = true;
322                            }
323                        }
324                    }
325                }
326            }
327        }
328
329        // fallback for file ending in analytics.js
330        if !ignore_script {
331            ignore_script = url.ends_with("analytics.js")
332                || url.ends_with("ads.js")
333                || url.ends_with("tracking.js")
334                || url.ends_with("track.js");
335        }
336
337        ignore_script
338    }
339
340    /// Determine if the request should be skipped.
341    fn skip_xhr(&self, skip_networking: bool, event: &EventRequestPaused) -> bool {
342        // XHR check
343        if !skip_networking
344            && (event.resource_type == ResourceType::Xhr
345                || event.resource_type == ResourceType::WebSocket
346                || event.resource_type == ResourceType::Fetch)
347        {
348            let request_url = event.request.url.as_str();
349
350            // check if part of ignore scripts.
351            let skip_analytics = self.block_analytics && ignore_script_xhr(request_url);
352
353            if skip_analytics {
354                true
355            } else if self.block_stylesheets || self.ignore_visuals {
356                let block_css = self.block_stylesheets;
357                let block_media = self.ignore_visuals;
358
359                let mut block_request = false;
360
361                if let Some(position) = request_url.rfind('.') {
362                    let hlen = request_url.len();
363                    let has_asset = hlen - position;
364
365                    if has_asset >= 3 {
366                        let next_position = position + 1;
367
368                        if block_media
369                            && IGNORE_XHR_ASSETS.contains::<CaseInsensitiveString>(
370                                &request_url[next_position..].into(),
371                            )
372                        {
373                            block_request = true;
374                        } else if block_css {
375                            block_request =
376                                CaseInsensitiveString::from(request_url[next_position..].as_bytes())
377                                    .contains(&**CSS_EXTENSION)
378                        }
379                    }
380                }
381
382                if !block_request {
383                    block_request = ignore_script_xhr_media(request_url);
384                }
385
386                block_request
387            } else {
388                skip_networking
389            }
390        } else {
391            skip_networking
392        }
393    }
394
395    #[cfg(not(feature = "adblock"))]
396    pub fn on_fetch_request_paused(&mut self, event: &EventRequestPaused) {
397        if !self.user_request_interception_enabled && self.protocol_request_interception_enabled {
398            self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
399        } else {
400            if let Some(network_id) = event.network_id.as_ref() {
401                if let Some(request_will_be_sent) =
402                    self.requests_will_be_sent.remove(network_id.as_ref())
403                {
404                    self.on_request(&request_will_be_sent, Some(event.request_id.clone().into()));
405                } else {
406                    let current_url = event.request.url.as_str();
407                    let javascript_resource = event.resource_type == ResourceType::Script;
408                    let skip_networking = event.resource_type == ResourceType::Other
409                        || event.resource_type == ResourceType::Manifest
410                        || event.resource_type == ResourceType::CspViolationReport
411                        || event.resource_type == ResourceType::Ping
412                        || event.resource_type == ResourceType::Prefetch;
413                    let network_resource = event.resource_type == ResourceType::Xhr
414                        || event.resource_type == ResourceType::Fetch
415                        || event.resource_type == ResourceType::WebSocket;
416
417                    // main initial check
418                    let skip_networking = if !skip_networking {
419                        IGNORE_NETWORKING_RESOURCE_MAP.contains(event.resource_type.as_ref())
420                            || self.ignore_visuals
421                                && (IGNORE_VISUAL_RESOURCE_MAP
422                                    .contains(event.resource_type.as_ref()))
423                            || self.block_stylesheets
424                                && ResourceType::Stylesheet == event.resource_type
425                            || self.block_javascript
426                                && javascript_resource
427                                && self.intercept_manager == NetworkInterceptManager::UNKNOWN
428                                && !ALLOWED_MATCHER.is_match(current_url)
429                    } else {
430                        skip_networking
431                    };
432
433                    let skip_networking = if !skip_networking
434                        && (self.only_html || self.ignore_visuals)
435                        && (javascript_resource || event.resource_type == ResourceType::Document)
436                    {
437                        ignore_script_embedded(current_url)
438                    } else {
439                        skip_networking
440                    };
441
442                    // analytics check
443                    let skip_networking = if !skip_networking && javascript_resource {
444                        self.ignore_script(
445                            current_url,
446                            self.block_analytics,
447                            self.intercept_manager,
448                        )
449                    } else {
450                        skip_networking
451                    };
452
453                    // XHR check
454                    let skip_networking = self.skip_xhr(skip_networking, &event);
455
456                    // custom interception layer.
457                    let skip_networking = if !skip_networking
458                        && (javascript_resource
459                            || network_resource
460                            || event.resource_type == ResourceType::Document)
461                    {
462                        self.intercept_manager.intercept_detection(
463                            &event.request.url,
464                            self.ignore_visuals,
465                            network_resource,
466                        )
467                    } else {
468                        skip_networking
469                    };
470
471                    if skip_networking {
472                        tracing::debug!(
473                            "Blocked: {:?} - {}",
474                            event.resource_type,
475                            event.request.url
476                        );
477                        let fullfill_params =
478                            crate::handler::network::fetch::FulfillRequestParams::new(
479                                event.request_id.clone(),
480                                200,
481                            );
482                        self.push_cdp_request(fullfill_params);
483                    } else {
484                        tracing::debug!(
485                            "Allowed: {:?} - {}",
486                            event.resource_type,
487                            event.request.url
488                        );
489
490                        let stream = ContinueRequestParams::new(event.request_id.clone());
491
492                        self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
493                    }
494                }
495            } else {
496                self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
497            }
498        }
499    }
500
501    #[cfg(feature = "adblock")]
502    pub fn on_fetch_request_paused(&mut self, event: &EventRequestPaused) {
503        if !self.user_request_interception_enabled && self.protocol_request_interception_enabled {
504            self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
505        } else {
506            if let Some(network_id) = event.network_id.as_ref() {
507                if let Some(request_will_be_sent) =
508                    self.requests_will_be_sent.remove(network_id.as_ref())
509                {
510                    self.on_request(&request_will_be_sent, Some(event.request_id.clone().into()));
511                } else {
512                    let current_url = event.request.url.as_str();
513                    let javascript_resource = event.resource_type == ResourceType::Script;
514                    let skip_networking = event.resource_type == ResourceType::Other
515                        || event.resource_type == ResourceType::Manifest
516                        || event.resource_type == ResourceType::CspViolationReport
517                        || event.resource_type == ResourceType::Ping
518                        || event.resource_type == ResourceType::Prefetch;
519                    let network_resource = event.resource_type == ResourceType::Xhr
520                        || event.resource_type == ResourceType::Fetch
521                        || event.resource_type == ResourceType::WebSocket;
522
523                    // main initial check
524                    let skip_networking = if !skip_networking {
525                        IGNORE_NETWORKING_RESOURCE_MAP.contains(event.resource_type.as_ref())
526                            || self.ignore_visuals
527                                && (IGNORE_VISUAL_RESOURCE_MAP
528                                    .contains(event.resource_type.as_ref()))
529                            || self.block_stylesheets
530                                && ResourceType::Stylesheet == event.resource_type
531                            || self.block_javascript
532                                && javascript_resource
533                                && self.intercept_manager == NetworkInterceptManager::UNKNOWN
534                                && !ALLOWED_MATCHER.is_match(current_url)
535                    } else {
536                        skip_networking
537                    };
538
539                    let skip_networking = if !skip_networking {
540                        self.detect_ad(event)
541                    } else {
542                        skip_networking
543                    };
544
545                    let skip_networking = if !skip_networking
546                        && (self.only_html || self.ignore_visuals)
547                        && (javascript_resource || event.resource_type == ResourceType::Document)
548                    {
549                        ignore_script_embedded(current_url)
550                    } else {
551                        skip_networking
552                    };
553
554                    // analytics check
555                    let skip_networking = if !skip_networking && javascript_resource {
556                        self.ignore_script(
557                            current_url,
558                            self.block_analytics,
559                            self.intercept_manager,
560                        )
561                    } else {
562                        skip_networking
563                    };
564
565                    // XHR check
566                    let skip_networking = self.skip_xhr(skip_networking, &event);
567
568                    // custom interception layer.
569                    let skip_networking = if !skip_networking
570                        && (javascript_resource
571                            || network_resource
572                            || event.resource_type == ResourceType::Document)
573                    {
574                        self.intercept_manager.intercept_detection(
575                            &event.request.url,
576                            self.ignore_visuals,
577                            network_resource,
578                        )
579                    } else {
580                        skip_networking
581                    };
582
583                    if skip_networking {
584                        let fullfill_params =
585                            crate::handler::network::fetch::FulfillRequestParams::new(
586                                event.request_id.clone(),
587                                200,
588                            );
589                        self.push_cdp_request(fullfill_params);
590                    } else {
591                        self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
592                    }
593                }
594            }
595        }
596
597        // if self.only_html {
598        //     self.made_request = true;
599        // }
600    }
601
602    /// Perform a page intercept for chrome
603    #[cfg(feature = "adblock")]
604    pub fn detect_ad(&self, event: &EventRequestPaused) -> bool {
605        use adblock::{
606            lists::{FilterSet, ParseOptions, RuleTypes},
607            Engine,
608        };
609
610        lazy_static::lazy_static! {
611            static ref AD_ENGINE: Engine = {
612                let mut filter_set = FilterSet::new(false);
613                let mut rules = ParseOptions::default();
614                rules.rule_types = RuleTypes::All;
615
616                filter_set.add_filters(
617                    &*crate::handler::blockers::adblock_patterns::ADBLOCK_PATTERNS,
618                    rules,
619                );
620
621                Engine::from_filter_set(filter_set, true)
622            };
623        };
624
625        let blockable = ResourceType::Image == event.resource_type
626            || event.resource_type == ResourceType::Media
627            || event.resource_type == ResourceType::Stylesheet
628            || event.resource_type == ResourceType::Document
629            || event.resource_type == ResourceType::Fetch
630            || event.resource_type == ResourceType::Xhr;
631
632        let u = &event.request.url;
633
634        let block_request = blockable
635            // set it to example.com for 3rd party handling is_same_site
636        && {
637            let request = adblock::request::Request::preparsed(
638                 &u,
639                 "example.com",
640                 "example.com",
641                 &event.resource_type.as_ref().to_lowercase(),
642                 !event.request.is_same_site.unwrap_or_default());
643
644            AD_ENGINE.check_network_request(&request).matched
645        };
646
647        block_request
648    }
649
650    pub fn on_fetch_auth_required(&mut self, event: &EventAuthRequired) {
651        let response = if self
652            .attempted_authentications
653            .contains(event.request_id.as_ref())
654        {
655            AuthChallengeResponseResponse::CancelAuth
656        } else if self.credentials.is_some() {
657            self.attempted_authentications
658                .insert(event.request_id.clone().into());
659            AuthChallengeResponseResponse::ProvideCredentials
660        } else {
661            AuthChallengeResponseResponse::Default
662        };
663
664        let mut auth = AuthChallengeResponse::new(response);
665        if let Some(creds) = self.credentials.clone() {
666            auth.username = Some(creds.username);
667            auth.password = Some(creds.password);
668        }
669        self.push_cdp_request(ContinueWithAuthParams::new(event.request_id.clone(), auth));
670    }
671
672    pub fn set_offline_mode(&mut self, value: bool) {
673        if self.offline == value {
674            return;
675        }
676        self.offline = value;
677        if let Ok(network) = EmulateNetworkConditionsParams::builder()
678            .offline(self.offline)
679            .latency(0)
680            .download_throughput(-1.)
681            .upload_throughput(-1.)
682            .build()
683        {
684            self.push_cdp_request(network);
685        }
686    }
687
688    /// Request interception doesn't happen for data URLs with Network Service.
689    pub fn on_request_will_be_sent(&mut self, event: &EventRequestWillBeSent) {
690        if self.protocol_request_interception_enabled && !event.request.url.starts_with("data:") {
691            if let Some(interception_id) = self
692                .request_id_to_interception_id
693                .remove(event.request_id.as_ref())
694            {
695                self.on_request(event, Some(interception_id));
696            } else {
697                // TODO remove the clone for event
698                self.requests_will_be_sent
699                    .insert(event.request_id.clone(), event.clone());
700            }
701        } else {
702            self.on_request(event, None);
703        }
704    }
705
706    pub fn on_request_served_from_cache(&mut self, event: &EventRequestServedFromCache) {
707        if let Some(request) = self.requests.get_mut(event.request_id.as_ref()) {
708            request.from_memory_cache = true;
709        }
710    }
711
712    pub fn on_response_received(&mut self, event: &EventResponseReceived) {
713        if let Some(mut request) = self.requests.remove(event.request_id.as_ref()) {
714            request.set_response(event.response.clone());
715            self.queued_events
716                .push_back(NetworkEvent::RequestFinished(request))
717        }
718    }
719
720    pub fn on_network_loading_finished(&mut self, event: &EventLoadingFinished) {
721        if let Some(request) = self.requests.remove(event.request_id.as_ref()) {
722            if let Some(interception_id) = request.interception_id.as_ref() {
723                self.attempted_authentications
724                    .remove(interception_id.as_ref());
725            }
726            self.queued_events
727                .push_back(NetworkEvent::RequestFinished(request));
728        }
729    }
730
731    pub fn on_network_loading_failed(&mut self, event: &EventLoadingFailed) {
732        if let Some(mut request) = self.requests.remove(event.request_id.as_ref()) {
733            request.failure_text = Some(event.error_text.clone());
734            if let Some(interception_id) = request.interception_id.as_ref() {
735                self.attempted_authentications
736                    .remove(interception_id.as_ref());
737            }
738            self.queued_events
739                .push_back(NetworkEvent::RequestFailed(request));
740        }
741    }
742
743    fn on_request(
744        &mut self,
745        event: &EventRequestWillBeSent,
746        interception_id: Option<InterceptionId>,
747    ) {
748        let mut redirect_chain = Vec::new();
749        if let Some(redirect_resp) = event.redirect_response.as_ref() {
750            if let Some(mut request) = self.requests.remove(event.request_id.as_ref()) {
751                self.handle_request_redirect(&mut request, redirect_resp.clone());
752                redirect_chain = std::mem::take(&mut request.redirect_chain);
753                redirect_chain.push(request);
754            }
755        }
756        let request = HttpRequest::new(
757            event.request_id.clone(),
758            event.frame_id.clone(),
759            interception_id,
760            self.user_request_interception_enabled,
761            redirect_chain,
762        );
763
764        self.requests.insert(event.request_id.clone(), request);
765        self.queued_events
766            .push_back(NetworkEvent::Request(event.request_id.clone()));
767    }
768
769    fn handle_request_redirect(&mut self, request: &mut HttpRequest, response: Response) {
770        request.set_response(response);
771        if let Some(interception_id) = request.interception_id.as_ref() {
772            self.attempted_authentications
773                .remove(interception_id.as_ref());
774        }
775    }
776}
777
778#[derive(Debug)]
779pub enum NetworkEvent {
780    SendCdpRequest((MethodId, serde_json::Value)),
781    Request(RequestId),
782    Response(RequestId),
783    RequestFailed(HttpRequest),
784    RequestFinished(HttpRequest),
785}