Skip to main content

net/fetch/
methods.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::{io, mem, str};
8
9use base64::Engine as _;
10use base64::engine::general_purpose;
11use content_security_policy as csp;
12use crossbeam_channel::Sender;
13use devtools_traits::DevtoolsControlMsg;
14use embedder_traits::resources::{self, Resource};
15use headers::{AccessControlExposeHeaders, ContentType, HeaderMapExt};
16use http::header::{self, HeaderMap, HeaderName, RANGE};
17use http::{HeaderValue, Method, StatusCode};
18use ipc_channel::ipc::{self, IpcSender};
19use log::{debug, trace, warn};
20use malloc_size_of_derive::MallocSizeOf;
21use mime::{self, Mime};
22use net_traits::fetch::headers::{determine_nosniff, extract_mime_type_as_mime};
23use net_traits::filemanager_thread::{FileTokenCheck, RelativePos};
24use net_traits::http_status::HttpStatus;
25use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer};
26use net_traits::request::{
27    BodyChunkRequest, BodyChunkResponse, CredentialsMode, Destination, Initiator,
28    InsecureRequestsPolicy, InternalRequest, Origin, ParserMetadata, RedirectMode, Referrer,
29    Request, RequestBody, RequestId, RequestMode, ResponseTainting, is_cors_safelisted_method,
30    is_cors_safelisted_request_header,
31};
32use net_traits::response::{Response, ResponseBody, ResponseType, TerminationReason};
33use net_traits::{
34    FetchTaskTarget, NetworkError, ReferrerPolicy, ResourceAttribute, ResourceFetchTiming,
35    ResourceFetchTimingContainer, ResourceTimeValue, ResourceTimingType, WebSocketDomAction,
36    WebSocketNetworkEvent, set_default_accept_language,
37};
38use parking_lot::Mutex;
39use rustc_hash::FxHashMap;
40use rustls_pki_types::CertificateDer;
41use serde::{Deserialize, Serialize};
42use servo_base::generic_channel::CallbackSetter;
43use servo_base::id::PipelineId;
44use servo_url::{Host, ServoUrl};
45use tokio::sync::Mutex as TokioMutex;
46use tokio::sync::mpsc::{UnboundedReceiver as TokioReceiver, UnboundedSender as TokioSender};
47
48use crate::connector::CACertificates;
49use crate::devtools::{
50    send_early_httprequest_to_devtools, send_response_to_devtools, send_security_info_to_devtools,
51};
52use crate::fetch::cors_cache::CorsCache;
53use crate::fetch::fetch_params::{
54    ConsumePreloadedResources, FetchParams, SharedPreloadedResources,
55};
56use crate::filemanager_thread::FileManager;
57use crate::http_loader::{HttpState, determine_requests_referrer, http_fetch, set_default_accept};
58use crate::protocols::{ProtocolRegistry, is_url_potentially_trustworthy};
59use crate::request_interceptor::RequestInterceptor;
60use crate::subresource_integrity::is_response_integrity_valid;
61
62pub type Target<'a> = &'a mut (dyn FetchTaskTarget + Send);
63
64#[derive(Clone, Deserialize, Serialize)]
65pub enum Data {
66    Payload(Vec<u8>),
67    Done,
68    Cancelled,
69    Error(NetworkError),
70}
71
72pub struct WebSocketChannel {
73    pub sender: IpcSender<WebSocketNetworkEvent>,
74    pub receiver: Option<CallbackSetter<WebSocketDomAction>>,
75}
76
77impl WebSocketChannel {
78    pub fn new(
79        sender: IpcSender<WebSocketNetworkEvent>,
80        receiver: Option<CallbackSetter<WebSocketDomAction>>,
81    ) -> Self {
82        Self { sender, receiver }
83    }
84}
85
86/// Used to keep track of keep-alive requests
87#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
88pub struct InFlightKeepAliveRecord {
89    pub(crate) request_id: RequestId,
90    /// Used to keep track of size of keep-alive requests.
91    pub(crate) keep_alive_body_length: u64,
92}
93
94pub type SharedInflightKeepAliveRecords =
95    Arc<Mutex<FxHashMap<PipelineId, Vec<InFlightKeepAliveRecord>>>>;
96
97#[derive(Clone)]
98pub struct FetchContext {
99    pub state: Arc<HttpState>,
100    pub user_agent: String,
101    pub devtools_chan: Option<Sender<DevtoolsControlMsg>>,
102    pub filemanager: FileManager,
103    pub file_token: FileTokenCheck,
104    pub request_interceptor: Arc<TokioMutex<RequestInterceptor>>,
105    pub cancellation_listener: Arc<CancellationListener>,
106    pub timing: ResourceFetchTimingContainer,
107    pub protocols: Arc<ProtocolRegistry>,
108    pub websocket_chan: Option<Arc<Mutex<WebSocketChannel>>>,
109    pub ca_certificates: CACertificates<'static>,
110    pub ignore_certificate_errors: bool,
111    pub preloaded_resources: SharedPreloadedResources,
112    pub in_flight_keep_alive_records: SharedInflightKeepAliveRecords,
113}
114
115#[derive(Default)]
116pub struct CancellationListener {
117    cancelled: AtomicBool,
118}
119
120impl CancellationListener {
121    pub(crate) fn cancelled(&self) -> bool {
122        self.cancelled.load(Ordering::Relaxed)
123    }
124
125    pub(crate) fn cancel(&self) {
126        self.cancelled.store(true, Ordering::Relaxed)
127    }
128}
129
130/// Closes the current process request body sender state when the net side fetch invocation ends.
131/// Redirect replay for navigation requests happens in a later fetch invocation with a newly
132/// deserialized "RequestBody", so each invocation owns closing only its local copy.
133pub(crate) struct AutoRequestBodyStreamCloser {
134    body: Option<RequestBody>,
135}
136
137impl AutoRequestBodyStreamCloser {
138    pub(crate) fn new(body: Option<&RequestBody>) -> Self {
139        Self {
140            body: body.cloned(),
141        }
142    }
143
144    pub(crate) fn disarm(&mut self) {
145        self.body = None;
146    }
147}
148
149impl Drop for AutoRequestBodyStreamCloser {
150    fn drop(&mut self) {
151        if let Some(body) = self.body.take() {
152            body.close_stream();
153        }
154    }
155}
156
157/// A manual navigation redirect keeps the same request body alive for a later net side redirect
158/// replay invocation. That later invocation becomes the next lifecycle owner and must close its
159/// local shared sender state once it reaches a terminal response.
160pub(crate) fn transfers_request_body_stream_to_later_manual_redirect(
161    request: &Request,
162    response: &Response,
163) -> bool {
164    request.mode == RequestMode::Navigate &&
165        request.redirect_mode == RedirectMode::Manual &&
166        request.body.is_some() &&
167        !response.is_network_error() &&
168        response
169            .actual_response()
170            .status
171            .try_code()
172            .is_some_and(|status| status.is_redirection())
173}
174
175pub type DoneChannel = Option<(TokioSender<Data>, TokioReceiver<Data>)>;
176
177/// [Fetch](https://fetch.spec.whatwg.org#concept-fetch)
178pub async fn fetch(request: Request, target: Target<'_>, context: &FetchContext) -> Response {
179    // Steps 7,4 of https://w3c.github.io/resource-timing/#processing-model
180    // rev order okay since spec says they're equal - https://w3c.github.io/resource-timing/#dfn-starttime
181    context.timing.set_attributes(&[
182        ResourceAttribute::FetchStart,
183        ResourceAttribute::StartTime(ResourceTimeValue::FetchStart),
184    ]);
185    fetch_with_cors_cache(request, &mut CorsCache::default(), target, context).await
186}
187
188/// Continuation of fetch from step 8.
189///
190/// <https://fetch.spec.whatwg.org#concept-fetch>
191pub async fn fetch_with_cors_cache(
192    request: Request,
193    cache: &mut CorsCache,
194    target: Target<'_>,
195    context: &FetchContext,
196) -> Response {
197    // Step 8. Let fetchParams be a new fetch params whose request is request
198    let mut fetch_params = FetchParams::new(request);
199    // Each net side fetch invocation owns closing its local deserialized request-body sender state
200    // once this function returns, even if navigation redirect replay later starts a new fetch with
201    // a fresh "RequestBody" copy.
202    let mut request_body_stream_closer =
203        AutoRequestBodyStreamCloser::new(fetch_params.request.body.as_ref());
204    let request = &mut fetch_params.request;
205
206    // Step 4. Populate request from client given request.
207    request.populate_request_from_client();
208
209    // Step 5. If request’s client is non-null, then:
210    // TODO
211    // Step 5.1. Set taskDestination to request’s client’s global object.
212    // TODO
213    // Step 5.2. Set crossOriginIsolatedCapability to request’s client’s cross-origin isolated capability.
214    // TODO
215
216    // Step 10. If all of the following conditions are true:
217    if
218    // - request’s URL’s scheme is an HTTP(S) scheme
219    matches!(request.current_url().scheme(), "http" | "https")
220        // - request’s mode is "same-origin", "cors", or "no-cors"
221        && matches!(request.mode, RequestMode::SameOrigin | RequestMode::CorsMode | RequestMode::NoCors)
222        // - request’s method is `GET`
223        && matches!(request.method, Method::GET)
224        // - request’s unsafe-request flag is not set or request’s header list is empty
225        && (!request.unsafe_request || request.headers.is_empty())
226    {
227        // - request’s client is not null, and request’s client’s global object is a Window object
228        if let Some(client) = request.client.as_ref() {
229            // Step 10.1. Assert: request’s origin is same origin with request’s client’s origin.
230            assert!(request.origin == client.origin);
231            // Step 10.2. Let onPreloadedResponseAvailable be an algorithm that runs the
232            // following step given a response response: set fetchParams’s preloaded response candidate to response.
233            // Step 10.3. Let foundPreloadedResource be the result of invoking consume a preloaded resource
234            // for request’s client, given request’s URL, request’s destination, request’s mode,
235            // request’s credentials mode, request’s integrity metadata, and onPreloadedResponseAvailable.
236            // Step 10.4. If foundPreloadedResource is true and fetchParams’s preloaded response candidate is null,
237            // then set fetchParams’s preloaded response candidate to "pending".
238            if let Some(candidate) =
239                client.consume_preloaded_resource(request, context.preloaded_resources.clone())
240            {
241                fetch_params.preload_response_candidate = candidate;
242            }
243        }
244    }
245
246    // Step 11. If request’s header list does not contain `Accept`, then:
247    set_default_accept(request);
248
249    // Step 12. If request’s header list does not contain `Accept-Language`, then user agents should
250    // append (`Accept-Language, an appropriate header value) to request’s header list.
251    set_default_accept_language(&mut request.headers);
252
253    // Step 15. If request’s internal priority is null, then use request’s priority, initiator,
254    // destination, and render-blocking in an implementation-defined manner to set request’s
255    // internal priority to an implementation-defined object.
256    // TODO: figure out what a Priority object is.
257
258    // Step 15. If request is a subresource request:
259    //
260    // We only check for keep-alive requests here, since that's currently the only usage
261    let should_track_in_flight_record = request.keep_alive && request.is_subresource_request();
262    let pipeline_id = request.pipeline_id;
263
264    if should_track_in_flight_record {
265        // Step 15.1. Let record be a new fetch record whose request is request
266        // and controller is fetchParams’s controller.
267        let record = InFlightKeepAliveRecord {
268            request_id: request.id,
269            keep_alive_body_length: request.keep_alive_body_length(),
270        };
271        // Step 15.2. Append record to request’s client’s fetch group’s fetch records.
272        let mut in_flight_records = context.in_flight_keep_alive_records.lock();
273        in_flight_records
274            .entry(pipeline_id.expect("Must always set a pipeline ID for keep-alive requests"))
275            .or_default()
276            .push(record);
277    };
278    let request_id = request.id;
279
280    // Step 17: Run main fetch given fetchParams.
281    let response = main_fetch(&mut fetch_params, cache, false, target, &mut None, context).await;
282
283    if transfers_request_body_stream_to_later_manual_redirect(&fetch_params.request, &response) {
284        request_body_stream_closer.disarm();
285    }
286
287    // Mimics <https://fetch.spec.whatwg.org/#done-flag>
288    if should_track_in_flight_record {
289        context
290            .in_flight_keep_alive_records
291            .lock()
292            .get_mut(&pipeline_id.expect("Must always set a pipeline ID for keep-alive requests"))
293            .expect("Must always have initialized tracked requests before starting fetch")
294            .retain(|record| record.request_id != request_id);
295    }
296
297    // Step 18: Return fetchParams’s controller.
298    // TODO: We don't implement fetchParams as defined in the spec
299    response
300}
301
302pub(crate) fn convert_request_to_csp_request(request: &Request) -> Option<csp::Request> {
303    if request.is_internal_request == InternalRequest::Yes {
304        return None;
305    }
306    let origin = match &request.origin {
307        Origin::Client => return None,
308        Origin::Origin(origin) => origin,
309    };
310
311    let csp_request = csp::Request {
312        url: request.url().into_url(),
313        current_url: request.current_url().into_url(),
314        origin: origin.clone().into_url_origin(),
315        redirect_count: request.redirect_count,
316        destination: request.destination,
317        initiator: match request.initiator {
318            Initiator::Download => csp::Initiator::Download,
319            Initiator::ImageSet => csp::Initiator::ImageSet,
320            Initiator::Manifest => csp::Initiator::Manifest,
321            Initiator::Prefetch => csp::Initiator::Prefetch,
322            _ => csp::Initiator::None,
323        },
324        nonce: request.cryptographic_nonce_metadata.clone(),
325        integrity_metadata: request.integrity_metadata.clone(),
326        parser_metadata: match request.parser_metadata {
327            ParserMetadata::ParserInserted => csp::ParserMetadata::ParserInserted,
328            ParserMetadata::NotParserInserted => csp::ParserMetadata::NotParserInserted,
329            ParserMetadata::Default => csp::ParserMetadata::None,
330        },
331    };
332    Some(csp_request)
333}
334
335/// <https://www.w3.org/TR/CSP/#should-block-request>
336pub fn should_request_be_blocked_by_csp(
337    csp_request: &csp::Request,
338    policy_container: &PolicyContainer,
339) -> (csp::CheckResult, Vec<csp::Violation>) {
340    policy_container
341        .csp_list
342        .as_ref()
343        .map(|c| c.should_request_be_blocked(csp_request))
344        .unwrap_or((csp::CheckResult::Allowed, Vec::new()))
345}
346
347/// <https://www.w3.org/TR/CSP/#report-for-request>
348pub fn report_violations_for_request_by_csp(
349    csp_request: &csp::Request,
350    policy_container: &PolicyContainer,
351) -> Vec<csp::Violation> {
352    policy_container
353        .csp_list
354        .as_ref()
355        .map(|c| c.report_violations_for_request(csp_request))
356        .unwrap_or_default()
357}
358
359fn should_response_be_blocked_by_csp(
360    csp_request: &csp::Request,
361    response: &Response,
362    policy_container: &PolicyContainer,
363) -> (csp::CheckResult, Vec<csp::Violation>) {
364    if response.is_network_error() {
365        return (csp::CheckResult::Allowed, Vec::new());
366    }
367    let csp_response = csp::Response {
368        url: response
369            .actual_response()
370            .url()
371            .cloned()
372            // NOTE(pylbrecht): for WebSocket connections, the URL scheme is converted to http(s)
373            // to integrate with fetch(). We need to convert it back to ws(s) to get valid CSP
374            // checks.
375            // https://github.com/w3c/webappsec-csp/issues/532
376            .map(|mut url| {
377                match csp_request.url.scheme() {
378                    "ws" | "wss" => {
379                        url.as_mut_url()
380                            .set_scheme(csp_request.url.scheme())
381                            .expect("failed to set URL scheme");
382                    },
383                    _ => {},
384                };
385                url
386            })
387            .expect("response must have a url")
388            .into_url(),
389        redirect_count: csp_request.redirect_count,
390    };
391    policy_container
392        .csp_list
393        .as_ref()
394        .map(|c| c.should_response_to_request_be_blocked(csp_request, &csp_response))
395        .unwrap_or((csp::CheckResult::Allowed, Vec::new()))
396}
397
398/// [Main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch)
399pub async fn main_fetch(
400    fetch_params: &mut FetchParams,
401    cache: &mut CorsCache,
402    recursive_flag: bool,
403    target: Target<'_>,
404    done_chan: &mut DoneChannel,
405    context: &FetchContext,
406) -> Response {
407    // Step 1: Let request be fetchParam's request.
408    let request = &mut fetch_params.request;
409    send_early_httprequest_to_devtools(request, context);
410    // Step 2: Let response be null.
411    let mut response = None;
412
413    // Servo internal: return a crash error when a crash error page is needed
414    if let Some(ref details) = request.crash {
415        response = Some(Response::network_error(NetworkError::Crash(
416            details.clone(),
417        )));
418    }
419
420    // Step 3: If request’s local-URLs-only flag is set and request’s
421    // current URL is not local, then set response to a network error.
422    if request.local_urls_only &&
423        !matches!(
424            request.current_url().scheme(),
425            "about" | "blob" | "data" | "filesystem"
426        )
427    {
428        response = Some(Response::network_error(NetworkError::UnsupportedScheme));
429    }
430
431    // The request should have a valid policy_container associated with it.
432    let policy_container = match &request.policy_container {
433        RequestPolicyContainer::Client => unreachable!(),
434        RequestPolicyContainer::PolicyContainer(container) => container.to_owned(),
435    };
436    let csp_request = convert_request_to_csp_request(request);
437    if let Some(csp_request) = csp_request.as_ref() {
438        // Step 2.2.
439        let violations = report_violations_for_request_by_csp(csp_request, &policy_container);
440
441        if !violations.is_empty() {
442            target.process_csp_violations(request, violations);
443        }
444    };
445
446    // Step 3.
447    // TODO: handle request abort.
448
449    // Step 4. Upgrade request to a potentially trustworthy URL, if appropriate.
450    if should_upgrade_request_to_potentially_trustworthy(request, context) ||
451        should_upgrade_mixed_content_request(request, &context.protocols)
452    {
453        trace!(
454            "upgrading {} targeting {:?}",
455            request.current_url(),
456            request.destination
457        );
458        if let Some(new_scheme) = match request.current_url().scheme() {
459            "http" => Some("https"),
460            "ws" => Some("wss"),
461            _ => None,
462        } {
463            request
464                .current_url_mut()
465                .as_mut_url()
466                .set_scheme(new_scheme)
467                .unwrap();
468        }
469    } else {
470        trace!(
471            "not upgrading {} targeting {:?} with {:?}",
472            request.current_url(),
473            request.destination,
474            request.insecure_requests_policy
475        );
476    }
477    if let Some(csp_request) = csp_request.as_ref() {
478        // Step 7. If should request be blocked due to a bad port, should fetching request be blocked
479        // as mixed content, or should request be blocked by Content Security Policy returns blocked,
480        // then set response to a network error.
481        let (check_result, violations) =
482            should_request_be_blocked_by_csp(csp_request, &policy_container);
483
484        if !violations.is_empty() {
485            target.process_csp_violations(request, violations);
486        }
487
488        if check_result == csp::CheckResult::Blocked {
489            warn!("Request blocked by CSP");
490            response = Some(Response::network_error(NetworkError::ContentSecurityPolicy))
491        }
492    };
493    if should_request_be_blocked_due_to_a_bad_port(&request.current_url()) {
494        response = Some(Response::network_error(NetworkError::InvalidPort));
495    }
496    if should_request_be_blocked_as_mixed_content(request, &context.protocols) {
497        response = Some(Response::network_error(NetworkError::MixedContent));
498    }
499
500    // Step 8: If request’s referrer policy is the empty string, then set request’s referrer policy
501    // to request’s policy container’s referrer policy.
502    if request.referrer_policy == ReferrerPolicy::EmptyString {
503        request.referrer_policy = policy_container.get_referrer_policy();
504    }
505
506    let referrer_url = match mem::replace(&mut request.referrer, Referrer::NoReferrer) {
507        Referrer::NoReferrer => None,
508        Referrer::ReferrerUrl(referrer_source) | Referrer::Client(referrer_source) => {
509            request.headers.remove(header::REFERER);
510            determine_requests_referrer(
511                request.referrer_policy,
512                referrer_source,
513                request.current_url(),
514            )
515        },
516    };
517    request.referrer = referrer_url.map_or(Referrer::NoReferrer, Referrer::ReferrerUrl);
518
519    // Step 9.
520    // TODO: handle FTP URLs.
521
522    // Step 10.
523    context
524        .state
525        .hsts_list
526        .read()
527        .apply_hsts_rules(request.current_url_mut());
528
529    // Step 11.
530    // Not applicable: see fetch_async.
531
532    let current_url = request.current_url();
533    let current_scheme = current_url.scheme();
534
535    // Intercept the request and maybe override the response.
536    context
537        .request_interceptor
538        .lock()
539        .await
540        .intercept_request(request, &mut response, context)
541        .await;
542
543    let mut response = match response {
544        Some(response) => response,
545        None => {
546            // Step 12. If response is null, then set response to the result
547            // of running the steps corresponding to the first matching statement:
548            let same_origin = if let Origin::Origin(ref origin) = request.origin {
549                *origin == request.current_url_with_blob_claim().origin()
550            } else {
551                false
552            };
553
554            // fetchParams’s preloaded response candidate is non-null
555            if let Some((response, preload_id)) =
556                fetch_params.preload_response_candidate.response().await
557            {
558                response.get_resource_timing().inner().preloaded = true;
559                context
560                    .preloaded_resources
561                    .lock()
562                    .unwrap()
563                    .remove(&preload_id);
564                response
565            }
566            // request's current URL's origin is same origin with request's origin, and request's
567            // response tainting is "basic"
568            else if (same_origin && request.response_tainting == ResponseTainting::Basic) ||
569                // request's current URL's scheme is "data"
570                current_scheme == "data" ||
571                // Note: Although it is not part of the specification, we make an exception here
572                // for custom protocols that are explicitly marked as active for fetch.
573                context.protocols.is_fetchable(current_scheme) ||
574                // request's mode is "navigate" or "websocket"
575                matches!(
576                    request.mode,
577                    RequestMode::Navigate | RequestMode::WebSocket { .. }
578                )
579            {
580                // Substep 1. Set request's response tainting to "basic".
581                request.response_tainting = ResponseTainting::Basic;
582
583                // Substep 2. Return the result of running scheme fetch given fetchParams.
584                scheme_fetch(fetch_params, cache, target, done_chan, context).await
585            } else if request.mode == RequestMode::SameOrigin {
586                Response::network_error(NetworkError::CrossOriginResponse)
587            } else if request.mode == RequestMode::NoCors {
588                // Substep 1. If request's redirect mode is not "follow", then return a network error.
589                if request.redirect_mode != RedirectMode::Follow {
590                    Response::network_error(NetworkError::RedirectError)
591                } else {
592                    // Substep 2. Set request's response tainting to "opaque".
593                    request.response_tainting = ResponseTainting::Opaque;
594
595                    // Substep 3. Return the result of running scheme fetch given fetchParams.
596                    scheme_fetch(fetch_params, cache, target, done_chan, context).await
597                }
598            } else if !matches!(current_scheme, "http" | "https") {
599                Response::network_error(NetworkError::UnsupportedScheme)
600            } else if request.use_cors_preflight ||
601                (request.unsafe_request &&
602                    (!is_cors_safelisted_method(&request.method) ||
603                        request.headers.iter().any(|(name, value)| {
604                            !is_cors_safelisted_request_header(&name, &value)
605                        })))
606            {
607                // Substep 1.
608                request.response_tainting = ResponseTainting::CorsTainting;
609                // Substep 2.
610                let response = http_fetch(
611                    fetch_params,
612                    cache,
613                    true,
614                    true,
615                    false,
616                    target,
617                    done_chan,
618                    context,
619                )
620                .await;
621                // Substep 3.
622                if response.is_network_error() {
623                    // TODO clear cache entries using request
624                }
625                // Substep 4.
626                response
627            } else {
628                // Substep 1.
629                request.response_tainting = ResponseTainting::CorsTainting;
630                // Substep 2.
631                http_fetch(
632                    fetch_params,
633                    cache,
634                    true,
635                    false,
636                    false,
637                    target,
638                    done_chan,
639                    context,
640                )
641                .await
642            }
643        },
644    };
645
646    // Step 13. If recursive is true, then return response.
647    if recursive_flag {
648        return response;
649    }
650
651    // reborrow request to avoid double mutable borrow
652    let request = &mut fetch_params.request;
653
654    // Step 14. If response is not a network error and response is not a filtered response, then:
655    let mut response = if !response.is_network_error() && response.internal_response.is_none() {
656        // Step 14.1 If request’s response tainting is "cors", then:
657        if request.response_tainting == ResponseTainting::CorsTainting {
658            // Step 14.1.1 Let headerNames be the result of extracting header list values given
659            // `Access-Control-Expose-Headers` and response’s header list.
660            let header_names: Option<Vec<HeaderName>> = response
661                .headers
662                .typed_get::<AccessControlExposeHeaders>()
663                .map(|v| v.iter().collect());
664            match header_names {
665                // Subsubstep 2.
666                Some(ref list)
667                    if request.credentials_mode != CredentialsMode::Include &&
668                        list.iter().any(|header| header == "*") =>
669                {
670                    response.cors_exposed_header_name_list = response
671                        .headers
672                        .iter()
673                        .map(|(name, _)| name.as_str().to_owned())
674                        .collect();
675                },
676                // Subsubstep 3.
677                Some(list) => {
678                    response.cors_exposed_header_name_list =
679                        list.iter().map(|h| h.as_str().to_owned()).collect();
680                },
681                _ => (),
682            }
683        }
684
685        // Step 14.2 Set response to the following filtered response with response as its internal response,
686        // depending on request’s response tainting:
687        let response_type = match request.response_tainting {
688            ResponseTainting::Basic => ResponseType::Basic,
689            ResponseTainting::CorsTainting => ResponseType::Cors,
690            ResponseTainting::Opaque => ResponseType::Opaque,
691        };
692        response.to_filtered(response_type)
693    } else {
694        response
695    };
696
697    let internal_error = {
698        // Tests for steps 17 and 18, before step 15 for borrowing concerns.
699        let response_is_network_error = response.is_network_error();
700        let should_replace_with_nosniff_error = !response_is_network_error &&
701            should_be_blocked_due_to_nosniff(request.destination, &response.headers);
702        let should_replace_with_mime_type_error = !response_is_network_error &&
703            should_be_blocked_due_to_mime_type(request.destination, &response.headers);
704        let should_replace_with_mixed_content = !response_is_network_error &&
705            should_response_be_blocked_as_mixed_content(request, &response, &context.protocols);
706        let should_replace_with_csp_error = csp_request.is_some_and(|csp_request| {
707            let (check_result, violations) =
708                should_response_be_blocked_by_csp(&csp_request, &response, &policy_container);
709            if !violations.is_empty() {
710                target.process_csp_violations(request, violations);
711            }
712            check_result == csp::CheckResult::Blocked
713        });
714
715        // Step 15.
716        let mut network_error_response = response
717            .get_network_error()
718            .cloned()
719            .map(Response::network_error);
720
721        // Step 15. Let internalResponse be response, if response is a network error;
722        // otherwise response’s internal response.
723        let response_type = response.response_type.clone(); // Needed later after the mutable borrow
724        let internal_response = if let Some(error_response) = network_error_response.as_mut() {
725            error_response
726        } else {
727            response.actual_response_mut()
728        };
729
730        // Step 16. If internalResponse’s URL list is empty, then set it to a clone of request’s URL list.
731        if internal_response.url_list.is_empty() {
732            internal_response.url_list = request
733                .url_list
734                .iter()
735                .map(|locked_url| locked_url.url())
736                .collect();
737        }
738
739        // Step 17. Set internalResponse’s redirect taint to request’s redirect-taint.
740        internal_response.redirect_taint = request.redirect_taint_for_request();
741
742        // Step 19. If response is not a network error and any of the following returns blocked
743        // * should internalResponse to request be blocked as mixed content
744        // * should internalResponse to request be blocked by Content Security Policy
745        // * should internalResponse to request be blocked due to its MIME type
746        // * should internalResponse to request be blocked due to nosniff
747        let mut blocked_error_response;
748
749        let internal_response = if should_replace_with_nosniff_error {
750            // Defer rebinding result
751            blocked_error_response = Response::network_error(NetworkError::Nosniff);
752            &blocked_error_response
753        } else if should_replace_with_mime_type_error {
754            // Defer rebinding result
755            blocked_error_response =
756                Response::network_error(NetworkError::MimeType("Blocked by MIME type".into()));
757            &blocked_error_response
758        } else if should_replace_with_mixed_content {
759            blocked_error_response = Response::network_error(NetworkError::MixedContent);
760            &blocked_error_response
761        } else if should_replace_with_csp_error {
762            blocked_error_response = Response::network_error(NetworkError::ContentSecurityPolicy);
763            &blocked_error_response
764        } else {
765            internal_response
766        };
767
768        // Step 20. If response’s type is "opaque", internalResponse’s status is 206, internalResponse’s
769        // range-requested flag is set, and request’s header list does not contain `Range`, then set
770        // response and internalResponse to a network error.
771        // Also checking if internal response is a network error to prevent crash from attemtping to
772        // read status of a network error if we blocked the request above.
773        let internal_response = if !internal_response.is_network_error() &&
774            response_type == ResponseType::Opaque &&
775            internal_response.status.code() == StatusCode::PARTIAL_CONTENT &&
776            internal_response.range_requested &&
777            !request.headers.contains_key(RANGE)
778        {
779            // Defer rebinding result
780            blocked_error_response =
781                Response::network_error(NetworkError::PartialResponseToNonRangeRequestError);
782            &blocked_error_response
783        } else {
784            internal_response
785        };
786
787        // Step 21. If response is not a network error and either request’s method is `HEAD` or `CONNECT`,
788        // or internalResponse’s status is a null body status, set internalResponse’s body to null and
789        // disregard any enqueuing toward it (if any).
790        // NOTE: We check `internal_response` since we did not mutate `response` in the previous steps.
791        let not_network_error = !response_is_network_error && !internal_response.is_network_error();
792        if not_network_error &&
793            (is_null_body_status(&internal_response.status) ||
794                matches!(request.method, Method::HEAD | Method::CONNECT))
795        {
796            // when Fetch is used only asynchronously, we will need to make sure
797            // that nothing tries to write to the body at this point
798            let mut body = internal_response.body.lock();
799            *body = ResponseBody::Empty;
800        }
801
802        internal_response.get_network_error().cloned()
803    };
804
805    // Execute deferred rebinding of response.
806    let mut response = if let Some(error) = internal_error {
807        Response::network_error(error)
808    } else {
809        response
810    };
811
812    // Step 19. If response is not a network error and any of the following returns blocked
813    let mut response_loaded = false;
814    let mut response = if !response.is_network_error() && !request.integrity_metadata.is_empty() {
815        // Step 19.1.
816        wait_for_response(request, &mut response, target, done_chan, context).await;
817        response_loaded = true;
818
819        // Step 19.2.
820        let integrity_metadata = &request.integrity_metadata;
821        if response.termination_reason.is_none() &&
822            !is_response_integrity_valid(integrity_metadata, &response)
823        {
824            Response::network_error(NetworkError::SubresourceIntegrity)
825        } else {
826            response
827        }
828    } else {
829        response
830    };
831
832    // Step 20.
833    if request.synchronous {
834        // process_response is not supposed to be used
835        // by sync fetch, but we overload it here for simplicity
836        target.process_response(request, &response);
837        if !response_loaded {
838            wait_for_response(request, &mut response, target, done_chan, context).await;
839        }
840        // overloaded similarly to process_response
841        target.process_response_eof(request, &response);
842        return response;
843    }
844
845    // Step 21.
846    if request.body.is_some() && matches!(current_scheme, "http" | "https") {
847        // XXXManishearth: We actually should be calling process_request
848        // in http_network_fetch. However, we can't yet follow the request
849        // upload progress, so I'm keeping it here for now and pretending
850        // the body got sent in one chunk
851        target.process_request_body(request);
852    }
853
854    // Step 22.
855    target.process_response(request, &response);
856    // Send Response to Devtools
857    send_response_to_devtools(request, context, &response, None);
858    send_security_info_to_devtools(request, context, &response);
859
860    // Step 23.
861    if !response_loaded {
862        wait_for_response(request, &mut response, target, done_chan, context).await;
863    }
864
865    // Step 24.
866    target.process_response_eof(request, &response);
867    // Send Response to Devtools
868    // This is done after process_response_eof to ensure that the body is fully
869    // processed before sending the response to Devtools.
870    send_response_to_devtools(request, context, &response, None);
871
872    context
873        .state
874        .http_cache
875        .update_awaiting_consumers(request, &response)
876        .await;
877
878    // Steps 25-27.
879    // TODO: remove this line when only asynchronous fetches are used
880    response
881}
882
883async fn wait_for_response(
884    request: &Request,
885    response: &mut Response,
886    target: Target<'_>,
887    done_chan: &mut DoneChannel,
888    context: &FetchContext,
889) {
890    if let Some(ref mut ch) = *done_chan {
891        let mut devtools_body = context.devtools_chan.as_ref().map(|_| Vec::new());
892        loop {
893            match ch.1.recv().await {
894                Some(Data::Payload(vec)) => {
895                    if let Some(body) = devtools_body.as_mut() {
896                        body.extend(&vec);
897                    }
898                    target.process_response_chunk(request, vec);
899                },
900                Some(Data::Error(network_error)) => {
901                    if network_error == NetworkError::DecompressionError {
902                        response.termination_reason = Some(TerminationReason::Fatal);
903                    }
904                    response.set_network_error(network_error);
905
906                    break;
907                },
908                Some(Data::Done) => {
909                    send_response_to_devtools(request, context, response, devtools_body);
910                    break;
911                },
912                Some(Data::Cancelled) => {
913                    response.aborted.store(true, Ordering::Release);
914                    break;
915                },
916                _ => {
917                    panic!("fetch worker should always send Done before terminating");
918                },
919            }
920        }
921    } else {
922        match *response.actual_response().body.lock() {
923            ResponseBody::Done(ref vec) if !vec.is_empty() => {
924                // in case there was no channel to wait for, the body was
925                // obtained synchronously via scheme_fetch for data/file/about/etc
926                // We should still send the body across as a chunk
927                target.process_response_chunk(request, vec.clone());
928                if context.devtools_chan.is_some() {
929                    // Now that we've replayed the entire cached body,
930                    // notify the DevTools server with the full Response.
931                    send_response_to_devtools(request, context, response, Some(vec.clone()));
932                }
933            },
934            ResponseBody::Done(_) | ResponseBody::Empty => {},
935            _ => unreachable!(),
936        }
937    }
938}
939
940/// Range header start and end values.
941pub enum RangeRequestBounds {
942    /// The range bounds are known and set to final values.
943    Final(RelativePos),
944    /// We need extra information to set the range bounds.
945    /// i.e. buffer or file size.
946    Pending(u64),
947}
948
949impl RangeRequestBounds {
950    pub fn get_final(&self, len: Option<u64>) -> Result<RelativePos, &'static str> {
951        match self {
952            RangeRequestBounds::Final(pos) => {
953                if let Some(len) = len &&
954                    pos.start <= len as i64
955                {
956                    return Ok(*pos);
957                }
958                Err("Tried to process RangeRequestBounds::Final without len")
959            },
960            RangeRequestBounds::Pending(offset) => Ok(RelativePos::from_opts(
961                if let Some(len) = len {
962                    Some((len - u64::min(len, *offset)) as i64)
963                } else {
964                    Some(0)
965                },
966                None,
967            )),
968        }
969    }
970}
971
972fn create_blank_reply(url: ServoUrl, timing_type: ResourceTimingType) -> Response {
973    let mut response = Response::new(url, ResourceFetchTiming::new(timing_type));
974    response
975        .headers
976        .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
977    *response.body.lock() = ResponseBody::Done(vec![]);
978    response.status = HttpStatus::default();
979    response
980}
981
982fn create_about_memory(url: ServoUrl, timing_type: ResourceTimingType) -> Response {
983    let mut response = Response::new(url, ResourceFetchTiming::new(timing_type));
984    response
985        .headers
986        .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
987    *response.body.lock() = ResponseBody::Done(resources::read_bytes(Resource::AboutMemoryHTML));
988    response.status = HttpStatus::default();
989    response
990}
991
992/// Handle a request from the user interface to ignore validation errors for a certificate.
993fn handle_allowcert_request(request: &mut Request, context: &FetchContext) -> io::Result<()> {
994    let error = |string| Err(io::Error::other(string));
995
996    let body = match request.body.as_mut() {
997        Some(body) => body,
998        None => return error("No body found"),
999    };
1000
1001    let stream = body.clone_stream();
1002    let mut stream = stream.lock();
1003    let (body_chan, body_port) = ipc::channel().unwrap();
1004    let Some(chunk_requester) = stream.as_mut() else {
1005        log::error!(
1006            "Could not connect to the request body stream because it has already been closed."
1007        );
1008        return Err(std::io::Error::other("Could not send BodyChunkRequest"));
1009    };
1010    chunk_requester
1011        .send(BodyChunkRequest::Connect(body_chan))
1012        .map_err(|error| {
1013            log::error!(
1014                "Could not connect to the request body stream because it has already been closed: {error}"
1015            );
1016            std::io::Error::other("Could not connect to request body stream")
1017        })?;
1018    chunk_requester
1019        .send(BodyChunkRequest::Chunk)
1020        .map_err(|error| {
1021            log::error!(
1022                "Could not request the first request body chunk because the body stream has already been closed: {error}"
1023            );
1024            std::io::Error::other("Could not request request body chunk")
1025        })?;
1026    let body_bytes = match body_port.recv().ok() {
1027        Some(BodyChunkResponse::Chunk(bytes)) => bytes,
1028        _ => return error("Certificate not sent in a single chunk"),
1029    };
1030
1031    let split_idx = match body_bytes.iter().position(|b| *b == b'&') {
1032        Some(split_idx) => split_idx,
1033        None => return error("Could not find ampersand in data"),
1034    };
1035    let (secret, cert_base64) = body_bytes.split_at(split_idx);
1036
1037    let secret = str::from_utf8(secret).ok().and_then(|s| s.parse().ok());
1038    if secret != Some(*net_traits::PRIVILEGED_SECRET) {
1039        return error("Invalid secret sent. Ignoring request");
1040    }
1041
1042    let cert_bytes = match general_purpose::STANDARD_NO_PAD.decode(&cert_base64[1..]) {
1043        Ok(bytes) => bytes,
1044        Err(_) => return error("Could not decode certificate base64"),
1045    };
1046
1047    context
1048        .state
1049        .override_manager
1050        .add_override(&CertificateDer::from_slice(&cert_bytes).into_owned());
1051    Ok(())
1052}
1053
1054/// [Scheme fetch](https://fetch.spec.whatwg.org#scheme-fetch)
1055async fn scheme_fetch(
1056    fetch_params: &mut FetchParams,
1057    cache: &mut CorsCache,
1058    target: Target<'_>,
1059    done_chan: &mut DoneChannel,
1060    context: &FetchContext,
1061) -> Response {
1062    // Step 1: If fetchParams is canceled, then return the appropriate network error for fetchParams.
1063
1064    // Step 2: Let request be fetchParams’s request.
1065    let request = &mut fetch_params.request;
1066    let url_and_blob_lock = request.current_url_with_blob_claim();
1067
1068    let scheme = url_and_blob_lock.scheme();
1069    match scheme {
1070        "about" if url_and_blob_lock.path() == "blank" => {
1071            create_blank_reply(url_and_blob_lock.url(), request.timing_type())
1072        },
1073        "about" if url_and_blob_lock.path() == "memory" => {
1074            create_about_memory(url_and_blob_lock.url(), request.timing_type())
1075        },
1076
1077        "chrome" if url_and_blob_lock.path() == "allowcert" => {
1078            if let Err(error) = handle_allowcert_request(request, context) {
1079                warn!("Could not handle allowcert request: {error}");
1080            }
1081            create_blank_reply(url_and_blob_lock.url(), request.timing_type())
1082        },
1083
1084        "http" | "https" => {
1085            http_fetch(
1086                fetch_params,
1087                cache,
1088                false,
1089                false,
1090                false,
1091                target,
1092                done_chan,
1093                context,
1094            )
1095            .await
1096        },
1097
1098        _ => match context.protocols.get(scheme) {
1099            Some(handler) => handler.load(request, done_chan, context).await,
1100            None => Response::network_error(NetworkError::UnsupportedScheme),
1101        },
1102    }
1103}
1104
1105fn is_null_body_status(status: &HttpStatus) -> bool {
1106    matches!(
1107        status.try_code(),
1108        Some(StatusCode::SWITCHING_PROTOCOLS) |
1109            Some(StatusCode::NO_CONTENT) |
1110            Some(StatusCode::RESET_CONTENT) |
1111            Some(StatusCode::NOT_MODIFIED)
1112    )
1113}
1114
1115/// <https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-nosniff?>
1116pub fn should_be_blocked_due_to_nosniff(
1117    destination: Destination,
1118    response_headers: &HeaderMap,
1119) -> bool {
1120    // Step 1
1121    if !determine_nosniff(response_headers) {
1122        return false;
1123    }
1124
1125    // Step 2
1126    // Note: an invalid MIME type will produce a `None`.
1127    let mime_type = extract_mime_type_as_mime(response_headers);
1128
1129    /// <https://html.spec.whatwg.org/multipage/#scriptingLanguages>
1130    #[inline]
1131    fn is_javascript_mime_type(mime_type: &Mime) -> bool {
1132        let javascript_mime_types: [Mime; 16] = [
1133            "application/ecmascript".parse().unwrap(),
1134            "application/javascript".parse().unwrap(),
1135            "application/x-ecmascript".parse().unwrap(),
1136            "application/x-javascript".parse().unwrap(),
1137            "text/ecmascript".parse().unwrap(),
1138            "text/javascript".parse().unwrap(),
1139            "text/javascript1.0".parse().unwrap(),
1140            "text/javascript1.1".parse().unwrap(),
1141            "text/javascript1.2".parse().unwrap(),
1142            "text/javascript1.3".parse().unwrap(),
1143            "text/javascript1.4".parse().unwrap(),
1144            "text/javascript1.5".parse().unwrap(),
1145            "text/jscript".parse().unwrap(),
1146            "text/livescript".parse().unwrap(),
1147            "text/x-ecmascript".parse().unwrap(),
1148            "text/x-javascript".parse().unwrap(),
1149        ];
1150
1151        javascript_mime_types
1152            .iter()
1153            .any(|mime| mime.type_() == mime_type.type_() && mime.subtype() == mime_type.subtype())
1154    }
1155
1156    match mime_type {
1157        // Step 4
1158        Some(ref mime_type) if destination.is_script_like() => !is_javascript_mime_type(mime_type),
1159        // Step 5
1160        Some(ref mime_type) if destination == Destination::Style => {
1161            mime_type.type_() != mime::TEXT && mime_type.subtype() != mime::CSS
1162        },
1163
1164        None if destination == Destination::Style || destination.is_script_like() => true,
1165        // Step 6
1166        _ => false,
1167    }
1168}
1169
1170/// <https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-mime-type?>
1171fn should_be_blocked_due_to_mime_type(
1172    destination: Destination,
1173    response_headers: &HeaderMap,
1174) -> bool {
1175    // Step 1: Let mimeType be the result of extracting a MIME type from response’s header list.
1176    let mime_type: mime::Mime = match extract_mime_type_as_mime(response_headers) {
1177        Some(mime_type) => mime_type,
1178        // Step 2: If mimeType is failure, then return allowed.
1179        None => return false,
1180    };
1181
1182    // Step 3: Let destination be request’s destination.
1183    // Step 4: If destination is script-like and one of the following is true, then return blocked:
1184    //    - mimeType’s essence starts with "audio/", "image/", or "video/".
1185    //    - mimeType’s essence is "text/csv".
1186    // Step 5: Return allowed.
1187    destination.is_script_like() &&
1188        match mime_type.type_() {
1189            mime::AUDIO | mime::VIDEO | mime::IMAGE => true,
1190            mime::TEXT if mime_type.subtype() == mime::CSV => true,
1191            _ => false,
1192        }
1193}
1194
1195/// <https://fetch.spec.whatwg.org/#block-bad-port>
1196pub fn should_request_be_blocked_due_to_a_bad_port(url: &ServoUrl) -> bool {
1197    // Step 1. Let url be request’s current URL.
1198    // NOTE: We receive the request url as an argument
1199
1200    // Step 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, then return blocked.
1201    let is_http_scheme = matches!(url.scheme(), "http" | "https");
1202    let is_bad_port = url.port().is_some_and(is_bad_port);
1203    if is_http_scheme && is_bad_port {
1204        return true;
1205    }
1206
1207    // Step 3. Return allowed.
1208    false
1209}
1210
1211/// <https://w3c.github.io/webappsec-mixed-content/#should-block-fetch>
1212pub fn should_request_be_blocked_as_mixed_content(
1213    request: &Request,
1214    protocol_registry: &ProtocolRegistry,
1215) -> bool {
1216    // Step 1. Return allowed if one or more of the following conditions are met:
1217    // 1.1. Does settings prohibit mixed security contexts?
1218    // returns "Does Not Restrict Mixed Security Contexts" when applied to request’s client.
1219    if do_settings_prohibit_mixed_security_contexts(request) ==
1220        MixedSecurityProhibited::NotProhibited
1221    {
1222        return false;
1223    }
1224
1225    // 1.2. request’s URL is a potentially trustworthy URL.
1226    if is_url_potentially_trustworthy(protocol_registry, &request.current_url()) {
1227        return false;
1228    }
1229
1230    // 1.3. The user agent has been instructed to allow mixed content.
1231
1232    // 1.4. request’s destination is "document", and request’s target browsing context has
1233    // no parent browsing context.
1234    if request.destination == Destination::Document {
1235        // TODO: request's target browsing context has no parent browsing context
1236        return false;
1237    }
1238
1239    true
1240}
1241
1242/// <https://w3c.github.io/webappsec-mixed-content/#should-block-response>
1243pub fn should_response_be_blocked_as_mixed_content(
1244    request: &Request,
1245    response: &Response,
1246    protocol_registry: &ProtocolRegistry,
1247) -> bool {
1248    // Step 1. Return allowed if one or more of the following conditions are met:
1249    // 1.1. Does settings prohibit mixed security contexts? returns Does Not Restrict Mixed Content
1250    // when applied to request’s client.
1251    if do_settings_prohibit_mixed_security_contexts(request) ==
1252        MixedSecurityProhibited::NotProhibited
1253    {
1254        return false;
1255    }
1256
1257    // 1.2. response’s url is a potentially trustworthy URL.
1258    if response
1259        .actual_response()
1260        .url()
1261        .is_some_and(|response_url| is_url_potentially_trustworthy(protocol_registry, response_url))
1262    {
1263        return false;
1264    }
1265
1266    // 1.3. TODO: The user agent has been instructed to allow mixed content.
1267
1268    // 1.4. request’s destination is "document", and request’s target browsing context
1269    // has no parent browsing context.
1270    if request.destination == Destination::Document {
1271        // TODO: if requests target browsing context has no parent browsing context
1272        return false;
1273    }
1274
1275    true
1276}
1277
1278/// <https://fetch.spec.whatwg.org/#bad-port>
1279fn is_bad_port(port: u16) -> bool {
1280    static BAD_PORTS: [u16; 78] = [
1281        1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101,
1282        102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389,
1283        427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636,
1284        993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667,
1285        6668, 6669, 6697, 10080,
1286    ];
1287
1288    BAD_PORTS.binary_search(&port).is_ok()
1289}
1290
1291// TODO : Investigate and need to revisit again
1292pub fn is_form_submission_request(request: &Request) -> bool {
1293    let content_type = request.headers.typed_get::<ContentType>();
1294    content_type.is_some_and(|ct| {
1295        let mime: Mime = ct.into();
1296        mime.type_() == mime::APPLICATION && mime.subtype() == mime::WWW_FORM_URLENCODED
1297    })
1298}
1299
1300/// <https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request>
1301fn should_upgrade_request_to_potentially_trustworthy(
1302    request: &mut Request,
1303    context: &FetchContext,
1304) -> bool {
1305    fn should_upgrade_navigation_request(request: &Request) -> bool {
1306        // Step 2.1 If request is a form submission, skip the remaining substeps, and continue upgrading request.
1307        if is_form_submission_request(request) {
1308            return true;
1309        }
1310
1311        // Step 2.2 If request’s client's target browsing context is a nested browsing context,
1312        // skip the remaining substeps and continue upgrading request.
1313        if request
1314            .client
1315            .as_ref()
1316            .is_some_and(|client| client.is_nested_browsing_context)
1317        {
1318            return true;
1319        }
1320
1321        // Step 2.4
1322        // TODO : check for insecure navigation set after its implemention
1323
1324        // Step 2.5 Return without further modifying request
1325        false
1326    }
1327
1328    // Step 1. If request is a navigation request,
1329    if request.is_navigation_request() {
1330        // Append a header named Upgrade-Insecure-Requests with a value of 1 to
1331        // request’s header list if any of the following criteria are met:
1332        // * request’s URL is not a potentially trustworthy URL
1333        // * request’s URL's host is not a preloadable HSTS host
1334        if !is_url_potentially_trustworthy(&context.protocols, &request.current_url()) ||
1335            request
1336                .current_url()
1337                .host_str()
1338                .is_none_or(|host| context.state.hsts_list.read().is_host_secure(host))
1339        {
1340            debug!("Appending the Upgrade-Insecure-Requests header to request’s header list");
1341            request
1342                .headers
1343                .insert("Upgrade-Insecure-Requests", HeaderValue::from_static("1"));
1344        }
1345
1346        if !should_upgrade_navigation_request(request) {
1347            return false;
1348        }
1349    }
1350
1351    // Step 3. Let upgrade state be the result of executing
1352    // §4.2 Should insecure requests be upgraded for client? upon request's client.
1353    // Step 4. If upgrade state is "Do Not Upgrade", return without modifying request.
1354    request
1355        .client
1356        .as_ref()
1357        .is_some_and(|client| client.insecure_requests_policy == InsecureRequestsPolicy::Upgrade)
1358}
1359
1360#[derive(Debug, PartialEq)]
1361pub enum MixedSecurityProhibited {
1362    Prohibited,
1363    NotProhibited,
1364}
1365
1366/// <https://w3c.github.io/webappsec-mixed-content/#categorize-settings-object>
1367fn do_settings_prohibit_mixed_security_contexts(request: &Request) -> MixedSecurityProhibited {
1368    if let Origin::Origin(ref origin) = request.origin {
1369        // Step 1. If settings’ origin is a potentially trustworthy origin,
1370        // then return "Prohibits Mixed Security Contexts".
1371        // NOTE: Workers created from a data: url are secure if they were created from secure contexts
1372        if origin.is_potentially_trustworthy() || origin.is_for_data_worker_from_secure_context() {
1373            return MixedSecurityProhibited::Prohibited;
1374        }
1375    }
1376
1377    // Step 2.2. For each navigable navigable in document’s ancestor navigables:
1378    // Step 2.2.1. If navigable’s active document's origin is a potentially trustworthy origin,
1379    // then return "Prohibits Mixed Security Contexts".
1380    if request.has_trustworthy_ancestor_origin {
1381        return MixedSecurityProhibited::Prohibited;
1382    }
1383
1384    MixedSecurityProhibited::NotProhibited
1385}
1386
1387/// <https://w3c.github.io/webappsec-mixed-content/#upgrade-algorithm>
1388fn should_upgrade_mixed_content_request(
1389    request: &Request,
1390    protocol_registry: &ProtocolRegistry,
1391) -> bool {
1392    let url = request.url();
1393    // Step 1.1 : request’s URL is a potentially trustworthy URL.
1394    if is_url_potentially_trustworthy(protocol_registry, &url) {
1395        return false;
1396    }
1397
1398    // Step 1.2 : request’s URL’s host is an IP address.
1399    match url.host() {
1400        Some(Host::Ipv4(_)) | Some(Host::Ipv6(_)) => return false,
1401        _ => (),
1402    }
1403
1404    // Step 1.3
1405    if do_settings_prohibit_mixed_security_contexts(request) ==
1406        MixedSecurityProhibited::NotProhibited
1407    {
1408        return false;
1409    }
1410
1411    // Step 1.4 : request’s destination is not "image", "audio", or "video".
1412    if !matches!(
1413        request.destination,
1414        Destination::Audio | Destination::Image | Destination::Video
1415    ) {
1416        return false;
1417    }
1418
1419    // Step 1.5 : request’s destination is "image" and request’s initiator is "imageset".
1420    if request.destination == Destination::Image && request.initiator == Initiator::ImageSet {
1421        return false;
1422    }
1423
1424    true
1425}