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