Skip to main content

net/
http_loader.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::collections::HashSet;
6use std::iter::FromIterator;
7use std::sync::Arc as StdArc;
8use std::time::{Duration, SystemTime};
9
10use async_recursion::async_recursion;
11use content_security_policy::percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
12use devtools_traits::ChromeToDevtoolsControlMsg;
13use embedder_traits::{AuthenticationResponse, GenericEmbedderProxy};
14use futures::{TryFutureExt, TryStreamExt, future};
15use headers::authorization::Basic;
16use headers::{
17    AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowMethods,
18    AccessControlMaxAge, AccessControlRequestMethod, Authorization, CacheControl, ContentLength,
19    HeaderMapExt, IfModifiedSince, LastModified, Pragma, Referer, StrictTransportSecurity,
20    UserAgent,
21};
22use http::header::{
23    self, ACCEPT, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_REQUEST_HEADERS, AUTHORIZATION,
24    CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LOCATION, CONTENT_TYPE, HeaderValue, RANGE,
25    WWW_AUTHENTICATE,
26};
27use http::{HeaderMap, Method, Request as HyperRequest, StatusCode};
28use http_body_util::combinators::BoxBody;
29use http_body_util::{BodyExt, Full};
30use hyper::Response as HyperResponse;
31use hyper::body::{Bytes, Frame};
32use hyper::ext::ReasonPhrase;
33use hyper::header::{HeaderName, TRANSFER_ENCODING};
34use ipc_channel::IpcError;
35use ipc_channel::ipc::{self, IpcSender};
36use ipc_channel::router::ROUTER;
37use log::{debug, error, info, log_enabled, warn};
38use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
39use net_traits::blob_url_store::UrlWithBlobClaim;
40use net_traits::fetch::headers::get_value_from_header_list;
41use net_traits::http_status::HttpStatus;
42use net_traits::policy_container::{EmbedderPolicyValue, RequestPolicyContainer};
43use net_traits::pub_domains::{is_same_site, reg_suffix};
44use net_traits::request::{
45    BodyChunkRequest, BodyChunkResponse, CacheMode, CredentialsMode, Destination, Initiator,
46    Origin, RedirectMode, Referrer, Request, RequestBuilder, RequestClient, RequestMode,
47    ResponseTainting, ServiceWorkersMode, TraversableForUserPrompts, get_cors_unsafe_header_names,
48    is_cors_non_wildcard_request_header_name, is_cors_safelisted_method,
49    is_cors_safelisted_request_header,
50};
51use net_traits::response::{CacheState, RedirectTaint, Response, ResponseBody, ResponseType};
52use net_traits::{
53    CookieSource, DOCUMENT_ACCEPT_HEADER_VALUE, NetworkError, RedirectEndValue, RedirectStartValue,
54    ReferrerPolicy, ResourceAttribute, ResourceFetchTimingContainer, ResourceTimeValue,
55    TlsSecurityInfo, TlsSecurityState,
56};
57use parking_lot::{Mutex, RwLock};
58use profile_traits::mem::{Report, ReportKind};
59use profile_traits::path;
60#[cfg(feature = "tracing")]
61use profile_traits::trace_span;
62use rustc_hash::FxHashMap;
63use servo_base::cross_process_instant::CrossProcessInstant;
64use servo_base::generic_channel::GenericSharedMemory;
65use servo_base::id::{BrowsingContextId, HistoryStateId, PipelineId};
66use servo_url::{ImmutableOrigin, ServoUrl};
67use tokio::sync::mpsc::{
68    Receiver as TokioReceiver, Sender as TokioSender, UnboundedReceiver, UnboundedSender, channel,
69    unbounded_channel,
70};
71use tokio_stream::wrappers::ReceiverStream;
72#[cfg(feature = "tracing")]
73use tracing::Instrument;
74
75use crate::async_runtime::spawn_task;
76use crate::connector::{
77    CertificateErrorOverrideManager, ServoClient, TlsHandshakeInfo, create_tls_config,
78};
79use crate::cookie::ServoCookie;
80use crate::cookie_storage::CookieStorage;
81use crate::decoder::Decoder;
82use crate::devtools::{
83    prepare_devtools_request, send_request_to_devtools, send_response_values_to_devtools,
84};
85use crate::embedder::NetToEmbedderMsg;
86use crate::fetch::cors_cache::CorsCache;
87use crate::fetch::fetch_params::FetchParams;
88use crate::fetch::headers::{SecFetchDest, SecFetchMode, SecFetchSite, SecFetchUser};
89use crate::fetch::methods::{Data, DoneChannel, FetchContext, Target, main_fetch};
90use crate::hsts::HstsList;
91use crate::http_cache::{
92    CacheKey, CachedResourcesOrGuard, HttpCache, construct_response, invalidate_cached_resources,
93    refresh,
94};
95use crate::resource_thread::{AuthCache, AuthCacheEntry};
96use crate::websocket_loader::start_websocket;
97
98/// The various states an entry of the HttpCache can be in.
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub enum HttpCacheEntryState {
101    /// The entry is fully up-to-date,
102    /// there are no pending concurrent stores,
103    /// and it is ready to construct cached responses.
104    ReadyToConstruct,
105    /// The entry is pending a number of concurrent stores.
106    PendingStore(usize),
107}
108
109pub struct HttpState {
110    pub hsts_list: RwLock<HstsList>,
111    pub cookie_jar: RwLock<CookieStorage>,
112    pub http_cache: HttpCache,
113    pub auth_cache: RwLock<AuthCache>,
114    pub history_states: RwLock<FxHashMap<HistoryStateId, Vec<u8>>>,
115    pub client: ServoClient,
116    pub override_manager: CertificateErrorOverrideManager,
117    pub embedder_proxy: GenericEmbedderProxy<NetToEmbedderMsg>,
118}
119
120impl HttpState {
121    pub(crate) fn memory_reports(&self, suffix: &str, ops: &mut MallocSizeOfOps) -> Vec<Report> {
122        vec![
123            Report {
124                path: path!["memory-cache", suffix],
125                kind: ReportKind::ExplicitJemallocHeapSize,
126                size: self.http_cache.size_of(ops),
127            },
128            Report {
129                path: path!["hsts-list", suffix],
130                kind: ReportKind::ExplicitJemallocHeapSize,
131                size: self.hsts_list.read().size_of(ops),
132            },
133        ]
134    }
135
136    async fn request_authentication(
137        &self,
138        request: &Request,
139        response: &Response,
140    ) -> Option<AuthenticationResponse> {
141        // We do not make an authentication request for non-WebView associated HTTP requests.
142        let webview_id = request.target_webview_id?;
143        let for_proxy = response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED;
144
145        // If this is not actually a navigation request return None.
146        if request.mode != RequestMode::Navigate {
147            return None;
148        }
149
150        let (sender, receiver) = tokio::sync::oneshot::channel();
151        self.embedder_proxy
152            .send(NetToEmbedderMsg::RequestAuthentication(
153                webview_id,
154                request.url(),
155                for_proxy,
156                sender,
157            ));
158        receiver.await.ok()?
159    }
160}
161
162/// Step 11 of <https://fetch.spec.whatwg.org/#concept-fetch>.
163pub(crate) fn set_default_accept(request: &mut Request) {
164    // Step 11. If request’s header list does not contain `Accept`, then:
165    if request.headers.contains_key(header::ACCEPT) {
166        return;
167    }
168
169    // Step 11.2. If request’s initiator is "prefetch", then set value to the document `Accept` header value.
170    let value = if request.initiator == Initiator::Prefetch {
171        DOCUMENT_ACCEPT_HEADER_VALUE
172    } else {
173        // Step 11.3. Otherwise, the user agent should set value to the first matching statement,
174        // if any, switching on request’s destination:
175        match request.destination {
176            Destination::Document | Destination::Frame | Destination::IFrame => {
177                DOCUMENT_ACCEPT_HEADER_VALUE
178            },
179            Destination::Image => {
180                HeaderValue::from_static("image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5")
181            },
182            Destination::Json => HeaderValue::from_static("application/json,*/*;q=0.5"),
183            Destination::Style => HeaderValue::from_static("text/css,*/*;q=0.1"),
184            // Step 11.1. Let value be `*/*`.
185            _ => HeaderValue::from_static("*/*"),
186        }
187    };
188
189    // Step 11.4. Append (`Accept`, value) to request’s header list.
190    request.headers.insert(header::ACCEPT, value);
191}
192
193fn set_default_accept_encoding(headers: &mut HeaderMap) {
194    if headers.contains_key(header::ACCEPT_ENCODING) {
195        return;
196    }
197
198    // TODO(eijebong): Change this once typed headers are done
199    headers.insert(
200        header::ACCEPT_ENCODING,
201        HeaderValue::from_static("gzip, deflate, br, zstd"),
202    );
203}
204
205/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-state-no-referrer-when-downgrade>
206fn no_referrer_when_downgrade(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
207    // Step 1
208    if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
209        return None;
210    }
211    // Step 2
212    strip_url_for_use_as_referrer(referrer_url, false)
213}
214
215/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-strict-origin>
216fn strict_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
217    // Step 1
218    if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
219        return None;
220    }
221    // Step 2
222    strip_url_for_use_as_referrer(referrer_url, true)
223}
224
225/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-strict-origin-when-cross-origin>
226fn strict_origin_when_cross_origin(
227    referrer_url: ServoUrl,
228    current_url: ServoUrl,
229) -> Option<ServoUrl> {
230    // Step 1
231    if referrer_url.origin() == current_url.origin() {
232        return strip_url_for_use_as_referrer(referrer_url, false);
233    }
234    // Step 2
235    if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
236        return None;
237    }
238    // Step 3
239    strip_url_for_use_as_referrer(referrer_url, true)
240}
241
242/// <https://html.spec.whatwg.org/multipage/#schemelessly-same-site>
243fn is_schemelessy_same_site(site_a: &ImmutableOrigin, site_b: &ImmutableOrigin) -> bool {
244    // Step 1
245    if !site_a.is_tuple() && !site_b.is_tuple() && site_a == site_b {
246        true
247    } else if site_a.is_tuple() && site_b.is_tuple() {
248        // Step 2.1
249        let host_a = site_a.host().map(|h| h.to_string()).unwrap_or_default();
250        let host_b = site_b.host().map(|h| h.to_string()).unwrap_or_default();
251
252        let host_a_reg = reg_suffix(&host_a);
253        let host_b_reg = reg_suffix(&host_b);
254
255        // Step 2.2-2.3
256        (site_a.host() == site_b.host() && host_a_reg.is_empty()) ||
257            (host_a_reg == host_b_reg && !host_a_reg.is_empty())
258    } else {
259        // Step 3
260        false
261    }
262}
263
264/// <https://w3c.github.io/webappsec-referrer-policy/#strip-url>
265fn strip_url_for_use_as_referrer(mut url: ServoUrl, origin_only: bool) -> Option<ServoUrl> {
266    const MAX_REFERRER_URL_LENGTH: usize = 4096;
267    // Step 2
268    if url.is_local_scheme() {
269        return None;
270    }
271    // Step 3-6
272    {
273        let url = url.as_mut_url();
274        let _ = url.set_username("");
275        let _ = url.set_password(None);
276        url.set_fragment(None);
277        // Note: The result of serializing referrer url should not be
278        // greater than 4096 as specified in Step 6 of
279        // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
280        if origin_only || url.as_str().len() > MAX_REFERRER_URL_LENGTH {
281            url.set_path("");
282            url.set_query(None);
283        }
284    }
285    // Step 7
286    Some(url)
287}
288
289/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-same-origin>
290fn same_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
291    // Step 1
292    if referrer_url.origin() == current_url.origin() {
293        return strip_url_for_use_as_referrer(referrer_url, false);
294    }
295    // Step 2
296    None
297}
298
299/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-origin-when-cross-origin>
300fn origin_when_cross_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
301    // Step 1
302    if referrer_url.origin() == current_url.origin() {
303        return strip_url_for_use_as_referrer(referrer_url, false);
304    }
305    // Step 2
306    strip_url_for_use_as_referrer(referrer_url, true)
307}
308
309/// <https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer>
310pub fn determine_requests_referrer(
311    referrer_policy: ReferrerPolicy,
312    referrer_source: ServoUrl,
313    current_url: ServoUrl,
314) -> Option<ServoUrl> {
315    match referrer_policy {
316        ReferrerPolicy::EmptyString | ReferrerPolicy::NoReferrer => None,
317        ReferrerPolicy::Origin => strip_url_for_use_as_referrer(referrer_source, true),
318        ReferrerPolicy::UnsafeUrl => strip_url_for_use_as_referrer(referrer_source, false),
319        ReferrerPolicy::StrictOrigin => strict_origin(referrer_source, current_url),
320        ReferrerPolicy::StrictOriginWhenCrossOrigin => {
321            strict_origin_when_cross_origin(referrer_source, current_url)
322        },
323        ReferrerPolicy::SameOrigin => same_origin(referrer_source, current_url),
324        ReferrerPolicy::OriginWhenCrossOrigin => {
325            origin_when_cross_origin(referrer_source, current_url)
326        },
327        ReferrerPolicy::NoReferrerWhenDowngrade => {
328            no_referrer_when_downgrade(referrer_source, current_url)
329        },
330    }
331}
332
333fn set_request_cookies(
334    url: &ServoUrl,
335    headers: &mut HeaderMap,
336    cookie_jar: &RwLock<CookieStorage>,
337) {
338    let mut cookie_jar = cookie_jar.write();
339    cookie_jar.remove_expired_cookies_for_url(url);
340    if let Some(cookie_list) = cookie_jar.cookies_for_url(url, CookieSource::HTTP) {
341        headers.insert(
342            header::COOKIE,
343            HeaderValue::from_bytes(cookie_list.as_bytes()).unwrap(),
344        );
345    }
346}
347
348fn set_cookie_for_url(cookie_jar: &RwLock<CookieStorage>, request: &ServoUrl, cookie_val: &str) {
349    let mut cookie_jar = cookie_jar.write();
350    let source = CookieSource::HTTP;
351
352    if let Some(cookie) = ServoCookie::from_cookie_string(cookie_val, request, source) {
353        cookie_jar.push(cookie, request, source);
354    }
355}
356
357fn set_cookies_from_headers(
358    url: &ServoUrl,
359    headers: &HeaderMap,
360    cookie_jar: &RwLock<CookieStorage>,
361) {
362    for cookie in headers.get_all(header::SET_COOKIE) {
363        let cookie_bytes = cookie.as_bytes();
364        if !ServoCookie::is_valid_name_or_value(cookie_bytes) {
365            continue;
366        }
367        if let Ok(cookie_str) = std::str::from_utf8(cookie_bytes) {
368            set_cookie_for_url(cookie_jar, url, cookie_str);
369        }
370    }
371}
372
373fn build_tls_security_info(handshake: &TlsHandshakeInfo, hsts_enabled: bool) -> TlsSecurityInfo {
374    // Simplified security state determination:
375    // Servo uses rustls, which only supports TLS 1.2+ and secure cipher suites (GCM, ChaCha20-Poly1305).
376    // rustls does NOT support TLS 1.0, TLS 1.1, SSL, or weak ciphers (RC4, 3DES, CBC, etc).
377    // Therefore, any successful TLS connection is secure by design.
378    //
379    // We only check for missing handshake information as a defensive measure.
380
381    let state = if handshake.protocol_version.is_none() || handshake.cipher_suite.is_none() {
382        // Missing handshake information indicates an incomplete or failed connection
383        TlsSecurityState::Insecure
384    } else {
385        // rustls guarantees TLS 1.2+ with secure ciphers
386        TlsSecurityState::Secure
387    };
388
389    TlsSecurityInfo {
390        state,
391        weakness_reasons: Vec::new(), // rustls never negotiates weak crypto
392        protocol_version: handshake.protocol_version.clone(),
393        cipher_suite: handshake.cipher_suite.clone(),
394        kea_group_name: handshake.kea_group_name.clone(),
395        signature_scheme_name: handshake.signature_scheme_name.clone(),
396        alpn_protocol: handshake.alpn_protocol.clone(),
397        certificate_chain_der: handshake.certificate_chain_der.clone(),
398        certificate_transparency: None,
399        hsts: hsts_enabled,
400        hpkp: false,
401        used_ech: handshake.used_ech,
402        used_delegated_credentials: false,
403        used_ocsp: false,
404        used_private_dns: false,
405    }
406}
407
408fn auth_from_cache(
409    auth_cache: &RwLock<AuthCache>,
410    origin: &ImmutableOrigin,
411) -> Option<Authorization<Basic>> {
412    if let Some(auth_entry) = auth_cache.read().entries.get(&origin.ascii_serialization()) {
413        let user_name = &auth_entry.user_name;
414        let password = &auth_entry.password;
415        Some(Authorization::basic(user_name, password))
416    } else {
417        None
418    }
419}
420
421/// Messages from the IPC route to the fetch worker,
422/// used to fill the body with bytes coming-in over IPC.
423enum BodyChunk {
424    /// A chunk of bytes.
425    Chunk(GenericSharedMemory),
426    /// Body is done.
427    Done,
428}
429
430/// The stream side of the body passed to hyper.
431enum BodyStream {
432    /// A receiver that can be used in Body::wrap_stream,
433    /// for streaming the request over the network.
434    Chunked(TokioReceiver<Result<Frame<Bytes>, hyper::Error>>),
435    /// A body whose bytes are buffered
436    /// and sent in one chunk over the network.
437    Buffered(UnboundedReceiver<BodyChunk>),
438}
439
440/// The sink side of the body passed to hyper,
441/// used to enqueue chunks.
442enum BodySink {
443    /// A Tokio sender used to feed chunks to the network stream.
444    Chunked(TokioSender<Result<Frame<Bytes>, hyper::Error>>),
445    /// A Crossbeam sender used to send chunks to the fetch worker,
446    /// where they will be buffered
447    /// in order to ensure they are not streamed them over the network.
448    Buffered(UnboundedSender<BodyChunk>),
449}
450
451impl BodySink {
452    fn transmit_bytes(&self, bytes: GenericSharedMemory) {
453        match self {
454            BodySink::Chunked(sender) => {
455                let sender = sender.clone();
456                spawn_task(async move {
457                    let _ = sender
458                        .send(Ok(Frame::data(Bytes::copy_from_slice(&bytes))))
459                        .await;
460                });
461            },
462            BodySink::Buffered(sender) => {
463                let _ = sender.send(BodyChunk::Chunk(bytes));
464            },
465        }
466    }
467
468    fn close(&self) {
469        match self {
470            BodySink::Chunked(_) => { /* no need to close sender */ },
471            BodySink::Buffered(sender) => {
472                let _ = sender.send(BodyChunk::Done);
473            },
474        }
475    }
476}
477
478fn request_body_stream_closed_error(action: &str) -> NetworkError {
479    NetworkError::Crash(format!(
480        "Request body stream has already been closed while trying to {action}."
481    ))
482}
483
484fn log_request_body_stream_closed(action: &str, error: Option<&IpcError>) {
485    match error {
486        Some(error) => {
487            error!("Request body stream has already been closed while trying to {action}: {error}")
488        },
489        None => error!("Request body stream has already been closed while trying to {action}."),
490    }
491}
492
493fn log_fetch_terminated_send_failure(terminated_with_error: bool, context: &str) {
494    warn!(
495        "Failed to notify request-body stream termination state ({terminated_with_error}) while {context} because the receiver was already dropped."
496    );
497}
498
499const FRAGMENT: &AsciiSet = &CONTROLS.add(b'|').add(b'{').add(b'}');
500
501#[allow(clippy::too_many_arguments)]
502#[servo_tracing::instrument(skip_all, fields(url=url.as_str()))]
503/// This sets up the callback infrastructure to send body frames to `body_sender` and fires the client request.
504async fn obtain_response(
505    client: &ServoClient,
506    url: &ServoUrl,
507    method: &Method,
508    request_headers: &mut HeaderMap,
509    body_sender: Option<StdArc<Mutex<Option<IpcSender<BodyChunkRequest>>>>>,
510    source_is_null: bool,
511    pipeline_id: &Option<PipelineId>,
512    request_id: Option<&str>,
513    destination: Destination,
514    is_xhr: bool,
515    context: &FetchContext,
516    fetch_terminated: UnboundedSender<bool>,
517    browsing_context_id: Option<BrowsingContextId>,
518) -> Result<(HyperResponse<Decoder>, Option<ChromeToDevtoolsControlMsg>), NetworkError> {
519    let mut headers = request_headers.clone();
520
521    let devtools_bytes = StdArc::new(Mutex::new(vec![]));
522
523    // https://url.spec.whatwg.org/#percent-encoded-bytes
524    let encoded_url = utf8_percent_encode(url.as_str(), FRAGMENT).to_string();
525
526    let request = if let Some(chunk_requester) = body_sender {
527        let (sink, stream) = if source_is_null {
528            // Step 4.2 of https://fetch.spec.whatwg.org/#concept-http-network-fetch
529            // TODO: this should not be set for HTTP/2(currently not supported?).
530            headers.insert(TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
531
532            let (sender, receiver) = channel(1);
533            (BodySink::Chunked(sender), BodyStream::Chunked(receiver))
534        } else {
535            // Note: Hyper seems to already buffer bytes when the request appears not stream-able,
536            // see https://github.com/hyperium/hyper/issues/2232#issuecomment-644322104
537            //
538            // However since this doesn't appear documented, and we're using an ancient version,
539            // for now we buffer manually to ensure we don't stream requests
540            // to servers that might not know how to handle them.
541            let (sender, receiver) = unbounded_channel();
542            (BodySink::Buffered(sender), BodyStream::Buffered(receiver))
543        };
544
545        obtain_response_setup_router_callback(
546            devtools_bytes.clone(),
547            chunk_requester,
548            sink,
549            fetch_terminated,
550        )?;
551
552        let body = match stream {
553            BodyStream::Chunked(receiver) => {
554                let stream = ReceiverStream::new(receiver);
555                BoxBody::new(http_body_util::StreamBody::new(stream))
556            },
557            BodyStream::Buffered(mut receiver) => {
558                // Accumulate bytes received over IPC into a vector.
559                let mut body = vec![];
560                loop {
561                    match receiver.recv().await {
562                        Some(BodyChunk::Chunk(bytes)) => {
563                            body.extend_from_slice(&bytes);
564                        },
565                        Some(BodyChunk::Done) => break,
566                        None => warn!("Failed to read all chunks from request body."),
567                    }
568                }
569                Full::new(body.into()).map_err(|_| unreachable!()).boxed()
570            },
571        };
572        HyperRequest::builder()
573            .method(method)
574            .uri(encoded_url)
575            .body(body)
576    } else {
577        HyperRequest::builder()
578            .method(method)
579            .uri(encoded_url)
580            .body(
581                http_body_util::Empty::new()
582                    .map_err(|_| unreachable!())
583                    .boxed(),
584            )
585    };
586
587    // TODO(#21261) connect_start: set if a persistent connection is *not* used and the last non-redirected
588    // fetch passes the timing allow check
589    let connect_start = CrossProcessInstant::now();
590    context.timing.set_attributes(&[
591        ResourceAttribute::DomainLookupStart,
592        ResourceAttribute::ConnectStart(connect_start),
593    ]);
594
595    // TODO: We currently don't know when the handhhake before the connection is done
596    // so our best bet would be to set `secure_connection_start` here when we are currently
597    // fetching on a HTTPS url.
598    if url.scheme() == "https" {
599        context
600            .timing
601            .set_attribute(ResourceAttribute::SecureConnectionStart);
602    }
603
604    let mut request = match request {
605        Ok(request) => request,
606        Err(error) => return Err(NetworkError::HttpError(error.to_string())),
607    };
608    *request.headers_mut() = headers.clone();
609
610    let connect_end = CrossProcessInstant::now();
611    context
612        .timing
613        .set_attribute(ResourceAttribute::ConnectEnd(connect_end));
614
615    let request_id = request_id.map(|v| v.to_owned());
616    let pipeline_id = *pipeline_id;
617    let closure_url = url.clone();
618    let method = method.clone();
619    let send_start = CrossProcessInstant::now();
620
621    let host = request.uri().host().unwrap_or("").to_owned();
622    let override_manager = context.state.override_manager.clone();
623    let headers = headers.clone();
624    let is_secure_scheme = url.is_secure_scheme();
625
626    // Generally, we use a persistent connection, so we will also set other PerformanceResourceTiming
627    //   attributes to this as well (domain_lookup_start, domain_lookup_end, connect_start, connect_end,
628    //   secure_connection_start)
629    context
630        .timing
631        .set_attribute(ResourceAttribute::RequestStart);
632
633    let client_future = client
634        .request(request)
635        .and_then(move |res| {
636            let send_end = CrossProcessInstant::now();
637
638            // TODO(#21271) response_start: immediately after receiving first byte of response
639
640            let msg = if let Some(request_id) = request_id {
641                if let Some(pipeline_id) = pipeline_id {
642                    if let Some(browsing_context_id) = browsing_context_id {
643                        Some(prepare_devtools_request(
644                            request_id,
645                            closure_url,
646                            method.clone(),
647                            headers,
648                            Some(devtools_bytes.lock().clone()),
649                            pipeline_id,
650                            (connect_end - connect_start).unsigned_abs(),
651                            (send_end - send_start).unsigned_abs(),
652                            destination,
653                            is_xhr,
654                            browsing_context_id,
655                        ))
656                    } else {
657                        debug!("Not notifying devtools (no browsing_context_id)");
658                        None
659                    }
660                    // TODO: ^This is not right, connect_start is taken before contructing the
661                    // request and connect_end at the end of it. send_start is takend before the
662                    // connection too. I'm not sure it's currently possible to get the time at the
663                    // point between the connection and the start of a request.
664                } else {
665                    debug!("Not notifying devtools (no pipeline_id)");
666                    None
667                }
668            } else {
669                debug!("Not notifying devtools (no request_id)");
670                None
671            };
672
673            future::ready(Ok((
674                Decoder::detect(res.map(|r| r.boxed()), is_secure_scheme),
675                msg,
676            )))
677        })
678        .map_err(move |error| {
679            warn!("network error: {error:?}");
680            NetworkError::from_hyper_error(
681                &error,
682                override_manager.remove_certificate_failing_verification(host.as_str()),
683            )
684        });
685
686    #[cfg(feature = "tracing")]
687    {
688        client_future.instrument(trace_span!("HyperRequest")).await
689    }
690
691    #[cfg(not(feature = "tracing"))]
692    {
693        client_future.await
694    }
695}
696
697/// Setup the callback mechanism to forward chunks from the request received to the `chunk_requester`.
698fn obtain_response_setup_router_callback(
699    devtools_bytes: StdArc<Mutex<Vec<u8>>>,
700    chunk_requester: StdArc<Mutex<Option<IpcSender<BodyChunkRequest>>>>,
701    sink: BodySink,
702    fetch_terminated: UnboundedSender<bool>,
703) -> Result<(), NetworkError> {
704    let (body_chan, body_port) = ipc::channel().unwrap();
705
706    {
707        let mut lock = chunk_requester.lock();
708        if let Some(chunk_requester) = lock.as_mut() {
709            if let Err(error) = chunk_requester.send(BodyChunkRequest::Connect(body_chan)) {
710                log_request_body_stream_closed("connect to the request body stream", Some(&error));
711                return Err(request_body_stream_closed_error(
712                    "connect to the request body stream",
713                ));
714            }
715
716            // https://fetch.spec.whatwg.org/#concept-request-transmit-body
717            // Request the first chunk, corresponding to Step 3 and 4.
718            if let Err(error) = chunk_requester.send(BodyChunkRequest::Chunk) {
719                log_request_body_stream_closed(
720                    "request the first request body chunk",
721                    Some(&error),
722                );
723                return Err(request_body_stream_closed_error(
724                    "request the first request body chunk",
725                ));
726            }
727        } else {
728            log_request_body_stream_closed("connect to the request body stream", None);
729            return Err(request_body_stream_closed_error(
730                "connect to the request body stream",
731            ));
732        }
733    }
734
735    ROUTER.add_typed_route(
736        body_port,
737        Box::new(move |message| {
738            info!("Received message");
739            let bytes = match message.unwrap() {
740                BodyChunkResponse::Chunk(bytes) => bytes,
741                BodyChunkResponse::Done => {
742                    // Step 3, abort these parallel steps.
743                    if fetch_terminated.send(false).is_err() {
744                        log_fetch_terminated_send_failure(
745                            false,
746                            "handling request body completion",
747                        );
748                    }
749                    sink.close();
750
751                    return;
752                },
753                BodyChunkResponse::Error => {
754                    // Step 4 and/or 5.
755                    // TODO: differentiate between the two steps,
756                    // where step 5 requires setting an `aborted` flag on the fetch.
757                    if fetch_terminated.send(true).is_err() {
758                        log_fetch_terminated_send_failure(
759                            true,
760                            "handling request body stream error",
761                        );
762                    }
763                    sink.close();
764
765                    return;
766                },
767            };
768
769            devtools_bytes.lock().extend_from_slice(&bytes);
770
771            // Step 5.1.2.2, transmit chunk over the network,
772            // currently implemented by sending the bytes to the fetch worker.
773            sink.transmit_bytes(bytes);
774
775            // Step 5.1.2.3
776            // Request the next chunk.
777            let mut chunk_requester = chunk_requester.lock();
778            if let Some(chunk_requester) = chunk_requester.as_mut() {
779                if let Err(error) = chunk_requester.send(BodyChunkRequest::Chunk) {
780                    log_request_body_stream_closed(
781                        "request the next request body chunk",
782                        Some(&error),
783                    );
784                    if fetch_terminated.send(true).is_err() {
785                        log_fetch_terminated_send_failure(
786                            true,
787                            "handling failure to request the next request body chunk",
788                        );
789                    }
790                    sink.close();
791                }
792            } else {
793                log_request_body_stream_closed("request the next request body chunk", None);
794                if fetch_terminated.send(true).is_err() {
795                    log_fetch_terminated_send_failure(
796                        true,
797                        "handling a closed request body stream while requesting the next chunk",
798                    );
799                }
800                sink.close();
801            }
802        }),
803    );
804
805    Ok(())
806}
807
808/// [HTTP fetch](https://fetch.spec.whatwg.org/#concept-http-fetch)
809#[async_recursion]
810#[allow(clippy::too_many_arguments)]
811pub(crate) async fn http_fetch(
812    fetch_params: &mut FetchParams,
813    cache: &mut CorsCache,
814    cors_flag: bool,
815    cors_preflight_flag: bool,
816    authentication_fetch_flag: bool,
817    target: Target<'async_recursion>,
818    done_chan: &mut DoneChannel,
819    context: &FetchContext,
820) -> Response {
821    // This is a new async fetch, reset the channel we are waiting on
822    *done_chan = None;
823    // Step 1. Let request be fetchParams’s request.
824    let request = &mut fetch_params.request;
825
826    // Step 2. Let response and internalResponse be null.
827    let mut response: Option<Response> = None;
828
829    // Step 3. If request’s service-workers mode is "all", then
830    if request.service_workers_mode == ServiceWorkersMode::All {
831        // TODO: Substep 1
832        // Set response to the result of invoking handle fetch for request.
833
834        // Substep 2
835        if let Some(ref res) = response {
836            // Subsubstep 1
837            // TODO: transmit body for request
838
839            // Subsubstep 2
840            // nothing to do, since actual_response is a function on response
841
842            // Subsubstep 3
843            if (res.response_type == ResponseType::Opaque && request.mode != RequestMode::NoCors) ||
844                (res.response_type == ResponseType::OpaqueRedirect &&
845                    request.redirect_mode != RedirectMode::Manual) ||
846                (res.url_list.len() > 1 && request.redirect_mode != RedirectMode::Follow) ||
847                res.is_network_error()
848            {
849                return Response::network_error(NetworkError::ConnectionFailure);
850            }
851
852            // Subsubstep 4
853            // TODO: set response's CSP list on actual_response
854        }
855    }
856
857    // Step 4. If response is null, then:
858    if response.is_none() {
859        // Step 4.1. If makeCORSPreflight is true and one of these conditions is true:
860        if cors_preflight_flag {
861            let method_cache_match = cache.match_method(request, request.method.clone());
862
863            // There is no method cache entry match for request’s method using request, and either
864            // request’s method is not a CORS-safelisted method or request’s use-CORS-preflight flag
865            // is set.
866            let method_mismatch = !method_cache_match &&
867                (!is_cors_safelisted_method(&request.method) || request.use_cors_preflight);
868
869            // There is at least one item in the CORS-unsafe request-header names with request’s
870            // header list for which there is no header-name cache entry match using request.
871            let header_mismatch = request.headers.iter().any(|(name, value)| {
872                !cache.match_header(request, name) &&
873                    !is_cors_safelisted_request_header(&name, &value)
874            });
875
876            // Then:
877            if method_mismatch || header_mismatch {
878                // Step 4.1.1. Let preflightResponse be the result of running
879                // CORS-preflight fetch given request.
880                let preflight_response = cors_preflight_fetch(request, cache, context).await;
881                // Step 4.1.2. If preflightResponse is a network error, then return preflightResponse.
882                if let Some(error) = preflight_response.get_network_error() {
883                    return Response::network_error(error.clone());
884                }
885            }
886        }
887
888        // Step 4.2. If request’s redirect mode is "follow",
889        // then set request’s service-workers mode to "none".
890        if request.redirect_mode == RedirectMode::Follow {
891            request.service_workers_mode = ServiceWorkersMode::None;
892        }
893
894        // Step 4.3. Set response and internalResponse to the result of
895        // running HTTP-network-or-cache fetch given fetchParams.
896        let mut fetch_result = http_network_or_cache_fetch(
897            fetch_params,
898            authentication_fetch_flag,
899            cors_flag,
900            done_chan,
901            context,
902        )
903        .await;
904
905        // Step 4.4. If request’s response tainting is "cors" and a CORS check for request
906        // and response returns failure, then return a network error.
907        if cors_flag && cors_check(&fetch_params.request, &fetch_result).is_err() {
908            return Response::network_error(NetworkError::CorsGeneral);
909        }
910
911        // Step 4.5. If the TAO check for request and response returns failure,
912        // then set request’s timing allow failed flag.
913        if let Err(()) = tao_check(&fetch_params.request, &fetch_result) {
914            context.timing.inner().mark_timing_check_failed();
915        }
916        fetch_result.return_internal = false;
917        response = Some(fetch_result);
918    }
919
920    let request = &mut fetch_params.request;
921
922    // response is guaranteed to be something by now
923    let mut response = response.unwrap();
924
925    // Step 5: If either request’s response tainting or response’s type is "opaque",
926    // and the cross-origin resource policy check with request’s origin, request’s client,
927    // request’s destination, and internalResponse returns blocked, then return a network error.
928    if (request.response_tainting == ResponseTainting::Opaque ||
929        response.response_type == ResponseType::Opaque) &&
930        request.client.as_ref().is_some_and(|client| {
931            cross_origin_resource_policy_check(
932                &request.origin,
933                client,
934                &response,
935                ForNavigation::No,
936            ) == CrossOriginResourcePolicy::Blocked
937        })
938    {
939        return Response::network_error(NetworkError::CrossOriginResponse);
940    }
941
942    // Step 6. If internalResponse’s status is a redirect status:
943    if response
944        .actual_response()
945        .status
946        .try_code()
947        .is_some_and(is_redirect_status)
948    {
949        // TODO Step 6.1. If request is a navigation request, then append to a request’s navigation
950        // timing allow values list given request and internalResponse.
951
952        // Step 6.2. If internalResponse’s status is not 303, request’s body is non-null,
953        // and the connection uses HTTP/2, then user agents may, and are even encouraged to,
954        // transmit an RST_STREAM frame.
955        if response.actual_response().status != StatusCode::SEE_OTHER {
956            // TODO: send RST_STREAM frame
957        }
958
959        // Step 6.3. Switch on request’s redirect mode:
960        response = match request.redirect_mode {
961            // Step 6.3."error".1. Set response to a network error.
962            RedirectMode::Error => Response::network_error(NetworkError::RedirectError),
963            RedirectMode::Manual => {
964                // Step 6.3."manual".1. If request’s mode is "navigate", then set fetchParams’s controller’s
965                // next manual redirect steps to run HTTP-redirect fetch given fetchParams and response.
966                if request.mode == RequestMode::Navigate {
967                    // TODO: We don't implement Fetch controller. Instead, we update the location url
968                    // of the response here and don't call `http_redirect_fetch`. That's get called later.
969                    // Once we have a fetch controller here, we should update the code as specced.
970                    let location_url =
971                        location_url_for_response(&response, request.current_url().fragment());
972                    response.actual_response_mut().location_url = location_url;
973                    response
974                } else {
975                    // Step 6.3."manual".2. Otherwise, set response to an opaque-redirect filtered
976                    // response whose internal response is internalResponse.
977                    response.to_filtered(ResponseType::OpaqueRedirect)
978                }
979            },
980            RedirectMode::Follow => {
981                // TODO Step 6.3."follow".1. Run the WebDriver BiDi response completed steps with
982                // request and response.
983                // set back to default
984                response.return_internal = true;
985
986                // Step 6.3."follow".2. Set response to the result of running HTTP-redirect fetch
987                // given fetchParams and response.
988                http_redirect_fetch(
989                    fetch_params,
990                    cache,
991                    response,
992                    cors_flag,
993                    target,
994                    done_chan,
995                    context,
996                )
997                .await
998            },
999        };
1000    }
1001
1002    // set back to default
1003    response.return_internal = true;
1004    context
1005        .timing
1006        .set_attribute(ResourceAttribute::RedirectCount(
1007            fetch_params.request.redirect_count as u16,
1008        ));
1009
1010    response.resource_timing = context.timing.clone();
1011
1012    // Step 7. Return response
1013    response
1014}
1015
1016/// <https://fetch.spec.whatwg.org/#concept-tao-check>
1017fn tao_check(request: &Request, response: &Response) -> Result<(), ()> {
1018    // Step 1. Assert: request’s origin is not "client".
1019    let Origin::Origin(ref request_origin) = request.origin else {
1020        unreachable!("origin cannot be \"client\" at this point");
1021    };
1022
1023    // Step 2. If request’s timing allow failed flag is set, then return failure.
1024
1025    // Step 3. Let values be the result of getting, decoding, and splitting `Timing-Allow-Origin`
1026    // from response’s header list.
1027    let values: Vec<&str> = response
1028        .headers
1029        .get_all("Timing-Allow-Origin")
1030        .iter()
1031        .map(|header_value| header_value.to_str().unwrap_or(""))
1032        .collect();
1033
1034    // Step 4. If values contains "*", then return success.
1035    if values.contains(&"*") {
1036        return Ok(());
1037    }
1038
1039    // Step 5. If values contains the result of serializing a request origin with request, then
1040    // return success.
1041    if values
1042        .iter()
1043        .any(|header_str| *header_str == request_origin.ascii_serialization())
1044    {
1045        return Ok(());
1046    }
1047
1048    // Step 6. If request’s mode is "navigate" and request’s current URL’s origin is not same origin
1049    // with request’s origin, then return failure.
1050    if request.mode == RequestMode::Navigate && request.current_url().origin() != *request_origin {
1051        return Err(());
1052    }
1053
1054    // Step 7. If request’s response tainting is "basic", then return success.
1055    if request.response_tainting == ResponseTainting::Basic {
1056        return Ok(());
1057    }
1058
1059    // Step 8. Return failure.
1060    Err(())
1061}
1062
1063// Convenience struct that implements Drop, for setting redirectEnd on function return
1064struct RedirectEndTimer(Option<ResourceFetchTimingContainer>);
1065
1066impl RedirectEndTimer {
1067    fn neuter(&mut self) {
1068        self.0 = None;
1069    }
1070}
1071
1072impl Drop for RedirectEndTimer {
1073    fn drop(&mut self) {
1074        let RedirectEndTimer(resource_fetch_timing_opt) = self;
1075
1076        resource_fetch_timing_opt.as_ref().map_or((), |t| {
1077            t.set_attribute(ResourceAttribute::RedirectEnd(RedirectEndValue::Zero));
1078        })
1079    }
1080}
1081
1082/// <https://fetch.spec.whatwg.org/#request-body-header-name>
1083static REQUEST_BODY_HEADER_NAMES: &[HeaderName] = &[
1084    CONTENT_ENCODING,
1085    CONTENT_LANGUAGE,
1086    CONTENT_LOCATION,
1087    CONTENT_TYPE,
1088];
1089
1090/// <https://fetch.spec.whatwg.org/#concept-response-location-url>
1091fn location_url_for_response(
1092    response: &Response,
1093    request_fragment: Option<&str>,
1094) -> Option<Result<ServoUrl, String>> {
1095    // Step 1. If response’s status is not a redirect status, then return null.
1096    assert!(
1097        response
1098            .actual_response()
1099            .status
1100            .try_code()
1101            .is_some_and(is_redirect_status)
1102    );
1103    // Step 2. Let location be the result of extracting header list values given `Location` and response’s header list.
1104    let mut location = response
1105        .actual_response()
1106        .headers
1107        .get(header::LOCATION)
1108        .and_then(|header_value| {
1109            HeaderValue::to_str(header_value)
1110                .map(|location_string| {
1111                    // Step 3. If location is a header value, then set location to the result of parsing location with response’s URL.
1112                    ServoUrl::parse_with_base(response.actual_response().url(), location_string)
1113                        .map_err(|error| error.to_string())
1114                })
1115                .ok()
1116        });
1117
1118    // Step 4. If location is a URL whose fragment is null, then set location’s fragment to requestFragment.
1119    if let Some(Ok(ref mut location)) = location &&
1120        location.fragment().is_none()
1121    {
1122        location.set_fragment(request_fragment);
1123    }
1124    // Step 5. Return location.
1125    location
1126}
1127
1128/// [HTTP redirect fetch](https://fetch.spec.whatwg.org/#http-redirect-fetch)
1129#[async_recursion]
1130pub async fn http_redirect_fetch(
1131    fetch_params: &mut FetchParams,
1132    cache: &mut CorsCache,
1133    mut response: Response,
1134    cors_flag: bool,
1135    target: Target<'async_recursion>,
1136    done_chan: &mut DoneChannel,
1137    context: &FetchContext,
1138) -> Response {
1139    let mut redirect_end_timer = RedirectEndTimer(Some(context.timing.clone()));
1140
1141    // Step 1. Let request be fetchParams’s request.
1142    let request = &mut fetch_params.request;
1143
1144    // Step 2. Let internalResponse be response, if response is not a filtered response; otherwise response’s internal response.
1145    assert!(response.return_internal);
1146
1147    // Step 3. Let locationURL be internalResponse’s location URL given request’s current URL’s fragment.
1148    let location_url = location_url_for_response(&response, request.current_url().fragment());
1149    response.actual_response_mut().location_url = location_url.clone();
1150
1151    let location_url = match location_url {
1152        // Step 4. If locationURL is null, then return response.
1153        None => return response,
1154        // Step 5. If locationURL is failure, then return a network error.
1155        Some(Err(err)) => {
1156            return Response::network_error(NetworkError::ResourceLoadError(
1157                "Location URL parse failure: ".to_owned() + &err,
1158            ));
1159        },
1160        // Step 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network error.
1161        Some(Ok(url)) if !matches!(url.scheme(), "http" | "https") => {
1162            return Response::network_error(NetworkError::UnsupportedScheme);
1163        },
1164        Some(Ok(url)) => url,
1165    };
1166
1167    // Step 1 of https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-fetchstart
1168    // TODO: check origin and timing allow check
1169    // start_time should equal redirect_start if nonzero; else fetch_start
1170    // updates start_time only if redirect_start is nonzero (implying TAO)
1171    context.timing.set_attributes(&[
1172        ResourceAttribute::RedirectStart(RedirectStartValue::FetchStart),
1173        ResourceAttribute::FetchStart,
1174        ResourceAttribute::StartTime(ResourceTimeValue::FetchStart),
1175        ResourceAttribute::StartTime(ResourceTimeValue::RedirectStart),
1176    ]);
1177
1178    // Step 7: If request’s redirect count is 20, then return a network error.
1179    if request.redirect_count >= 20 {
1180        return Response::network_error(NetworkError::TooManyRedirects);
1181    }
1182
1183    // Step 8: Increase request’s redirect count by 1.
1184    request.redirect_count += 1;
1185
1186    // Step 9. If request’s mode is "cors", locationURL includes credentials,
1187    // and request’s origin is not same origin with locationURL’s origin, then return a network error.
1188    let same_origin = match request.origin {
1189        Origin::Origin(ref origin) => *origin == location_url.origin(),
1190        Origin::Client => panic!(
1191            "Request origin should not be client for {}",
1192            request.current_url()
1193        ),
1194    };
1195
1196    let has_credentials = has_credentials(&location_url);
1197
1198    if request.mode == RequestMode::CorsMode && !same_origin && has_credentials {
1199        return Response::network_error(NetworkError::CorsCredentials);
1200    }
1201
1202    if cors_flag && location_url.origin() != request.current_url().origin() {
1203        request.origin = Origin::Origin(ImmutableOrigin::new_opaque());
1204    }
1205
1206    // Step 10. If request’s response tainting is "cors" and locationURL includes credentials, then return a network error.
1207    if cors_flag && has_credentials {
1208        return Response::network_error(NetworkError::CorsCredentials);
1209    }
1210
1211    // Step 11: If internalResponse’s status is not 303, request’s body is non-null, and request’s
1212    // body’s source is null, then return a network error.
1213    if response.actual_response().status != StatusCode::SEE_OTHER &&
1214        request.body.as_ref().is_some_and(|b| b.source_is_null())
1215    {
1216        return Response::network_error(NetworkError::ConnectionFailure);
1217    }
1218
1219    // Step 12. If one of the following is true
1220    if response
1221        .actual_response()
1222        .status
1223        .try_code()
1224        .is_some_and(|code| {
1225            // internalResponse’s status is 301 or 302 and request’s method is `POST`
1226            ((code == StatusCode::MOVED_PERMANENTLY || code == StatusCode::FOUND) &&
1227                request.method == Method::POST) ||
1228                // internalResponse’s status is 303 and request’s method is not `GET` or `HEAD`
1229                (code == StatusCode::SEE_OTHER &&
1230                    request.method != Method::HEAD &&
1231                    request.method != Method::GET)
1232        })
1233    {
1234        // Step 12.1. Set request’s method to `GET` and request’s body to null.
1235        request.method = Method::GET;
1236        request.body = None;
1237        // Step 12.2. For each headerName of request-body-header name, delete headerName from request’s header list.
1238        for name in REQUEST_BODY_HEADER_NAMES {
1239            request.headers.remove(name);
1240        }
1241    }
1242
1243    // Step 13: If request’s current URL’s origin is not same origin with locationURL’s origin, then
1244    // for each headerName of CORS non-wildcard request-header name, delete headerName from
1245    // request’s header list.
1246    if location_url.origin() != request.current_url().origin() {
1247        // This list currently only contains the AUTHORIZATION header
1248        // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
1249        request.headers.remove(AUTHORIZATION);
1250    }
1251
1252    // Step 14: If request’s body is non-null, then set request’s body to the body of the result of
1253    // safely extracting request’s body’s source.
1254    if let Some(body) = request.body.as_mut() {
1255        body.extract_source();
1256    }
1257
1258    // Steps 15-17 relate to timing, which is not implemented 1:1 with the spec.
1259
1260    // Step 18: Append locationURL to request’s URL list.
1261    request
1262        .url_list
1263        .push(UrlWithBlobClaim::from_url_without_having_claimed_blob(
1264            location_url,
1265        ));
1266
1267    // Step 19: Invoke set request’s referrer policy on redirect on request and internalResponse.
1268    set_requests_referrer_policy_on_redirect(request, response.actual_response());
1269
1270    // Step 20: Let recursive be true.
1271    // Step 21: If request’s redirect mode is "manual", then...
1272    let recursive_flag = request.redirect_mode != RedirectMode::Manual;
1273
1274    // Step 22: Return the result of running main fetch given fetchParams and recursive.
1275    let fetch_response = main_fetch(
1276        fetch_params,
1277        cache,
1278        recursive_flag,
1279        target,
1280        done_chan,
1281        context,
1282    )
1283    .await;
1284
1285    // TODO: timing allow check
1286    context.timing.set_attribute(ResourceAttribute::RedirectEnd(
1287        RedirectEndValue::ResponseEnd,
1288    ));
1289    redirect_end_timer.neuter();
1290
1291    fetch_response
1292}
1293
1294/// [HTTP network or cache fetch](https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch)
1295#[async_recursion]
1296#[servo_tracing::instrument(skip_all,fields(url=fetch_params.request.url().as_str()))]
1297async fn http_network_or_cache_fetch(
1298    fetch_params: &mut FetchParams,
1299    authentication_fetch_flag: bool,
1300    cors_flag: bool,
1301    done_chan: &mut DoneChannel,
1302    context: &FetchContext,
1303) -> Response {
1304    // Step 2. Let httpFetchParams be null.
1305    let http_fetch_params: &mut FetchParams;
1306    let mut fetch_params_copy: FetchParams;
1307
1308    // Step 3. Let httpRequest be null. (See step 8 for initialization)
1309
1310    // Step 4. Let response be null.
1311    let mut response: Option<Response> = None;
1312
1313    // Step 7. Let the revalidatingFlag be unset.
1314    let mut revalidating_flag = false;
1315
1316    // TODO(#33616): Step 8. Run these steps, but abort when fetchParams is canceled:
1317    // Step 8.1. If request’s traversable for user prompts is "no-traversable"
1318    // and request’s redirect mode is "error", then set httpFetchParams to fetchParams and httpRequest to request.
1319    let http_request = if fetch_params.request.traversable_for_user_prompts ==
1320        TraversableForUserPrompts::NoTraversable &&
1321        fetch_params.request.redirect_mode == RedirectMode::Error
1322    {
1323        http_fetch_params = fetch_params;
1324        &mut http_fetch_params.request
1325    }
1326    // Step 8.2 Otherwise:
1327    else {
1328        // Step 8.2.1 - 8.2.3: Set httpRequest to a clone of request
1329        // and Set httpFetchParams to a copy of fetchParams.
1330        fetch_params_copy =
1331            std::mem::replace(fetch_params, FetchParams::new(fetch_params.request.clone()));
1332        http_fetch_params = &mut fetch_params_copy;
1333
1334        &mut http_fetch_params.request
1335    };
1336
1337    // Step 8.3: Let includeCredentials be true if one of:
1338    let include_credentials = match http_request.credentials_mode {
1339        // request’s credentials mode is "include"
1340        CredentialsMode::Include => true,
1341        // request’s credentials mode is "same-origin" and request’s response tainting is "basic"
1342        CredentialsMode::CredentialsSameOrigin
1343            if http_request.response_tainting == ResponseTainting::Basic =>
1344        {
1345            true
1346        },
1347        _ => false,
1348    };
1349
1350    // Step 8.4: If Cross-Origin-Embedder-Policy allows credentials with request returns false, then
1351    // set includeCredentials to false.
1352    // TODO(#33616): Requires request's client object
1353
1354    // Step 8.5 Let contentLength be httpRequest’s body’s length, if httpRequest’s body is non-null;
1355    // otherwise null.
1356    let content_length = http_request
1357        .body
1358        .as_ref()
1359        .and_then(|body| body.len().map(|size| size as u64));
1360
1361    // Step 8.6 Let contentLengthHeaderValue be null.
1362    let mut content_length_header_value = None;
1363
1364    // Step 8.7 If httpRequest’s body is null and httpRequest’s method is `POST` or `PUT`,
1365    // then set contentLengthHeaderValue to `0`.
1366    if http_request.body.is_none() && matches!(http_request.method, Method::POST | Method::PUT) {
1367        content_length_header_value = Some(0);
1368    }
1369
1370    // Step 8.8 If contentLength is non-null, then set contentLengthHeaderValue to contentLength,
1371    // serialized and isomorphic encoded.
1372    // NOTE: The header will later be serialized using HeaderMap::typed_insert
1373    if let Some(content_length) = content_length {
1374        content_length_header_value = Some(content_length);
1375    };
1376
1377    // Step 8.9 If contentLengthHeaderValue is non-null, then append (`Content-Length`, contentLengthHeaderValue)
1378    // to httpRequest’s header list.
1379    if let Some(content_length_header_value) = content_length_header_value {
1380        http_request
1381            .headers
1382            .typed_insert(ContentLength(content_length_header_value));
1383    }
1384
1385    // Step 8.10 If contentLength is non-null and httpRequest’s keepalive is true, then:
1386    if http_request.keep_alive &&
1387        let Some(content_length) = content_length
1388    {
1389        // Step 8.10.1. Let inflightKeepaliveBytes be 0.
1390        // Step 8.10.2. Let group be httpRequest’s client’s fetch group.
1391        // Step 8.10.3. Let inflightRecords be the set of fetch records
1392        // in group whose request’s keepalive is true and done flag is unset.
1393        let in_flight_keep_alive_bytes: u64 = context
1394            .in_flight_keep_alive_records
1395            .lock()
1396            .get(
1397                &http_request
1398                    .pipeline_id
1399                    .expect("Must always set a pipeline ID for keep-alive requests"),
1400            )
1401            .map(|records| {
1402                // Step 8.10.4. For each fetchRecord of inflightRecords:
1403                // Step 8.10.4.1. Let inflightRequest be fetchRecord’s request.
1404                // Step 8.10.4.2. Increment inflightKeepaliveBytes by inflightRequest’s body’s length.
1405                records
1406                    .iter()
1407                    .map(|record| {
1408                        if record.request_id == http_request.id {
1409                            // Don't double count for this request. We have already added it in
1410                            // `fetch::methods::fetch_with_cors_cache`
1411                            0
1412                        } else {
1413                            record.keep_alive_body_length
1414                        }
1415                    })
1416                    .sum()
1417            })
1418            .unwrap_or_default();
1419        // Step 8.10.5. If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error.
1420        if content_length + in_flight_keep_alive_bytes > 64 * 1024 {
1421            return Response::network_error(NetworkError::TooManyInFlightKeepAliveRequests);
1422        }
1423    }
1424
1425    // Step 8.11: If httpRequest’s referrer is a URL, then:
1426    match http_request.referrer {
1427        Referrer::ReferrerUrl(ref http_request_referrer) |
1428        Referrer::Client(ref http_request_referrer) => {
1429            // Step 8.11.1: Let referrerValue be httpRequest’s referrer, serialized and isomorphic
1430            // encoded.
1431            if let Ok(referer) = http_request_referrer.as_str().parse::<Referer>() {
1432                // Step 8.11.2: Append (`Referer`, referrerValue) to httpRequest’s header list.
1433                http_request.headers.typed_insert(referer);
1434            } else {
1435                // This error should only happen in cases where hyper and rust-url disagree
1436                // about how to parse a referer.
1437                // https://github.com/servo/servo/issues/24175
1438                error!("Failed to parse {} as referrer", http_request_referrer);
1439            }
1440        },
1441        _ => {},
1442    };
1443
1444    // Step 8.12 Append a request `Origin` header for httpRequest.
1445    append_a_request_origin_header(http_request);
1446
1447    // Step 8.13 Append the Fetch metadata headers for httpRequest.
1448    append_the_fetch_metadata_headers(http_request);
1449
1450    // Step 8.14: If httpRequest’s initiator is "prefetch", then set a structured field value given
1451    // (`Sec-Purpose`, the token "prefetch") in httpRequest’s header list.
1452    if http_request.initiator == Initiator::Prefetch &&
1453        let Ok(value) = HeaderValue::from_str("prefetch")
1454    {
1455        http_request.headers.insert("Sec-Purpose", value);
1456    }
1457
1458    // Step 8.15: If httpRequest’s header list does not contain `User-Agent`, then user agents
1459    // should append (`User-Agent`, default `User-Agent` value) to httpRequest’s header list.
1460    if !http_request.headers.contains_key(header::USER_AGENT) {
1461        http_request
1462            .headers
1463            .typed_insert::<UserAgent>(context.user_agent.parse().unwrap());
1464    }
1465
1466    // Steps 8.16 to 8.18
1467    append_cache_data_to_headers(http_request);
1468
1469    // Step 8.19: If httpRequest’s header list contains `Range`, then append (`Accept-Encoding`,
1470    // `identity`) to httpRequest’s header list.
1471    if http_request.headers.contains_key(header::RANGE) &&
1472        let Ok(value) = HeaderValue::from_str("identity")
1473    {
1474        http_request.headers.insert("Accept-Encoding", value);
1475    }
1476
1477    // Step 8.20: Modify httpRequest’s header list per HTTP. Do not append a given header if
1478    // httpRequest’s header list contains that header’s name.
1479    // `Accept`, `Accept-Charset`, and `Accept-Language` must not be included at this point.
1480    http_request.headers.remove(header::HOST);
1481    // unlike http_loader, we should not set the accept header here
1482    set_default_accept_encoding(&mut http_request.headers);
1483
1484    let current_url = http_request.current_url();
1485
1486    // Step 8.21: If includeCredentials is true, then:
1487    // TODO some of this step can't be implemented yet
1488    if include_credentials {
1489        // Substep 1
1490        // TODO http://mxr.mozilla.org/servo/source/components/net/http_loader.rs#504
1491        // XXXManishearth http_loader has block_cookies: support content blocking here too
1492        set_request_cookies(
1493            &current_url,
1494            &mut http_request.headers,
1495            &context.state.cookie_jar,
1496        );
1497        // Substep 2
1498        if !http_request.headers.contains_key(header::AUTHORIZATION) {
1499            // Substep 3
1500            let mut authorization_value = None;
1501
1502            // Substep 4
1503            if let Some(basic) = auth_from_cache(&context.state.auth_cache, &current_url.origin()) &&
1504                (!http_request.use_url_credentials || !has_credentials(&current_url))
1505            {
1506                authorization_value = Some(basic);
1507            }
1508
1509            // Substep 5
1510            if authentication_fetch_flag &&
1511                authorization_value.is_none() &&
1512                has_credentials(&current_url)
1513            {
1514                authorization_value = Some(Authorization::basic(
1515                    current_url.username(),
1516                    current_url.password().unwrap_or(""),
1517                ));
1518            }
1519
1520            // Substep 6
1521            if let Some(basic) = authorization_value {
1522                http_request.headers.typed_insert(basic);
1523            }
1524        }
1525    }
1526
1527    // TODO(#33616) Step 8.22 If there’s a proxy-authentication entry, use it as appropriate.
1528    let should_wait = {
1529        // Enter critical section on cache entry.
1530        let mut cache_guard = block_for_cache_ready(
1531            context,
1532            http_request,
1533            done_chan,
1534            &mut revalidating_flag,
1535            &mut response,
1536        )
1537        .await;
1538
1539        // TODO(#33616): Step 9. If aborted, then return the appropriate network error for fetchParams.
1540
1541        // Step 10. If response is null, then:
1542        if response.is_none() {
1543            // Step 10.1 If httpRequest’s cache mode is "only-if-cached", then return a network error.
1544            if http_request.cache_mode == CacheMode::OnlyIfCached {
1545                // Exit critical section of cache entry.
1546                return Response::network_error(NetworkError::CacheError);
1547            }
1548
1549            // Step 10.2 Let forwardResponse be the result of running HTTP-network fetch given httpFetchParams,
1550            // includeCredentials, and isNewConnectionFetch.
1551            drop(cache_guard);
1552            let forward_response =
1553                http_network_fetch(http_fetch_params, include_credentials, done_chan, context)
1554                    .await;
1555
1556            let http_request = &mut http_fetch_params.request;
1557            let request_key = CacheKey::new(http_request);
1558            cache_guard = context
1559                .state
1560                .http_cache
1561                .get_or_guard(request_key.clone())
1562                .await;
1563            // Step 10.3 If httpRequest’s method is unsafe and forwardResponse’s status is in the range 200 to 399,
1564            // inclusive, invalidate appropriate stored responses in httpCache, as per the
1565            // "Invalidating Stored Responses" chapter of HTTP Caching, and set storedResponse to null.
1566            if forward_response.status.in_range(200..=399) && !http_request.method.is_safe() {
1567                if let Some(guard) = cache_guard.try_as_mut() {
1568                    invalidate_cached_resources(guard);
1569                }
1570                context
1571                    .state
1572                    .http_cache
1573                    .invalidate_related_urls(http_request, &forward_response, &request_key)
1574                    .await;
1575            }
1576
1577            // Step 10.4 If the revalidatingFlag is set and forwardResponse’s status is 304, then:
1578            if revalidating_flag && forward_response.status == StatusCode::NOT_MODIFIED {
1579                // Ensure done_chan is None,
1580                // since the network response will be replaced by the revalidated stored one.
1581                *done_chan = None;
1582                if let Some(guard) = cache_guard.try_as_mut() {
1583                    response = refresh(http_request, forward_response.clone(), done_chan, guard);
1584                }
1585
1586                if let Some(response) = &mut response {
1587                    response.cache_state = CacheState::Validated;
1588                }
1589            }
1590
1591            // Step 10.5 If response is null, then:
1592            if response.is_none() {
1593                // Step 10.5.1 Set response to forwardResponse.
1594                let forward_response = response.insert(forward_response);
1595
1596                // Per https://httpwg.org/specs/rfc9111.html#response.cacheability we must not cache responses
1597                // if the No-Store directive is present
1598                if http_request.cache_mode != CacheMode::NoStore {
1599                    // Step 10.5.2 Store httpRequest and forwardResponse in httpCache, as per the
1600                    //             "Storing Responses in Caches" chapter of HTTP Caching.
1601                    cache_guard.insert(http_request, forward_response);
1602                }
1603            }
1604            false
1605        } else {
1606            true
1607        }
1608    }; // Exit Critical Section on cache entry
1609
1610    if should_wait {
1611        // If the cache constructed a response, and that is still receiving from the network,
1612        // we must wait for it to finish in case it is still receiving from the network.
1613        // Note: this means only the fetch from which the original network response originated
1614        // will be able to stream it; all others receive a cached response in one chunk.
1615        wait_for_inflight_requests(done_chan, &mut response).await;
1616    }
1617
1618    let http_request = &mut http_fetch_params.request;
1619    let mut response = response.unwrap();
1620
1621    // Step 11. Set response’s URL list to a clone of httpRequest’s URL list.
1622    response.url_list = http_request
1623        .url_list
1624        .iter()
1625        .map(|claimed_url| claimed_url.url())
1626        .collect();
1627
1628    // Step 12. If httpRequest’s header list contains `Range`, then set response’s range-requested flag.
1629    if http_request.headers.contains_key(RANGE) {
1630        response.range_requested = true;
1631    }
1632
1633    // Step 13. Set response’s request-includes-credentials to includeCredentials.
1634    response.request_includes_credentials = include_credentials;
1635
1636    // Step 14. If response’s status is 401, httpRequest’s response tainting is not "cors",
1637    // includeCredentials is true, and request’s window is an environment settings object, then:
1638    // TODO(#33616): Figure out what to do with request window objects
1639    // NOTE: Requiring a WWW-Authenticate header here is ad-hoc, but seems to match what other browsers are
1640    // doing. See Step 14.1.
1641    if response.status.try_code() == Some(StatusCode::UNAUTHORIZED) &&
1642        !cors_flag &&
1643        include_credentials &&
1644        response.headers.contains_key(WWW_AUTHENTICATE)
1645    {
1646        // TODO: Step 14.1 Spec says requires testing on multiple WWW-Authenticate headers
1647
1648        let request = &mut fetch_params.request;
1649
1650        // Step 14.2 If request’s body is non-null, then:
1651        if request.body.is_some() {
1652            // TODO Implement body source
1653        }
1654
1655        // Step 14.3 If request’s use-URL-credentials flag is unset or isAuthenticationFetch is true, then:
1656        if !request.use_url_credentials || authentication_fetch_flag {
1657            let Some(credentials) = context
1658                .state
1659                .request_authentication(request, &response)
1660                .await
1661            else {
1662                return response;
1663            };
1664
1665            if let Err(err) = request
1666                .current_url_mut()
1667                .set_username(&credentials.username)
1668            {
1669                error!("error setting username for url: {:?}", err);
1670                return response;
1671            };
1672
1673            if let Err(err) = request
1674                .current_url_mut()
1675                .set_password(Some(&credentials.password))
1676            {
1677                error!("error setting password for url: {:?}", err);
1678                return response;
1679            };
1680        }
1681
1682        // Make sure this is set to None,
1683        // since we're about to start a new `http_network_or_cache_fetch`.
1684        *done_chan = None;
1685
1686        // Step 14.4 Set response to the result of running HTTP-network-or-cache fetch given fetchParams and true.
1687        response = http_network_or_cache_fetch(
1688            fetch_params,
1689            true, /* authentication flag */
1690            cors_flag,
1691            done_chan,
1692            context,
1693        )
1694        .await;
1695    }
1696
1697    // Step 15. If response’s status is 407, then:
1698    if response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED {
1699        let request = &mut fetch_params.request;
1700        // Step 15.1 If request’s traversable for user prompts is "no-traversable", then return a network error.
1701
1702        if request.traversable_for_user_prompts == TraversableForUserPrompts::NoTraversable {
1703            return Response::network_error(NetworkError::ResourceLoadError(
1704                "Can't find Window object".into(),
1705            ));
1706        }
1707
1708        // (Step 15.2 does not exist, requires testing on Proxy-Authenticate headers)
1709
1710        // TODO(#33616): Step 15.3 If fetchParams is canceled, then return
1711        // the appropriate network error for fetchParams.
1712
1713        // Step 15.4 Prompt the end user as appropriate in request’s window
1714        // window and store the result as a proxy-authentication entry.
1715        let Some(credentials) = context
1716            .state
1717            .request_authentication(request, &response)
1718            .await
1719        else {
1720            return response;
1721        };
1722
1723        // Store the credentials as a proxy-authentication entry.
1724        let entry = AuthCacheEntry {
1725            user_name: credentials.username,
1726            password: credentials.password,
1727        };
1728        {
1729            let mut auth_cache = context.state.auth_cache.write();
1730            let key = request.current_url().origin().ascii_serialization();
1731            auth_cache.entries.insert(key, entry);
1732        }
1733
1734        // Make sure this is set to None,
1735        // since we're about to start a new `http_network_or_cache_fetch`.
1736        *done_chan = None;
1737
1738        // Step 15.5 Set response to the result of running HTTP-network-or-cache fetch given fetchParams.
1739        response = http_network_or_cache_fetch(
1740            fetch_params,
1741            false, /* authentication flag */
1742            cors_flag,
1743            done_chan,
1744            context,
1745        )
1746        .await;
1747    }
1748
1749    // TODO(#33616): Step 16. If all of the following are true:
1750    // * response’s status is 421
1751    // * isNewConnectionFetch is false
1752    // * request’s body is null, or request’s body is non-null and request’s body’s source is non-null
1753    // then: [..]
1754
1755    // Step 17. If isAuthenticationFetch is true, then create an authentication entry for request and the given realm.
1756    if authentication_fetch_flag {
1757        // TODO(#33616)
1758    }
1759
1760    // Step 18. Return response.
1761    response
1762}
1763
1764/// If the cache is not ready to construct a response, wait.
1765///
1766/// The cache is not ready if a previous fetch checked the cache, found nothing,
1767/// and moved on to a network fetch, and hasn't updated the cache yet with a pending resource.
1768///
1769/// Note that this is a different workflow from the one involving `wait_for_cached_response`.
1770/// That one happens when a fetch gets a cache hit, and the resource is pending completion from the network.
1771#[servo_tracing::instrument(skip_all)]
1772async fn block_for_cache_ready<'a>(
1773    context: &'a FetchContext,
1774    http_request: &mut Request,
1775    done_chan: &mut DoneChannel,
1776    revalidating_flag: &mut bool,
1777    response: &mut Option<Response>,
1778) -> CachedResourcesOrGuard<'a> {
1779    let entry_key = CacheKey::new(http_request);
1780    let guard_result = context.state.http_cache.get_or_guard(entry_key).await;
1781
1782    match guard_result {
1783        CachedResourcesOrGuard::Guard(_) => {
1784            *done_chan = None;
1785        },
1786        CachedResourcesOrGuard::Value(ref cached_resources) => {
1787            // TODO(#33616): Step 8.23 Set httpCache to the result of determining the
1788            // HTTP cache partition, given httpRequest.
1789            // Step 8.25.1 Set storedResponse to the result of selecting a response from the httpCache,
1790            //              possibly needing validation, as per the "Constructing Responses from Caches"
1791            //              chapter of HTTP Caching, if any.
1792            let stored_response = construct_response(http_request, done_chan, cached_resources);
1793            // Step 8.25.2 If storedResponse is non-null, then:
1794            if let Some(response_from_cache) = stored_response {
1795                let response_headers = response_from_cache.response.headers.clone();
1796                // Substep 1, 2, 3, 4
1797                let (cached_response, needs_revalidation) =
1798                    match (http_request.cache_mode, &http_request.mode) {
1799                        (CacheMode::ForceCache, _) => (Some(response_from_cache.response), false),
1800                        (CacheMode::OnlyIfCached, &RequestMode::SameOrigin) => {
1801                            (Some(response_from_cache.response), false)
1802                        },
1803                        (CacheMode::OnlyIfCached, _) |
1804                        (CacheMode::NoStore, _) |
1805                        (CacheMode::Reload, _) => (None, false),
1806                        (_, _) => (
1807                            Some(response_from_cache.response),
1808                            response_from_cache.needs_validation,
1809                        ),
1810                    };
1811
1812                if needs_revalidation {
1813                    *revalidating_flag = true;
1814                    // Substep 5
1815                    if let Some(http_date) = response_headers.typed_get::<LastModified>() {
1816                        let http_date: SystemTime = http_date.into();
1817                        http_request
1818                            .headers
1819                            .typed_insert(IfModifiedSince::from(http_date));
1820                    }
1821                    if let Some(entity_tag) = response_headers.get(header::ETAG) {
1822                        http_request
1823                            .headers
1824                            .insert(header::IF_NONE_MATCH, entity_tag.clone());
1825                    }
1826                } else {
1827                    // Substep 6
1828                    *response = cached_response;
1829                    if let Some(response) = response {
1830                        response.cache_state = CacheState::Local;
1831                    }
1832                }
1833                if response.is_none() {
1834                    // Ensure the done chan is not set if we're not using the cached response,
1835                    // as the cache might have set it to Some if it constructed a pending response.
1836                    *done_chan = None;
1837                }
1838            }
1839        },
1840    }
1841    guard_result
1842}
1843
1844/// Wait for a cached response from channel.
1845/// Happens when a fetch gets a cache hit, and the resource is pending completion from the network.
1846async fn wait_for_inflight_requests(done_chan: &mut DoneChannel, response: &mut Option<Response>) {
1847    if let Some(ref mut ch) = *done_chan {
1848        // The cache constructed a response with a body of ResponseBody::Receiving.
1849        // We wait for the response in the cache to "finish",
1850        // with a body of either Done or Cancelled.
1851        assert!(response.is_some());
1852
1853        loop {
1854            match ch.1.recv().await {
1855                Some(Data::Payload(_)) => {},
1856                Some(Data::Done) => break, // Return the full response as if it was initially cached as such.
1857                Some(Data::Cancelled) => {
1858                    // The response was cancelled while the fetch was ongoing.
1859                    break;
1860                },
1861                _ => panic!("HTTP cache should always send Done or Cancelled"),
1862            }
1863        }
1864    }
1865    // Set done_chan back to None, it's cache-related usefulness ends here.
1866    *done_chan = None;
1867}
1868
1869/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check>
1870///
1871/// This is obtained from [cross_origin_resource_policy_check]
1872#[derive(PartialEq)]
1873enum CrossOriginResourcePolicy {
1874    Allowed,
1875    Blocked,
1876}
1877
1878enum ForNavigation {
1879    #[expect(dead_code)]
1880    Yes,
1881    No,
1882}
1883
1884/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check>
1885fn cross_origin_resource_policy_check(
1886    origin: &Origin,
1887    request_client: &RequestClient,
1888    response: &Response,
1889    for_navigation: ForNavigation,
1890) -> CrossOriginResourcePolicy {
1891    // Step 1. Set forNavigation to false if it is not given.
1892    //
1893    // That's the default value of the enum
1894
1895    // Step 2. Let embedderPolicy be settingsObject’s policy container’s embedder policy.
1896    let RequestPolicyContainer::PolicyContainer(ref policy_container) =
1897        request_client.policy_container
1898    else {
1899        return CrossOriginResourcePolicy::Blocked;
1900    };
1901
1902    let embedder_policy = &policy_container.embedder_policy;
1903
1904    // Step 3. If the cross-origin resource policy internal check with origin, "unsafe-none",
1905    // response, and forNavigation returns blocked, then return blocked.
1906    if cross_origin_resource_policy_internal_check(
1907        origin,
1908        EmbedderPolicyValue::UnsafeNone,
1909        response,
1910        &for_navigation,
1911    ) == CrossOriginResourcePolicy::Blocked
1912    {
1913        return CrossOriginResourcePolicy::Blocked;
1914    }
1915
1916    // TODO Step 4. If the cross-origin resource policy internal check with origin,
1917    // embedderPolicy’s report only value, response, and forNavigation returns blocked, then queue
1918    // a cross-origin embedder policy CORP violation report with response, settingsObject,
1919    // destination, and true.
1920
1921    // Step 5. If the cross-origin resource policy internal check with origin, embedderPolicy’s
1922    // value, response, and forNavigation returns allowed, then return allowed.
1923    if cross_origin_resource_policy_internal_check(
1924        origin,
1925        embedder_policy.value,
1926        response,
1927        &for_navigation,
1928    ) == CrossOriginResourcePolicy::Allowed
1929    {
1930        return CrossOriginResourcePolicy::Allowed;
1931    }
1932
1933    // TODO Step 6. Queue a cross-origin embedder policy CORP violation report with response,
1934    // settingsObject, destination, and false.
1935
1936    // Step 7. Return blocked.
1937    CrossOriginResourcePolicy::Blocked
1938}
1939
1940/// <https://fetch.spec.whatwg.org/#cross-origin-resource-policy-internal-check>
1941fn cross_origin_resource_policy_internal_check(
1942    origin: &Origin,
1943    embedder_policy_value: EmbedderPolicyValue,
1944    response: &Response,
1945    for_navigation: &ForNavigation,
1946) -> CrossOriginResourcePolicy {
1947    // Step 1. If forNavigation is true and embedderPolicyValue is "unsafe-none", then return allowed.
1948    if let ForNavigation::Yes = for_navigation &&
1949        let EmbedderPolicyValue::UnsafeNone = embedder_policy_value
1950    {
1951        return CrossOriginResourcePolicy::Allowed;
1952    }
1953
1954    // Step 2. Let policy be the result of getting `Cross-Origin-Resource-Policy` from response’s header list.
1955    let policy = response
1956        .headers
1957        .get(HeaderName::from_static("cross-origin-resource-policy"))
1958        .and_then(|h| h.to_str().ok());
1959
1960    // Step 3. If policy is neither `same-origin`, `same-site`, nor `cross-origin`, then set policy to null.
1961    let policy = policy
1962        .filter(|&s| s == "same-origin" || s == "same-site" || s == "cross-origin")
1963        // Step 4. If policy is null, then switch on embedderPolicyValue:
1964        .or(match embedder_policy_value {
1965            // Do nothing.
1966            EmbedderPolicyValue::UnsafeNone => None,
1967            // Set policy to `same-origin`.
1968            EmbedderPolicyValue::RequireCorp => Some("same-origin"),
1969        });
1970
1971    // Step 5. Switch on policy:
1972    match policy {
1973        Some("same-origin") => {
1974            // If origin is same origin with response’s URL’s origin, then return allowed.
1975            if let Origin::Origin(request_origin) = origin &&
1976                response
1977                    .url()
1978                    .is_some_and(|url| request_origin == &url.origin())
1979            {
1980                return CrossOriginResourcePolicy::Allowed;
1981            }
1982
1983            // Otherwise, return blocked.
1984            CrossOriginResourcePolicy::Blocked
1985        },
1986        Some("same-site") => {
1987            if let Some(response_url) = response.url() {
1988                // If all of the following are true
1989                // origin is schemelessly same site with response’s URL’s origin
1990                // origin’s scheme is "https" or response’s URL’s scheme is not "https"
1991                if let Origin::Origin(request_origin) = origin &&
1992                    is_schemelessy_same_site(request_origin, &response_url.origin()) &&
1993                    (request_origin.scheme() == Some("https") ||
1994                        response_url.scheme() != "https")
1995                {
1996                    return CrossOriginResourcePolicy::Allowed;
1997                }
1998            }
1999            // Otherwise, return blocked.
2000            CrossOriginResourcePolicy::Blocked
2001        },
2002        // null / 'cross-origin'
2003        // Return allowed.
2004        _ => CrossOriginResourcePolicy::Allowed,
2005    }
2006}
2007
2008// Convenience struct that implements Done, for setting responseEnd on function return
2009struct ResponseEndTimer(Option<ResourceFetchTimingContainer>);
2010
2011impl ResponseEndTimer {
2012    fn neuter(&mut self) {
2013        self.0 = None;
2014    }
2015}
2016
2017impl Drop for ResponseEndTimer {
2018    fn drop(&mut self) {
2019        let ResponseEndTimer(resource_fetch_timing_opt) = self;
2020
2021        resource_fetch_timing_opt.as_ref().map_or((), |t| {
2022            t.set_attribute(ResourceAttribute::ResponseEnd);
2023        })
2024    }
2025}
2026
2027/// [HTTP network fetch](https://fetch.spec.whatwg.org/#http-network-fetch)
2028#[servo_tracing::instrument(skip_all,fields(url=fetch_params.request.url().as_str()))]
2029async fn http_network_fetch(
2030    fetch_params: &mut FetchParams,
2031    credentials_flag: bool,
2032    done_chan: &mut DoneChannel,
2033    context: &FetchContext,
2034) -> Response {
2035    let mut response_end_timer = ResponseEndTimer(Some(context.timing.clone()));
2036
2037    // Step 1: Let request be fetchParams’s request.
2038    let request = &mut fetch_params.request;
2039
2040    // TODO Step 2. If request’s client is offline, then return a network error.
2041
2042    // TODO Step 3. Let response be null.
2043
2044    // TODO Step 4. Let timingInfo be fetchParams’s timing info.
2045
2046    // TODO Step 5. Let networkPartitionKey be the result of determining the network partition key
2047    // given request.
2048
2049    let url = request.current_url();
2050    let request_id = request.id.0.to_string();
2051    if log_enabled!(log::Level::Info) {
2052        info!("{:?} request for {}", request.method, url);
2053        for header in request.headers.iter() {
2054            debug!(" - {:?}", header);
2055        }
2056    }
2057
2058    // XHR uses the default destination; other kinds of fetches (which haven't been implemented yet)
2059    // do not. Once we support other kinds of fetches we'll need to be more fine grained here
2060    // since things like image fetches are classified differently by devtools
2061    let is_xhr = request.destination == Destination::None;
2062
2063    // The receiver will receive true if there has been an error streaming the request body.
2064    let (fetch_terminated_sender, mut fetch_terminated_receiver) = unbounded_channel();
2065
2066    let body = request.body.as_ref().map(|body| body.clone_stream());
2067
2068    if body.is_none() {
2069        // There cannot be an error streaming a non-existent body.
2070        // However in such a case the channel will remain unused
2071        // and drop inside `obtain_response`.
2072        // Send the confirmation now, ensuring the receiver will not dis-connect first.
2073        let _ = fetch_terminated_sender.send(false);
2074    }
2075
2076    let browsing_context_id = request.target_webview_id.map(Into::into);
2077
2078    // Step 6. Let newConnection be "yes" if forceNewConnection is true; otherwise "no".
2079
2080    // Step 7. Switch on request’s mode:
2081    let (res, msg) = match &request.mode {
2082        // Let connection be the result of obtaining a WebSocket connection, given request’s current URL.
2083        RequestMode::WebSocket {
2084            protocols,
2085            original_url: _,
2086        } => {
2087            // https://fetch.spec.whatwg.org/#websocket-opening-handshake
2088
2089            let (resource_event_sender, dom_action_receiver) = {
2090                let mut websocket_chan = context.websocket_chan.as_ref().unwrap().lock();
2091                (
2092                    websocket_chan.sender.clone(),
2093                    websocket_chan.receiver.take().unwrap(),
2094                )
2095            };
2096
2097            let mut tls_config = create_tls_config(
2098                context.ca_certificates.clone(),
2099                context.ignore_certificate_errors,
2100                context.state.override_manager.clone(),
2101            );
2102            tls_config.alpn_protocols = vec!["http/1.1".to_string().into()];
2103
2104            let response = match start_websocket(
2105                context.state.clone(),
2106                resource_event_sender,
2107                protocols,
2108                request,
2109                tls_config,
2110                dom_action_receiver,
2111            )
2112            .await
2113            {
2114                Ok(response) => response,
2115                Err(error) => {
2116                    return Response::network_error(NetworkError::WebsocketConnectionFailure(
2117                        format!("{error:?}"),
2118                    ));
2119                },
2120            };
2121
2122            let response = response.map(|r| match r {
2123                Some(body) => Full::from(body).map_err(|_| unreachable!()).boxed(),
2124                None => http_body_util::Empty::new()
2125                    .map_err(|_| unreachable!())
2126                    .boxed(),
2127            });
2128            (Decoder::detect(response, url.is_secure_scheme()), None)
2129        },
2130        // Let connection be the result of obtaining a connection, given networkPartitionKey,
2131        // request’s current URL, includeCredentials, and newConnection.
2132        _ => {
2133            let response_future = obtain_response(
2134                &context.state.client,
2135                &url,
2136                &request.method,
2137                &mut request.headers,
2138                body,
2139                request
2140                    .body
2141                    .as_ref()
2142                    .is_some_and(|body| body.source_is_null()),
2143                &request.pipeline_id,
2144                Some(&request_id),
2145                request.destination,
2146                is_xhr,
2147                context,
2148                fetch_terminated_sender,
2149                browsing_context_id,
2150            );
2151
2152            // This will only get the headers, the body is read later
2153            let (res, msg) = match response_future.await {
2154                Ok(wrapped_response) => wrapped_response,
2155                Err(error) => return Response::network_error(error),
2156            };
2157            (res, msg)
2158        },
2159    };
2160
2161    if log_enabled!(log::Level::Info) {
2162        debug!("{:?} response for {}", res.version(), url);
2163        for header in res.headers().iter() {
2164            debug!(" - {:?}", header);
2165        }
2166    }
2167
2168    // Check if there was an error while streaming the request body.
2169    //
2170    match fetch_terminated_receiver.recv().await {
2171        Some(true) => return Response::network_error(NetworkError::ConnectionFailure),
2172        Some(false) => {},
2173        _ => warn!("Failed to receive confirmation request was streamed without error."),
2174    }
2175
2176    let timing = context.timing.inner().clone();
2177    let mut response = Response::new(url.clone(), timing);
2178
2179    if let Some(handshake_info) = res.extensions().get::<TlsHandshakeInfo>() {
2180        let mut hsts_enabled = url
2181            .host_str()
2182            .is_some_and(|host| context.state.hsts_list.read().is_host_secure(host));
2183
2184        if url.scheme() == "https" &&
2185            let Some(sts) = res.headers().typed_get::<StrictTransportSecurity>()
2186        {
2187            // max-age > 0 enables HSTS, max-age = 0 disables it (RFC 6797 Section 6.1.1)
2188            hsts_enabled = sts.max_age().as_secs() > 0;
2189        }
2190        response.tls_security_info = Some(build_tls_security_info(handshake_info, hsts_enabled));
2191    }
2192
2193    let status_text = res
2194        .extensions()
2195        .get::<ReasonPhrase>()
2196        .map(ReasonPhrase::as_bytes)
2197        .or_else(|| res.status().canonical_reason().map(str::as_bytes))
2198        .map(Vec::from)
2199        .unwrap_or_default();
2200    response.status = HttpStatus::new(res.status(), status_text);
2201
2202    info!("got {:?} response for {:?}", res.status(), request.url());
2203    response.headers = res.headers().clone();
2204    response.referrer = request.referrer.to_url().cloned();
2205    response.referrer_policy = request.referrer_policy;
2206
2207    let res_body = response.body.clone();
2208
2209    // We're about to spawn a future to be waited on here
2210    let (done_sender, done_receiver) = unbounded_channel();
2211    *done_chan = Some((done_sender.clone(), done_receiver));
2212
2213    let devtools_sender = context.devtools_chan.clone();
2214    let cancellation_listener = context.cancellation_listener.clone();
2215    if cancellation_listener.cancelled() {
2216        return Response::network_error(NetworkError::LoadCancelled);
2217    }
2218
2219    *res_body.lock() = ResponseBody::Receiving(vec![]);
2220    let res_body2 = res_body.clone();
2221
2222    if let Some(ref sender) = devtools_sender &&
2223        let Some(m) = msg
2224    {
2225        send_request_to_devtools(m, sender);
2226    }
2227
2228    let done_sender2 = done_sender.clone();
2229    let done_sender3 = done_sender.clone();
2230    let timing_ptr2 = context.timing.clone();
2231    let timing_ptr3 = context.timing.clone();
2232    let devtools_request = request.clone();
2233    let url1 = devtools_request.url();
2234    let url2 = url1.clone();
2235
2236    let status = response.status.clone();
2237    let headers = response.headers.clone();
2238    let devtools_chan = context.devtools_chan.clone();
2239
2240    spawn_task(
2241        res.into_body()
2242            .try_fold(res_body, move |res_body, chunk| {
2243                if cancellation_listener.cancelled() {
2244                    *res_body.lock() = ResponseBody::Done(vec![]);
2245                    let _ = done_sender.send(Data::Cancelled);
2246                    return future::ready(Err(std::io::Error::new(
2247                        std::io::ErrorKind::Interrupted,
2248                        "Fetch aborted",
2249                    )));
2250                }
2251                if let ResponseBody::Receiving(ref mut body) = *res_body.lock() {
2252                    let bytes = chunk;
2253                    body.extend_from_slice(&bytes);
2254                    let _ = done_sender.send(Data::Payload(bytes.to_vec()));
2255                }
2256                future::ready(Ok(res_body))
2257            })
2258            .and_then(move |res_body| {
2259                debug!("successfully finished response for {:?}", url1);
2260                let mut body = res_body.lock();
2261                let completed_body = match *body {
2262                    ResponseBody::Receiving(ref mut body) => std::mem::take(body),
2263                    _ => vec![],
2264                };
2265                let devtools_response_body = completed_body.clone();
2266                *body = ResponseBody::Done(completed_body);
2267                send_response_values_to_devtools(
2268                    Some(headers),
2269                    status,
2270                    Some(devtools_response_body),
2271                    CacheState::None,
2272                    &devtools_request,
2273                    devtools_chan,
2274                );
2275                timing_ptr2.set_attribute(ResourceAttribute::ResponseEnd);
2276                let _ = done_sender2.send(Data::Done);
2277                future::ready(Ok(()))
2278            })
2279            .map_err(move |error| {
2280                if let std::io::ErrorKind::InvalidData = error.kind() {
2281                    debug!("Content decompression error for {:?}", url2);
2282                    let _ = done_sender3.send(Data::Error(NetworkError::DecompressionError));
2283                    let mut body = res_body2.lock();
2284
2285                    *body = ResponseBody::Done(vec![]);
2286                }
2287                debug!("finished response for {:?}", url2);
2288                let mut body = res_body2.lock();
2289                let completed_body = match *body {
2290                    ResponseBody::Receiving(ref mut body) => std::mem::take(body),
2291                    _ => vec![],
2292                };
2293                *body = ResponseBody::Done(completed_body);
2294                timing_ptr3.set_attribute(ResourceAttribute::ResponseEnd);
2295                let _ = done_sender3.send(Data::Done);
2296            }),
2297    );
2298
2299    // TODO these substeps aren't possible yet
2300    // Substep 1
2301
2302    // Substep 2
2303
2304    // TODO Read request
2305
2306    // Step 6-11
2307    // (needs stream bodies)
2308
2309    // Step 13
2310    // TODO this step isn't possible yet (CSP)
2311
2312    // Step 14, update the cached response, done via the shared response body.
2313
2314    // TODO this step isn't possible yet
2315    // Step 15
2316    if credentials_flag {
2317        set_cookies_from_headers(&url, &response.headers, &context.state.cookie_jar);
2318    }
2319    context
2320        .state
2321        .hsts_list
2322        .write()
2323        .update_hsts_list_from_response(&url, &response.headers);
2324
2325    // TODO these steps
2326    // Step 16
2327    // Substep 1
2328    // Substep 2
2329    // Sub-substep 1
2330    // Sub-substep 2
2331    // Sub-substep 3
2332    // Sub-substep 4
2333    // Substep 3
2334
2335    // Step 16
2336
2337    // Ensure we don't override "responseEnd" on successful return of this function
2338    response_end_timer.neuter();
2339    response
2340}
2341
2342/// [CORS preflight fetch](https://fetch.spec.whatwg.org#cors-preflight-fetch)
2343async fn cors_preflight_fetch(
2344    request: &Request,
2345    cache: &mut CorsCache,
2346    context: &FetchContext,
2347) -> Response {
2348    // Step 1. Let preflight be a new request whose method is `OPTIONS`, URL list is a clone
2349    // of request’s URL list, initiator is request’s initiator, destination is request’s destination,
2350    // origin is request’s origin, referrer is request’s referrer, referrer policy is request’s
2351    // referrer policy, mode is "cors", and response tainting is "cors".
2352    let mut preflight = RequestBuilder::new(
2353        request.target_webview_id,
2354        request.current_url_with_blob_claim(),
2355        request.referrer.clone(),
2356    )
2357    .method(Method::OPTIONS)
2358    .origin(match &request.origin {
2359        Origin::Client => {
2360            unreachable!("We shouldn't get Client origin in cors_preflight_fetch.")
2361        },
2362        Origin::Origin(origin) => origin.clone(),
2363    })
2364    .pipeline_id(request.pipeline_id)
2365    .initiator(request.initiator)
2366    .destination(request.destination)
2367    .referrer_policy(request.referrer_policy)
2368    .mode(RequestMode::CorsMode)
2369    .response_tainting(ResponseTainting::CorsTainting)
2370    .policy_container(match &request.policy_container {
2371        RequestPolicyContainer::Client => {
2372            unreachable!("We should have a policy container for request in cors_preflight_fetch")
2373        },
2374        RequestPolicyContainer::PolicyContainer(policy_container) => policy_container.clone(),
2375    })
2376    .url_list(
2377        request
2378            .url_list
2379            .iter()
2380            .map(|claimed_url| claimed_url.url())
2381            .collect(),
2382    )
2383    .build();
2384
2385    // Step 2. Append (`Accept`, `*/*`) to preflight’s header list.
2386    preflight
2387        .headers
2388        .insert(ACCEPT, HeaderValue::from_static("*/*"));
2389
2390    // Step 3. Append (`Access-Control-Request-Method`, request’s method) to preflight’s header list.
2391    preflight
2392        .headers
2393        .typed_insert::<AccessControlRequestMethod>(AccessControlRequestMethod::from(
2394            request.method.clone(),
2395        ));
2396
2397    // Step 4. Let headers be the CORS-unsafe request-header names with request’s header list.
2398    let headers = get_cors_unsafe_header_names(&request.headers);
2399
2400    // Step 5 If headers is not empty, then:
2401    if !headers.is_empty() {
2402        // 5.1 Let value be the items in headers separated from each other by `,`
2403        // TODO(36451): replace this with typed_insert when headers fixes headers#207
2404        preflight.headers.insert(
2405            ACCESS_CONTROL_REQUEST_HEADERS,
2406            HeaderValue::from_bytes(itertools::join(headers.iter(), ",").as_bytes())
2407                .unwrap_or(HeaderValue::from_static("")),
2408        );
2409    }
2410
2411    // Step 6. Let response be the result of running HTTP-network-or-cache fetch given a
2412    // new fetch params whose request is preflight.
2413    let mut fetch_params = FetchParams::new(preflight);
2414    let response =
2415        http_network_or_cache_fetch(&mut fetch_params, false, false, &mut None, context).await;
2416
2417    // Step 7. If a CORS check for request and response returns success and response’s status is an ok status, then:
2418    if cors_check(request, &response).is_ok() && response.status.code().is_success() {
2419        // Step 7.1 Let methods be the result of extracting header list values given
2420        // `Access-Control-Allow-Methods` and response’s header list.
2421        let mut methods = if response
2422            .headers
2423            .contains_key(header::ACCESS_CONTROL_ALLOW_METHODS)
2424        {
2425            match response.headers.typed_get::<AccessControlAllowMethods>() {
2426                Some(methods) => methods.iter().collect(),
2427                // Step 7.3 If either methods or headerNames is failure, return a network error.
2428                None => {
2429                    return Response::network_error(NetworkError::CorsAllowMethods);
2430                },
2431            }
2432        } else {
2433            vec![]
2434        };
2435
2436        // Step 7.2 Let headerNames be the result of extracting header list values given
2437        // `Access-Control-Allow-Headers` and response’s header list.
2438        let header_names = if response
2439            .headers
2440            .contains_key(header::ACCESS_CONTROL_ALLOW_HEADERS)
2441        {
2442            match response.headers.typed_get::<AccessControlAllowHeaders>() {
2443                Some(names) => names.iter().collect(),
2444                // Step 7.3 If either methods or headerNames is failure, return a network error.
2445                None => {
2446                    return Response::network_error(NetworkError::CorsAllowHeaders);
2447                },
2448            }
2449        } else {
2450            vec![]
2451        };
2452
2453        debug!(
2454            "CORS check: Allowed methods: {:?}, current method: {:?}",
2455            methods, request.method
2456        );
2457
2458        // Step 7.4 If methods is null and request’s use-CORS-preflight flag is set,
2459        // then set methods to a new list containing request’s method.
2460        if methods.is_empty() && request.use_cors_preflight {
2461            methods = vec![request.method.clone()];
2462        }
2463
2464        // Step 7.5 If request’s method is not in methods, request’s method is not a CORS-safelisted method,
2465        // and request’s credentials mode is "include" or methods does not contain `*`, then return a network error.
2466        if methods
2467            .iter()
2468            .all(|method| *method.as_str() != *request.method.as_ref()) &&
2469            !is_cors_safelisted_method(&request.method) &&
2470            (request.credentials_mode == CredentialsMode::Include ||
2471                methods.iter().all(|method| method.as_ref() != "*"))
2472        {
2473            return Response::network_error(NetworkError::CorsMethod);
2474        }
2475
2476        debug!(
2477            "CORS check: Allowed headers: {:?}, current headers: {:?}",
2478            header_names, request.headers
2479        );
2480
2481        // Step 7.6 If one of request’s header list’s names is a CORS non-wildcard request-header name
2482        // and is not a byte-case-insensitive match for an item in headerNames, then return a network error.
2483        if request.headers.iter().any(|(name, _)| {
2484            is_cors_non_wildcard_request_header_name(name) &&
2485                header_names.iter().all(|header_name| header_name != name)
2486        }) {
2487            return Response::network_error(NetworkError::CorsAuthorization);
2488        }
2489
2490        // Step 7.7 For each unsafeName of the CORS-unsafe request-header names with request’s header list,
2491        // if unsafeName is not a byte-case-insensitive match for an item in headerNames and request’s credentials
2492        // mode is "include" or headerNames does not contain `*`, return a network error.
2493        let unsafe_names = get_cors_unsafe_header_names(&request.headers);
2494        let header_names_set: HashSet<&HeaderName> = HashSet::from_iter(header_names.iter());
2495        let header_names_contains_star = header_names
2496            .iter()
2497            .any(|header_name| header_name.as_str() == "*");
2498        for unsafe_name in unsafe_names.iter() {
2499            if !header_names_set.contains(unsafe_name) &&
2500                (request.credentials_mode == CredentialsMode::Include ||
2501                    !header_names_contains_star)
2502            {
2503                return Response::network_error(NetworkError::CorsHeaders);
2504            }
2505        }
2506
2507        // Step 7.8 Let max-age be the result of extracting header list values given
2508        // `Access-Control-Max-Age` and response’s header list.
2509        let max_age: Option<Duration> = response
2510            .headers
2511            .typed_get::<AccessControlMaxAge>()
2512            .map(|acma| acma.into());
2513
2514        // Step 7.9 If max-age is failure or null, then set max-age to 5.
2515        let max_age = max_age.unwrap_or(Duration::from_secs(5));
2516
2517        // Step 7.10 If max-age is greater than an imposed limit on max-age, then set max-age to the imposed limit.
2518        // TODO: Need to define what an imposed limit on max-age is
2519
2520        // Step 7.11 If the user agent does not provide for a cache, then return response.
2521        // NOTE: This can be ignored, we do have a CORS cache
2522
2523        // Step 7.12 For each method in methods for which there is a method cache entry match using request,
2524        // set matching entry’s max-age to max-age.
2525        // Step 7.13 For each method in methods for which there is no method cache entry match using request,
2526        // create a new cache entry with request, max-age, method, and null.
2527        for method in &methods {
2528            cache.match_method_and_update(request, method.clone(), max_age);
2529        }
2530
2531        // Step 7.14 For each headerName in headerNames for which there is a header-name cache entry match using request,
2532        // set matching entry’s max-age to max-age.
2533        // Step 7.15 For each headerName in headerNames for which there is no header-name cache entry match using request,
2534        // create a new cache entry with request, max-age, null, and headerName.
2535        for header_name in &header_names {
2536            cache.match_header_and_update(request, header_name, max_age);
2537        }
2538
2539        // Step 7.16 Return response.
2540        return response;
2541    }
2542
2543    // Step 8 Return a network error.
2544    Response::network_error(NetworkError::CorsGeneral)
2545}
2546
2547/// [CORS check](https://fetch.spec.whatwg.org#concept-cors-check)
2548fn cors_check(request: &Request, response: &Response) -> Result<(), ()> {
2549    // Step 1. Let origin be the result of getting `Access-Control-Allow-Origin` from response’s header list.
2550    let Some(origins) =
2551        get_value_from_header_list(ACCESS_CONTROL_ALLOW_ORIGIN.as_str(), &response.headers)
2552    else {
2553        // Step 2. If origin is null, then return failure.
2554        return Err(());
2555    };
2556    let origin = origins.into_iter().map(char::from).collect::<String>();
2557
2558    // Step 3. If request’s credentials mode is not "include" and origin is `*`, then return success.
2559    if request.credentials_mode != CredentialsMode::Include && origin == "*" {
2560        return Ok(());
2561    }
2562
2563    // Step 4. If the result of byte-serializing a request origin with request is not origin, then return failure.
2564    if serialize_request_origin(request).to_string() != origin {
2565        return Err(());
2566    }
2567
2568    // Step 5. If request’s credentials mode is not "include", then return success.
2569    if request.credentials_mode != CredentialsMode::Include {
2570        return Ok(());
2571    }
2572
2573    // Step 6. Let credentials be the result of getting `Access-Control-Allow-Credentials` from response’s header list.
2574    let credentials = response
2575        .headers
2576        .typed_get::<AccessControlAllowCredentials>();
2577
2578    // Step 7. If credentials is `true`, then return success.
2579    if credentials.is_some() {
2580        return Ok(());
2581    }
2582
2583    // Step 8. Return failure.
2584    Err(())
2585}
2586
2587fn has_credentials(url: &ServoUrl) -> bool {
2588    !url.username().is_empty() || url.password().is_some()
2589}
2590
2591fn is_no_store_cache(headers: &HeaderMap) -> bool {
2592    headers.contains_key(header::IF_MODIFIED_SINCE) |
2593        headers.contains_key(header::IF_NONE_MATCH) |
2594        headers.contains_key(header::IF_UNMODIFIED_SINCE) |
2595        headers.contains_key(header::IF_MATCH) |
2596        headers.contains_key(header::IF_RANGE)
2597}
2598
2599/// <https://fetch.spec.whatwg.org/#redirect-status>
2600fn is_redirect_status(status: StatusCode) -> bool {
2601    matches!(
2602        status,
2603        StatusCode::MOVED_PERMANENTLY |
2604            StatusCode::FOUND |
2605            StatusCode::SEE_OTHER |
2606            StatusCode::TEMPORARY_REDIRECT |
2607            StatusCode::PERMANENT_REDIRECT
2608    )
2609}
2610
2611/// <https://fetch.spec.whatwg.org/#serializing-a-request-origin>
2612fn serialize_request_origin(request: &Request) -> headers::Origin {
2613    // Step 1. Assert: request’s origin is not "client".
2614    let Origin::Origin(origin) = &request.origin else {
2615        panic!("origin cannot be \"client\" at this point in time");
2616    };
2617
2618    // Step 2. If request’s redirect-taint is not "same-origin", then return "null".
2619    if request.redirect_taint_for_request() != RedirectTaint::SameOrigin {
2620        return headers::Origin::NULL;
2621    }
2622
2623    // Step 3. Return request’s origin, serialized.
2624    serialize_origin(origin)
2625}
2626
2627/// Step 3 of <https://fetch.spec.whatwg.org/#serializing-a-request-origin>.
2628pub fn serialize_origin(origin: &ImmutableOrigin) -> headers::Origin {
2629    match origin {
2630        ImmutableOrigin::Opaque(_) => headers::Origin::NULL,
2631        ImmutableOrigin::Tuple(scheme, host, port) => {
2632            // Note: This must be kept in sync with `Origin::ascii_serialization()`, which does not
2633            // use the port number when a default port is used.
2634            let port = match (scheme.as_ref(), port) {
2635                ("http" | "ws", 80) | ("https" | "wss", 443) | ("ftp", 21) => None,
2636                _ => Some(*port),
2637            };
2638
2639            // TODO: Ensure that hyper/servo don't disagree about valid origin headers
2640            headers::Origin::try_from_parts(scheme, &host.to_string(), port)
2641                .unwrap_or(headers::Origin::NULL)
2642        },
2643    }
2644}
2645
2646/// <https://fetch.spec.whatwg.org/#append-a-request-origin-header>
2647#[expect(
2648    clippy::collapsible_match,
2649    reason = "The current way follows the spec more closely"
2650)]
2651fn append_a_request_origin_header(request: &mut Request) {
2652    // Step 1. Assert: request’s origin is not "client".
2653    let Origin::Origin(request_origin) = &request.origin else {
2654        panic!("origin cannot be \"client\" at this point in time");
2655    };
2656
2657    // Step 2. Let serializedOrigin be the result of byte-serializing a request origin with request.
2658    let mut serialized_origin = serialize_request_origin(request);
2659
2660    // Step 3. If request’s response tainting is "cors" or request’s mode is "websocket",
2661    //         then append (`Origin`, serializedOrigin) to request’s header list.
2662    if request.response_tainting == ResponseTainting::CorsTainting ||
2663        matches!(request.mode, RequestMode::WebSocket { .. })
2664    {
2665        request.headers.typed_insert(serialized_origin);
2666    }
2667    // Step 4. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
2668    else if !matches!(request.method, Method::GET | Method::HEAD) {
2669        // Step 4.1 If request’s mode is not "cors", then switch on request’s referrer policy:
2670        if request.mode != RequestMode::CorsMode {
2671            match request.referrer_policy {
2672                ReferrerPolicy::NoReferrer => {
2673                    // Set serializedOrigin to `null`.
2674                    serialized_origin = headers::Origin::NULL;
2675                },
2676                ReferrerPolicy::NoReferrerWhenDowngrade |
2677                ReferrerPolicy::StrictOrigin |
2678                ReferrerPolicy::StrictOriginWhenCrossOrigin => {
2679                    // If request’s origin is a tuple origin, its scheme is "https", and
2680                    // request’s current URL’s scheme is not "https", then set serializedOrigin to `null`.
2681                    if let ImmutableOrigin::Tuple(scheme, _, _) = &request_origin &&
2682                        scheme == "https" &&
2683                        request.current_url().scheme() != "https"
2684                    {
2685                        serialized_origin = headers::Origin::NULL;
2686                    }
2687                },
2688                ReferrerPolicy::SameOrigin => {
2689                    // If request’s origin is not same origin with request’s current URL’s origin,
2690                    // then set serializedOrigin to `null`.
2691                    if *request_origin != request.current_url().origin() {
2692                        serialized_origin = headers::Origin::NULL;
2693                    }
2694                },
2695                _ => {
2696                    // Otherwise, do nothing.
2697                },
2698            };
2699        }
2700
2701        // Step 4.2. Append (`Origin`, serializedOrigin) to request’s header list.
2702        request.headers.typed_insert(serialized_origin);
2703    }
2704}
2705
2706/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-append-the-fetch-metadata-headers-for-a-request>
2707fn append_the_fetch_metadata_headers(r: &mut Request) {
2708    // Step 1. If r’s url is not an potentially trustworthy URL, return.
2709    if !r.url().is_potentially_trustworthy() {
2710        return;
2711    }
2712
2713    // Step 2. Set the Sec-Fetch-Dest header for r.
2714    set_the_sec_fetch_dest_header(r);
2715
2716    // Step 3. Set the Sec-Fetch-Mode header for r.
2717    set_the_sec_fetch_mode_header(r);
2718
2719    // Step 4. Set the Sec-Fetch-Site header for r.
2720    set_the_sec_fetch_site_header(r);
2721
2722    // Step 5. Set the Sec-Fetch-User header for r.
2723    set_the_sec_fetch_user_header(r);
2724}
2725
2726/// Steps 8.16 to 8.18 in [HTTP network or cache fetch](https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch)
2727fn append_cache_data_to_headers(http_request: &mut Request) {
2728    match http_request.cache_mode {
2729        // Step 8.16: If httpRequest’s cache mode is "default" and httpRequest’s header list
2730        // contains `If-Modified-Since`, `If-None-Match`, `If-Unmodified-Since`, `If-Match`, or
2731        // `If-Range`, then set httpRequest’s cache mode to "no-store".
2732        CacheMode::Default if is_no_store_cache(&http_request.headers) => {
2733            http_request.cache_mode = CacheMode::NoStore;
2734        },
2735
2736        // Note that the following steps (8.17 and 8.18) are being considered for removal:
2737        // https://github.com/whatwg/fetch/issues/722#issuecomment-1420264615
2738
2739        // Step 8.17: If httpRequest’s cache mode is "no-cache", httpRequest’s prevent no-cache
2740        // cache-control header modification flag is unset, and httpRequest’s header list does not
2741        // contain `Cache-Control`, then append (`Cache-Control`, `max-age=0`) to httpRequest’s
2742        // header list.
2743        // TODO: Implement request's prevent no-cache cache-control header modification flag
2744        // https://fetch.spec.whatwg.org/#no-cache-prevent-cache-control
2745        CacheMode::NoCache if !http_request.headers.contains_key(header::CACHE_CONTROL) => {
2746            http_request
2747                .headers
2748                .typed_insert(CacheControl::new().with_max_age(Duration::from_secs(0)));
2749        },
2750
2751        // Step 8.18: If httpRequest’s cache mode is "no-store" or "reload", then:
2752        CacheMode::Reload | CacheMode::NoStore => {
2753            // Step 8.18.1: If httpRequest’s header list does not contain `Pragma`, then append
2754            // (`Pragma`, `no-cache`) to httpRequest’s header list.
2755            if !http_request.headers.contains_key(header::PRAGMA) {
2756                http_request.headers.typed_insert(Pragma::no_cache());
2757            }
2758
2759            // Step 8.18.2: If httpRequest’s header list does not contain `Cache-Control`, then
2760            // append (`Cache-Control`, `no-cache`) to httpRequest’s header list.
2761            if !http_request.headers.contains_key(header::CACHE_CONTROL) {
2762                http_request
2763                    .headers
2764                    .typed_insert(CacheControl::new().with_no_cache());
2765            }
2766        },
2767
2768        _ => {},
2769    }
2770}
2771
2772/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-dest>
2773fn set_the_sec_fetch_dest_header(r: &mut Request) {
2774    // Step 1. Assert: r’s url is a potentially trustworthy URL.
2775    debug_assert!(r.url().is_potentially_trustworthy());
2776
2777    // Step 2. Let header be a Structured Header whose value is a token.
2778    // Step 3. If r’s destination is the empty string, set header’s value to the string "empty".
2779    // Otherwise, set header’s value to r’s destination.
2780    let header = r.destination;
2781
2782    // Step 4. Set a structured field value `Sec-Fetch-Dest`/header in r’s header list.
2783    r.headers.typed_insert(SecFetchDest(header));
2784}
2785
2786/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-mode>
2787fn set_the_sec_fetch_mode_header(r: &mut Request) {
2788    // Step 1. Assert: r’s url is a potentially trustworthy URL.
2789    debug_assert!(r.url().is_potentially_trustworthy());
2790
2791    // Step 2. Let header be a Structured Header whose value is a token.
2792    // Step 3. Set header’s value to r’s mode.
2793    let header = &r.mode;
2794
2795    // Step 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
2796    r.headers.typed_insert(SecFetchMode::from(header));
2797}
2798
2799/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-site>
2800fn set_the_sec_fetch_site_header(r: &mut Request) {
2801    // The webappsec spec seems to have a similar issue as
2802    // https://github.com/whatwg/fetch/issues/1773
2803    let Origin::Origin(request_origin) = &r.origin else {
2804        panic!("request origin cannot be \"client\" at this point")
2805    };
2806
2807    // Step 1. Assert: r’s url is a potentially trustworthy URL.
2808    debug_assert!(r.url().is_potentially_trustworthy());
2809
2810    // Step 2. Let header be a Structured Header whose value is a token.
2811    // Step 3. Set header’s value to same-origin.
2812    let mut header = SecFetchSite::SameOrigin;
2813
2814    // TODO: Step 3. If r is a navigation request that was explicitly caused by a
2815    // user’s interaction with the user agent, then set header’s value to none.
2816
2817    // Step 5. If header’s value is not none, then for each url in r’s url list:
2818    if header != SecFetchSite::None {
2819        for url in &r.url_list {
2820            // Step 5.1 If url is same origin with r’s origin, continue.
2821            if url.origin() == *request_origin {
2822                continue;
2823            }
2824
2825            // Step 5.2 Set header’s value to cross-site.
2826            header = SecFetchSite::CrossSite;
2827
2828            // Step 5.3 If r’s origin is not same site with url’s origin, then break.
2829            if !is_same_site(request_origin, &url.origin()) {
2830                break;
2831            }
2832
2833            // Step 5.4 Set header’s value to same-site.
2834            header = SecFetchSite::SameSite;
2835        }
2836    }
2837
2838    // Step 6. Set a structured field value `Sec-Fetch-Site`/header in r’s header list.
2839    r.headers.typed_insert(header);
2840}
2841
2842/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-user>
2843fn set_the_sec_fetch_user_header(r: &mut Request) {
2844    // Step 1. Assert: r’s url is a potentially trustworthy URL.
2845    debug_assert!(r.url().is_potentially_trustworthy());
2846
2847    // Step 2. If r is not a navigation request, or if r’s user-activation is false, return.
2848    // TODO user activation
2849    if !r.is_navigation_request() {
2850        return;
2851    }
2852
2853    // Step 3. Let header be a Structured Header whose value is a token.
2854    // Step 4. Set header’s value to true.
2855    let header = SecFetchUser;
2856
2857    // Step 5. Set a structured field value `Sec-Fetch-User`/header in r’s header list.
2858    r.headers.typed_insert(header);
2859}
2860
2861/// <https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect>
2862fn set_requests_referrer_policy_on_redirect(request: &mut Request, response: &Response) {
2863    // Step 1: Let policy be the result of executing § 8.1 Parse a referrer policy from a
2864    // Referrer-Policy header on actualResponse.
2865    let referrer_policy: ReferrerPolicy = response
2866        .headers
2867        .typed_get::<headers::ReferrerPolicy>()
2868        .into();
2869
2870    // Step 2: If policy is not the empty string, then set request’s referrer policy to policy.
2871    if referrer_policy != ReferrerPolicy::EmptyString {
2872        request.referrer_policy = referrer_policy;
2873    }
2874}