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    // unused atm for remote connections, needs to be used for self launches.
138    user_request_interception_enabled: bool,
139    protocol_request_interception_enabled: bool,
140    offline: bool,
141    request_timeout: Duration,
142    // made_request: bool,
143    /// Ignore visuals (no pings, prefetching, and etc).
144    pub ignore_visuals: bool,
145    /// Block CSS stylesheets.
146    pub block_stylesheets: bool,
147    /// Block javascript that is not critical to rendering.
148    pub block_javascript: bool,
149    /// Block analytics from rendering
150    pub block_analytics: bool,
151    /// Only html from loading.
152    pub only_html: bool,
153    /// The custom intercept handle logic to run on the website.
154    pub intercept_manager: NetworkInterceptManager,
155}
156
157impl NetworkManager {
158    pub fn new(ignore_httpserrors: bool, request_timeout: Duration) -> Self {
159        Self {
160            queued_events: Default::default(),
161            ignore_httpserrors,
162            requests: Default::default(),
163            requests_will_be_sent: Default::default(),
164            extra_headers: Default::default(),
165            request_id_to_interception_id: Default::default(),
166            user_cache_disabled: false,
167            attempted_authentications: Default::default(),
168            credentials: None,
169            user_request_interception_enabled: false,
170            protocol_request_interception_enabled: false,
171            offline: false,
172            request_timeout,
173            ignore_visuals: false,
174            block_javascript: false,
175            block_stylesheets: false,
176            block_analytics: true,
177            only_html: false,
178            intercept_manager: NetworkInterceptManager::UNKNOWN,
179        }
180    }
181
182    pub fn init_commands(&self) -> CommandChain {
183        let enable = EnableParams::default();
184        let mut v = vec![];
185
186        if let Ok(c) = serde_json::to_value(&enable) {
187            v.push((enable.identifier(), c));
188        }
189
190        let cmds = if self.ignore_httpserrors {
191            let ignore = SetIgnoreCertificateErrorsParams::new(true);
192
193            if let Ok(ignored) = serde_json::to_value(&ignore) {
194                v.push((ignore.identifier(), ignored));
195            }
196
197            v
198        } else {
199            v
200        };
201
202        CommandChain::new(cmds, self.request_timeout)
203    }
204
205    fn push_cdp_request<T: Command>(&mut self, cmd: T) {
206        let method = cmd.identifier();
207        if let Ok(params) = serde_json::to_value(cmd) {
208            self.queued_events
209                .push_back(NetworkEvent::SendCdpRequest((method, params)));
210        }
211    }
212
213    /// The next event to handle
214    pub fn poll(&mut self) -> Option<NetworkEvent> {
215        self.queued_events.pop_front()
216    }
217
218    pub fn extra_headers(&self) -> &std::collections::HashMap<String, String> {
219        &self.extra_headers
220    }
221
222    pub fn set_extra_headers(&mut self, headers: std::collections::HashMap<String, String>) {
223        self.extra_headers = headers;
224        self.extra_headers.remove(PROXY_AUTHORIZATION.as_str());
225        if let Ok(headers) = serde_json::to_value(&self.extra_headers) {
226            self.push_cdp_request(SetExtraHttpHeadersParams::new(Headers::new(headers)));
227        }
228    }
229
230    pub fn set_service_worker_enabled(&mut self, bypass: bool) {
231        self.push_cdp_request(SetBypassServiceWorkerParams::new(bypass));
232    }
233
234    pub fn set_request_interception(&mut self, enabled: bool) {
235        self.user_request_interception_enabled = enabled;
236        self.update_protocol_request_interception();
237    }
238
239    pub fn set_cache_enabled(&mut self, enabled: bool) {
240        self.user_cache_disabled = !enabled;
241        self.update_protocol_cache_disabled();
242    }
243
244    pub fn update_protocol_cache_disabled(&mut self) {
245        self.push_cdp_request(SetCacheDisabledParams::new(
246            self.user_cache_disabled || self.protocol_request_interception_enabled,
247        ));
248    }
249
250    pub fn authenticate(&mut self, credentials: Credentials) {
251        self.credentials = Some(credentials);
252        self.update_protocol_request_interception()
253    }
254
255    fn update_protocol_request_interception(&mut self) {
256        let enabled = self.user_request_interception_enabled || self.credentials.is_some();
257
258        if enabled == self.protocol_request_interception_enabled {
259            return;
260        }
261
262        self.update_protocol_cache_disabled();
263
264        if enabled {
265            self.push_cdp_request(
266                fetch::EnableParams::builder()
267                    .handle_auth_requests(true)
268                    .pattern(RequestPattern::builder().url_pattern("*").build())
269                    .build(),
270            )
271        } else {
272            self.push_cdp_request(DisableParams::default())
273        }
274    }
275
276    /// Url matches analytics that we want to ignore or trackers.
277    pub(crate) fn ignore_script(
278        &self,
279        url: &str,
280        block_analytics: bool,
281        intercept_manager: NetworkInterceptManager,
282    ) -> bool {
283        let mut ignore_script = block_analytics && URL_IGNORE_TRIE.contains_prefix(url);
284
285        if !ignore_script {
286            if let Some(index) = url.find("//") {
287                let pos = index + 2;
288
289                // Ensure there is something after `//`
290                if pos < url.len() {
291                    // Find the first slash after the `//`
292                    if let Some(slash_index) = url[pos..].find('/') {
293                        let base_path_index = pos + slash_index + 1;
294
295                        if url.len() > base_path_index {
296                            let new_url: &str = &url[base_path_index..];
297
298                            ignore_script = URL_IGNORE_TRIE_PATHS.contains_prefix(new_url);
299
300                            // ignore assets we do not need for frameworks
301                            if !ignore_script
302                                && intercept_manager == NetworkInterceptManager::UNKNOWN
303                            {
304                                let hydration_file =
305                                    JS_FRAMEWORK_PATH.iter().any(|p| new_url.starts_with(p));
306
307                                // ignore astro paths
308                                if hydration_file && new_url.ends_with(".js") {
309                                    ignore_script = true;
310                                }
311                            }
312
313                            if !ignore_script
314                                && URL_IGNORE_SCRIPT_BASE_PATHS.contains_prefix(new_url)
315                            {
316                                ignore_script = true;
317                            }
318
319                            if !ignore_script
320                                && self.ignore_visuals
321                                && URL_IGNORE_SCRIPT_STYLES_PATHS.contains_prefix(new_url)
322                            {
323                                ignore_script = true;
324                            }
325                        }
326                    }
327                }
328            }
329        }
330
331        // fallback for file ending in analytics.js
332        if !ignore_script {
333            ignore_script = url.ends_with("analytics.js")
334                || url.ends_with("ads.js")
335                || url.ends_with("tracking.js")
336                || url.ends_with("track.js");
337        }
338
339        ignore_script
340    }
341
342    /// Determine if the request should be skipped.
343    fn skip_xhr(
344        &self,
345        skip_networking: bool,
346        event: &EventRequestPaused,
347        network_event: bool,
348    ) -> bool {
349        // XHR check
350        if !skip_networking && network_event {
351            let request_url = event.request.url.as_str();
352
353            // check if part of ignore scripts.
354            let skip_analytics = self.block_analytics && ignore_script_xhr(request_url);
355
356            if skip_analytics {
357                true
358            } else if self.block_stylesheets || self.ignore_visuals {
359                let block_css = self.block_stylesheets;
360                let block_media = self.ignore_visuals;
361
362                let mut block_request = false;
363
364                if let Some(position) = request_url.rfind('.') {
365                    let hlen = request_url.len();
366                    let has_asset = hlen - position;
367
368                    if has_asset >= 3 {
369                        let next_position = position + 1;
370
371                        if block_media
372                            && IGNORE_XHR_ASSETS.contains::<CaseInsensitiveString>(
373                                &request_url[next_position..].into(),
374                            )
375                        {
376                            block_request = true;
377                        } else if block_css {
378                            block_request =
379                                CaseInsensitiveString::from(request_url[next_position..].as_bytes())
380                                    .contains(&**CSS_EXTENSION)
381                        }
382                    }
383                }
384
385                if !block_request {
386                    block_request = ignore_script_xhr_media(request_url);
387                }
388
389                block_request
390            } else {
391                skip_networking
392            }
393        } else {
394            skip_networking
395        }
396    }
397
398    #[cfg(not(feature = "adblock"))]
399    pub fn on_fetch_request_paused(&mut self, event: &EventRequestPaused) {
400        use super::blockers::block_websites::block_website;
401
402        if !self.user_request_interception_enabled && self.protocol_request_interception_enabled {
403            self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
404        } else {
405            if let Some(network_id) = event.network_id.as_ref() {
406                if let Some(request_will_be_sent) =
407                    self.requests_will_be_sent.remove(network_id.as_ref())
408                {
409                    self.on_request(&request_will_be_sent, Some(event.request_id.clone().into()));
410                } else {
411                    let current_url = event.request.url.as_str();
412                    let javascript_resource = event.resource_type == ResourceType::Script;
413                    let skip_networking = event.resource_type == ResourceType::Other
414                        || event.resource_type == ResourceType::Manifest
415                        || event.resource_type == ResourceType::CspViolationReport
416                        || event.resource_type == ResourceType::Ping
417                        || event.resource_type == ResourceType::Prefetch;
418                    let network_resource = event.resource_type == ResourceType::Xhr
419                        || event.resource_type == ResourceType::Fetch
420                        || event.resource_type == ResourceType::WebSocket;
421
422                    // main initial check
423                    let skip_networking = if !skip_networking {
424                        IGNORE_NETWORKING_RESOURCE_MAP.contains(event.resource_type.as_ref())
425                            || self.ignore_visuals
426                                && (IGNORE_VISUAL_RESOURCE_MAP
427                                    .contains(event.resource_type.as_ref()))
428                            || self.block_stylesheets
429                                && ResourceType::Stylesheet == event.resource_type
430                            || self.block_javascript
431                                && javascript_resource
432                                && self.intercept_manager == NetworkInterceptManager::UNKNOWN
433                                && !ALLOWED_MATCHER.is_match(current_url)
434                    } else {
435                        skip_networking
436                    };
437
438                    let skip_networking = if !skip_networking
439                        && (self.only_html || self.ignore_visuals)
440                        && (javascript_resource || event.resource_type == ResourceType::Document)
441                    {
442                        ignore_script_embedded(current_url)
443                    } else {
444                        skip_networking
445                    };
446
447                    // analytics check
448                    let skip_networking = if !skip_networking && javascript_resource {
449                        self.ignore_script(
450                            current_url,
451                            self.block_analytics,
452                            self.intercept_manager,
453                        )
454                    } else {
455                        skip_networking
456                    };
457
458                    let network_event = event.resource_type == ResourceType::Xhr
459                        || event.resource_type == ResourceType::WebSocket
460                        || event.resource_type == ResourceType::Fetch;
461
462                    // XHR check
463                    let skip_networking = self.skip_xhr(skip_networking, &event, network_event);
464
465                    // custom interception layer.
466                    let skip_networking = if !skip_networking
467                        && (javascript_resource
468                            || network_resource
469                            || event.resource_type == ResourceType::Document)
470                    {
471                        self.intercept_manager.intercept_detection(
472                            &event.request.url,
473                            self.ignore_visuals,
474                            network_resource,
475                        )
476                    } else {
477                        skip_networking
478                    };
479
480                    let skip_networking =
481                        if !skip_networking && (javascript_resource || network_event) {
482                            block_website(&event.request.url)
483                        } else {
484                            skip_networking
485                        };
486
487                    if skip_networking {
488                        tracing::debug!(
489                            "Blocked: {:?} - {}",
490                            event.resource_type,
491                            event.request.url
492                        );
493                        let fullfill_params =
494                            crate::handler::network::fetch::FulfillRequestParams::new(
495                                event.request_id.clone(),
496                                200,
497                            );
498                        self.push_cdp_request(fullfill_params);
499                    } else {
500                        tracing::debug!(
501                            "Allowed: {:?} - {}",
502                            event.resource_type,
503                            event.request.url
504                        );
505
506                        self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
507                    }
508                }
509            } else {
510                self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
511            }
512        }
513    }
514
515    #[cfg(feature = "adblock")]
516    pub fn on_fetch_request_paused(&mut self, event: &EventRequestPaused) {
517        if !self.user_request_interception_enabled && self.protocol_request_interception_enabled {
518            self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
519        } else {
520            if let Some(network_id) = event.network_id.as_ref() {
521                if let Some(request_will_be_sent) =
522                    self.requests_will_be_sent.remove(network_id.as_ref())
523                {
524                    self.on_request(&request_will_be_sent, Some(event.request_id.clone().into()));
525                } else {
526                    let current_url = event.request.url.as_str();
527                    let javascript_resource = event.resource_type == ResourceType::Script;
528                    let skip_networking = event.resource_type == ResourceType::Other
529                        || event.resource_type == ResourceType::Manifest
530                        || event.resource_type == ResourceType::CspViolationReport
531                        || event.resource_type == ResourceType::Ping
532                        || event.resource_type == ResourceType::Prefetch;
533                    let network_resource = event.resource_type == ResourceType::Xhr
534                        || event.resource_type == ResourceType::Fetch
535                        || event.resource_type == ResourceType::WebSocket;
536
537                    // main initial check
538                    let skip_networking = if !skip_networking {
539                        IGNORE_NETWORKING_RESOURCE_MAP.contains(event.resource_type.as_ref())
540                            || self.ignore_visuals
541                                && (IGNORE_VISUAL_RESOURCE_MAP
542                                    .contains(event.resource_type.as_ref()))
543                            || self.block_stylesheets
544                                && ResourceType::Stylesheet == event.resource_type
545                            || self.block_javascript
546                                && javascript_resource
547                                && self.intercept_manager == NetworkInterceptManager::UNKNOWN
548                                && !ALLOWED_MATCHER.is_match(current_url)
549                    } else {
550                        skip_networking
551                    };
552
553                    let skip_networking = if !skip_networking {
554                        self.detect_ad(event)
555                    } else {
556                        skip_networking
557                    };
558
559                    let skip_networking = if !skip_networking
560                        && (self.only_html || self.ignore_visuals)
561                        && (javascript_resource || event.resource_type == ResourceType::Document)
562                    {
563                        ignore_script_embedded(current_url)
564                    } else {
565                        skip_networking
566                    };
567
568                    // analytics check
569                    let skip_networking = if !skip_networking && javascript_resource {
570                        self.ignore_script(
571                            current_url,
572                            self.block_analytics,
573                            self.intercept_manager,
574                        )
575                    } else {
576                        skip_networking
577                    };
578
579                    let network_event = event.resource_type == ResourceType::Xhr
580                        || event.resource_type == ResourceType::WebSocket
581                        || event.resource_type == ResourceType::Fetch;
582
583                    // XHR check
584                    let skip_networking = self.skip_xhr(skip_networking, &event, network_event);
585
586                    // custom interception layer.
587                    let skip_networking = if !skip_networking
588                        && (javascript_resource
589                            || network_resource
590                            || event.resource_type == ResourceType::Document)
591                    {
592                        self.intercept_manager.intercept_detection(
593                            &event.request.url,
594                            self.ignore_visuals,
595                            network_resource,
596                        )
597                    } else {
598                        skip_networking
599                    };
600
601                    let skip_networking =
602                        if !skip_networking && (javascript_resource || network_event) {
603                            block_website(&event.request.url)
604                        } else {
605                            skip_networking
606                        };
607
608                    if skip_networking {
609                        let fullfill_params =
610                            crate::handler::network::fetch::FulfillRequestParams::new(
611                                event.request_id.clone(),
612                                200,
613                            );
614                        self.push_cdp_request(fullfill_params);
615                    } else {
616                        self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
617                    }
618                }
619            }
620        }
621
622        // if self.only_html {
623        //     self.made_request = true;
624        // }
625    }
626
627    /// Perform a page intercept for chrome
628    #[cfg(feature = "adblock")]
629    pub fn detect_ad(&self, event: &EventRequestPaused) -> bool {
630        use adblock::{
631            lists::{FilterSet, ParseOptions, RuleTypes},
632            Engine,
633        };
634
635        lazy_static::lazy_static! {
636            static ref AD_ENGINE: Engine = {
637                let mut filter_set = FilterSet::new(false);
638                let mut rules = ParseOptions::default();
639                rules.rule_types = RuleTypes::All;
640
641                filter_set.add_filters(
642                    &*crate::handler::blockers::adblock_patterns::ADBLOCK_PATTERNS,
643                    rules,
644                );
645
646                Engine::from_filter_set(filter_set, true)
647            };
648        };
649
650        let blockable = ResourceType::Image == event.resource_type
651            || event.resource_type == ResourceType::Media
652            || event.resource_type == ResourceType::Stylesheet
653            || event.resource_type == ResourceType::Document
654            || event.resource_type == ResourceType::Fetch
655            || event.resource_type == ResourceType::Xhr;
656
657        let u = &event.request.url;
658
659        let block_request = blockable
660            // set it to example.com for 3rd party handling is_same_site
661        && {
662            let request = adblock::request::Request::preparsed(
663                 &u,
664                 "example.com",
665                 "example.com",
666                 &event.resource_type.as_ref().to_lowercase(),
667                 !event.request.is_same_site.unwrap_or_default());
668
669            AD_ENGINE.check_network_request(&request).matched
670        };
671
672        block_request
673    }
674
675    pub fn on_fetch_auth_required(&mut self, event: &EventAuthRequired) {
676        let response = if self
677            .attempted_authentications
678            .contains(event.request_id.as_ref())
679        {
680            AuthChallengeResponseResponse::CancelAuth
681        } else if self.credentials.is_some() {
682            self.attempted_authentications
683                .insert(event.request_id.clone().into());
684            AuthChallengeResponseResponse::ProvideCredentials
685        } else {
686            AuthChallengeResponseResponse::Default
687        };
688
689        let mut auth = AuthChallengeResponse::new(response);
690        if let Some(creds) = self.credentials.clone() {
691            auth.username = Some(creds.username);
692            auth.password = Some(creds.password);
693        }
694        self.push_cdp_request(ContinueWithAuthParams::new(event.request_id.clone(), auth));
695    }
696
697    pub fn set_offline_mode(&mut self, value: bool) {
698        if self.offline == value {
699            return;
700        }
701        self.offline = value;
702        if let Ok(network) = EmulateNetworkConditionsParams::builder()
703            .offline(self.offline)
704            .latency(0)
705            .download_throughput(-1.)
706            .upload_throughput(-1.)
707            .build()
708        {
709            self.push_cdp_request(network);
710        }
711    }
712
713    /// Request interception doesn't happen for data URLs with Network Service.
714    pub fn on_request_will_be_sent(&mut self, event: &EventRequestWillBeSent) {
715        if self.protocol_request_interception_enabled && !event.request.url.starts_with("data:") {
716            if let Some(interception_id) = self
717                .request_id_to_interception_id
718                .remove(event.request_id.as_ref())
719            {
720                self.on_request(event, Some(interception_id));
721            } else {
722                // TODO remove the clone for event
723                self.requests_will_be_sent
724                    .insert(event.request_id.clone(), event.clone());
725            }
726        } else {
727            self.on_request(event, None);
728        }
729    }
730
731    pub fn on_request_served_from_cache(&mut self, event: &EventRequestServedFromCache) {
732        if let Some(request) = self.requests.get_mut(event.request_id.as_ref()) {
733            request.from_memory_cache = true;
734        }
735    }
736
737    pub fn on_response_received(&mut self, event: &EventResponseReceived) {
738        if let Some(mut request) = self.requests.remove(event.request_id.as_ref()) {
739            request.set_response(event.response.clone());
740            self.queued_events
741                .push_back(NetworkEvent::RequestFinished(request))
742        }
743    }
744
745    pub fn on_network_loading_finished(&mut self, event: &EventLoadingFinished) {
746        if let Some(request) = self.requests.remove(event.request_id.as_ref()) {
747            if let Some(interception_id) = request.interception_id.as_ref() {
748                self.attempted_authentications
749                    .remove(interception_id.as_ref());
750            }
751            self.queued_events
752                .push_back(NetworkEvent::RequestFinished(request));
753        }
754    }
755
756    pub fn on_network_loading_failed(&mut self, event: &EventLoadingFailed) {
757        if let Some(mut request) = self.requests.remove(event.request_id.as_ref()) {
758            request.failure_text = Some(event.error_text.clone());
759            if let Some(interception_id) = request.interception_id.as_ref() {
760                self.attempted_authentications
761                    .remove(interception_id.as_ref());
762            }
763            self.queued_events
764                .push_back(NetworkEvent::RequestFailed(request));
765        }
766    }
767
768    fn on_request(
769        &mut self,
770        event: &EventRequestWillBeSent,
771        interception_id: Option<InterceptionId>,
772    ) {
773        let mut redirect_chain = Vec::new();
774        if let Some(redirect_resp) = event.redirect_response.as_ref() {
775            if let Some(mut request) = self.requests.remove(event.request_id.as_ref()) {
776                self.handle_request_redirect(&mut request, redirect_resp.clone());
777                redirect_chain = std::mem::take(&mut request.redirect_chain);
778                redirect_chain.push(request);
779            }
780        }
781        let request = HttpRequest::new(
782            event.request_id.clone(),
783            event.frame_id.clone(),
784            interception_id,
785            self.user_request_interception_enabled,
786            redirect_chain,
787        );
788
789        self.requests.insert(event.request_id.clone(), request);
790        self.queued_events
791            .push_back(NetworkEvent::Request(event.request_id.clone()));
792    }
793
794    fn handle_request_redirect(&mut self, request: &mut HttpRequest, response: Response) {
795        request.set_response(response);
796        if let Some(interception_id) = request.interception_id.as_ref() {
797            self.attempted_authentications
798                .remove(interception_id.as_ref());
799        }
800    }
801}
802
803#[derive(Debug)]
804pub enum NetworkEvent {
805    SendCdpRequest((MethodId, serde_json::Value)),
806    Request(RequestId),
807    Response(RequestId),
808    RequestFailed(HttpRequest),
809    RequestFinished(HttpRequest),
810}