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,
139 protocol_request_interception_enabled: bool,
140 offline: bool,
141 request_timeout: Duration,
142 pub ignore_visuals: bool,
145 pub block_stylesheets: bool,
147 pub block_javascript: bool,
149 pub block_analytics: bool,
151 pub only_html: bool,
153 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 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 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 if pos < url.len() {
291 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 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 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 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 fn skip_xhr(&self, skip_networking: bool, event: &EventRequestPaused) -> bool {
344 if !skip_networking
346 && (event.resource_type == ResourceType::Xhr
347 || event.resource_type == ResourceType::WebSocket
348 || event.resource_type == ResourceType::Fetch)
349 {
350 let request_url = event.request.url.as_str();
351
352 let skip_analytics = self.block_analytics && ignore_script_xhr(request_url);
354
355 if skip_analytics {
356 true
357 } else if self.block_stylesheets || self.ignore_visuals {
358 let block_css = self.block_stylesheets;
359 let block_media = self.ignore_visuals;
360
361 let mut block_request = false;
362
363 if let Some(position) = request_url.rfind('.') {
364 let hlen = request_url.len();
365 let has_asset = hlen - position;
366
367 if has_asset >= 3 {
368 let next_position = position + 1;
369
370 if block_media
371 && IGNORE_XHR_ASSETS.contains::<CaseInsensitiveString>(
372 &request_url[next_position..].into(),
373 )
374 {
375 block_request = true;
376 } else if block_css {
377 block_request =
378 CaseInsensitiveString::from(request_url[next_position..].as_bytes())
379 .contains(&**CSS_EXTENSION)
380 }
381 }
382 }
383
384 if !block_request {
385 block_request = ignore_script_xhr_media(request_url);
386 }
387
388 block_request
389 } else {
390 skip_networking
391 }
392 } else {
393 skip_networking
394 }
395 }
396
397 #[cfg(not(feature = "adblock"))]
398 pub fn on_fetch_request_paused(&mut self, event: &EventRequestPaused) {
399 if !self.user_request_interception_enabled && self.protocol_request_interception_enabled {
400 self.push_cdp_request(ContinueRequestParams::new(event.request_id.clone()))
401 } else {
402 if let Some(network_id) = event.network_id.as_ref() {
403 if let Some(request_will_be_sent) =
404 self.requests_will_be_sent.remove(network_id.as_ref())
405 {
406 self.on_request(&request_will_be_sent, Some(event.request_id.clone().into()));
407 } else {
408 let current_url = event.request.url.as_str();
409 let javascript_resource = event.resource_type == ResourceType::Script;
410 let skip_networking = event.resource_type == ResourceType::Other
411 || event.resource_type == ResourceType::Manifest
412 || event.resource_type == ResourceType::CspViolationReport
413 || event.resource_type == ResourceType::Ping
414 || event.resource_type == ResourceType::Prefetch;
415 let network_resource = event.resource_type == ResourceType::Xhr
416 || event.resource_type == ResourceType::Fetch
417 || event.resource_type == ResourceType::WebSocket;
418
419 let skip_networking = if !skip_networking {
421 IGNORE_NETWORKING_RESOURCE_MAP.contains(event.resource_type.as_ref())
422 || self.ignore_visuals
423 && (IGNORE_VISUAL_RESOURCE_MAP
424 .contains(event.resource_type.as_ref()))
425 || self.block_stylesheets
426 && ResourceType::Stylesheet == event.resource_type
427 || self.block_javascript
428 && javascript_resource
429 && self.intercept_manager == NetworkInterceptManager::UNKNOWN
430 && !ALLOWED_MATCHER.is_match(current_url)
431 } else {
432 skip_networking
433 };
434
435 let skip_networking = if !skip_networking
436 && (self.only_html || self.ignore_visuals)
437 && (javascript_resource || event.resource_type == ResourceType::Document)
438 {
439 ignore_script_embedded(current_url)
440 } else {
441 skip_networking
442 };
443
444 let skip_networking = if !skip_networking && javascript_resource {
446 self.ignore_script(
447 current_url,
448 self.block_analytics,
449 self.intercept_manager,
450 )
451 } else {
452 skip_networking
453 };
454
455 let skip_networking = self.skip_xhr(skip_networking, &event);
457
458 let skip_networking = if !skip_networking
460 && (javascript_resource
461 || network_resource
462 || event.resource_type == ResourceType::Document)
463 {
464 self.intercept_manager.intercept_detection(
465 &event.request.url,
466 self.ignore_visuals,
467 network_resource,
468 )
469 } else {
470 skip_networking
471 };
472
473 if skip_networking {
474 tracing::debug!(
475 "Blocked: {:?} - {}",
476 event.resource_type,
477 event.request.url
478 );
479 let fullfill_params =
480 crate::handler::network::fetch::FulfillRequestParams::new(
481 event.request_id.clone(),
482 200,
483 );
484 self.push_cdp_request(fullfill_params);
485 } else {
486 tracing::debug!(
487 "Allowed: {:?} - {}",
488 event.resource_type,
489 event.request.url
490 );
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}