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 static ref JS_FRAMEWORK_ALLOW: Vec<&'static str> = vec![
42 "jquery", "angular",
44 "react", "vue", "bootstrap",
47 "d3",
48 "lodash",
49 "ajax",
50 "application",
51 "app", "main",
53 "index",
54 "bundle",
55 "vendor",
56 "runtime",
57 "polyfill",
58 "scripts",
59 "/wp-content/js/", "https://m.stripe.network/",
62 "https://challenges.cloudflare.com/",
63 "https://js.stripe.com/",
64 "https://cdn.prod.website-files.com/", "https://cdnjs.cloudflare.com/", "https://code.jquery.com/jquery-"
67 ];
68
69 pub static ref ALLOWED_MATCHER: AhoCorasick = AhoCorasick::new(JS_FRAMEWORK_ALLOW.iter()).unwrap();
71
72 pub static ref JS_FRAMEWORK_PATH: phf::Set<&'static str> = {
74 phf::phf_set! {
75 "_next/static/", "_astro/",
77 }
78 };
79
80 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 pub static ref IGNORE_VISUAL_RESOURCE_MAP: phf::Set<&'static str> = phf::phf_set! {
109 "Image",
110 "Media",
111 "Font"
112 };
113
114 pub static ref IGNORE_NETWORKING_RESOURCE_MAP: phf::Set<&'static str> = phf::phf_set! {
116 "Prefetch",
117 "Ping",
118 };
119
120 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 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 pub ignore_visuals: bool,
144 pub block_stylesheets: bool,
146 pub block_javascript: bool,
148 pub block_analytics: bool,
150 pub only_html: bool,
152 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 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 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 if pos < url.len() {
289 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 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 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 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 fn skip_xhr(&self, skip_networking: bool, event: &EventRequestPaused) -> bool {
342 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 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 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 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 let skip_networking = self.skip_xhr(skip_networking, &event);
455
456 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 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 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 let skip_networking = self.skip_xhr(skip_networking, &event);
567
568 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 }
601
602 #[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 && {
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 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 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}