use std::cmp::min;
use std::collections::HashSet;
use std::iter::FromIterator;
use std::sync::Arc as StdArc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, SystemTime};
use async_recursion::async_recursion;
use content_security_policy::percent_encoding::{AsciiSet, CONTROLS};
use embedder_traits::{AuthenticationResponse, GenericEmbedderProxy};
use futures::{TryFutureExt, TryStreamExt, future};
use headers::authorization::Basic;
use headers::{
AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowMethods,
AccessControlMaxAge, AccessControlRequestMethod, Authorization, CacheControl, ContentLength,
HeaderMapExt, IfModifiedSince, LastModified, Pragma, Referer, StrictTransportSecurity,
UserAgent,
};
use http::header::{
self, ACCEPT, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_REQUEST_HEADERS, AUTHORIZATION,
CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LOCATION, CONTENT_TYPE, HeaderValue, RANGE,
WWW_AUTHENTICATE,
};
use http::{HeaderMap, Method, StatusCode};
use http_body_util::{BodyExt, Full};
use hyper::ext::ReasonPhrase;
use hyper::header::HeaderName;
use ipc_channel::IpcError;
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER;
use log::{debug, error, info, log_enabled, warn};
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
use net_traits::blob_url_store::UrlWithBlobClaim;
use net_traits::fetch::headers::get_value_from_header_list;
use net_traits::http_status::HttpStatus;
use net_traits::policy_container::{EmbedderPolicyValue, RequestPolicyContainer};
use net_traits::pub_domains::{is_same_site, reg_suffix};
use net_traits::request::{
BodyChunkRequest, BodyChunkResponse, CacheMode, CredentialsMode, Destination, Initiator,
Origin, RedirectMode, Referrer, Request, RequestBuilder, RequestClient, RequestMode,
ResponseTainting, ServiceWorkersMode, TraversableForUserPrompts, get_cors_unsafe_header_names,
is_cors_non_wildcard_request_header_name, is_cors_safelisted_method,
is_cors_safelisted_request_header,
};
use net_traits::response::{CacheState, RedirectTaint, Response, ResponseBody, ResponseType};
use net_traits::{
CookieSource, DOCUMENT_ACCEPT_HEADER_VALUE, DiscardFetch, NetworkError, RedirectEndValue,
RedirectStartValue, ReferrerPolicy, ResourceAttribute, ResourceFetchTimingContainer,
ResourceTimeValue, TlsSecurityInfo, TlsSecurityState,
};
use parking_lot::{Mutex, RwLock};
use profile_traits::mem::{Report, ReportKind};
use profile_traits::path;
use rustc_hash::FxHashMap;
use servo_base::generic_channel::GenericSharedMemory;
use servo_base::id::HistoryStateId;
use servo_config::pref;
use servo_url::{ImmutableOrigin, ServoUrl};
use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
use crate::async_runtime::spawn_task;
use crate::connector::{
CertificateErrorOverrideManager, TlsHandshakeInfo, create_tls_config,
};
use crate::cookie::ServoCookie;
use crate::cookie_storage::CookieStorage;
use crate::decoder::Decoder;
use crate::devtools::{send_request_to_devtools, send_response_values_to_devtools};
use crate::embedder::NetToEmbedderMsg;
use crate::fetch::cors_cache::CorsCache;
use crate::fetch::fetch_params::FetchParams;
use crate::fetch::headers::{SecFetchDest, SecFetchMode, SecFetchSite, SecFetchUser};
use crate::fetch::methods::{Data, DoneChannel, FetchContext, Target, fetch, main_fetch};
use crate::hsts::HstsList;
use crate::http_cache::{
CacheKey, CachedResourcesOrGuard, HttpCache, ValidationStatus, construct_response,
invalidate_cached_resources, refresh,
};
use crate::resource_thread::{AuthCache, AuthCacheEntry};
use crate::websocket_loader::start_websocket;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum HttpCacheEntryState {
ReadyToConstruct,
PendingStore(usize),
}
pub struct HttpState {
pub hsts_list: RwLock<HstsList>,
pub cookie_jar: RwLock<CookieStorage>,
pub http_cache: HttpCache,
pub auth_cache: RwLock<AuthCache>,
pub history_states: RwLock<FxHashMap<HistoryStateId, Vec<u8>>>,
pub override_manager: CertificateErrorOverrideManager,
pub embedder_proxy: GenericEmbedderProxy<NetToEmbedderMsg>,
}
impl HttpState {
pub(crate) fn memory_reports(&self, suffix: &str, ops: &mut MallocSizeOfOps) -> Vec<Report> {
vec![
Report {
path: path!["memory-cache", suffix],
kind: ReportKind::ExplicitJemallocHeapSize,
size: self.http_cache.size_of(ops),
},
Report {
path: path!["hsts-list", suffix],
kind: ReportKind::ExplicitJemallocHeapSize,
size: self.hsts_list.read().size_of(ops),
},
]
}
async fn request_authentication(
&self,
request: &Request,
response: &Response,
) -> Option<AuthenticationResponse> {
let webview_id = request.target_webview_id?;
let for_proxy = response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED;
if request.mode != RequestMode::Navigate {
return None;
}
let (sender, receiver) = tokio::sync::oneshot::channel();
self.embedder_proxy
.send(NetToEmbedderMsg::RequestAuthentication(
webview_id,
request.url(),
for_proxy,
sender,
));
receiver.await.ok()?
}
}
pub(crate) fn set_default_accept(request: &mut Request) {
if request.headers.contains_key(header::ACCEPT) {
return;
}
let value = if request.initiator == Initiator::Prefetch {
DOCUMENT_ACCEPT_HEADER_VALUE
} else {
match request.destination {
Destination::Document | Destination::Frame | Destination::IFrame => {
DOCUMENT_ACCEPT_HEADER_VALUE
},
Destination::Image => {
HeaderValue::from_static("image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5")
},
Destination::Json => HeaderValue::from_static("application/json,*/*;q=0.5"),
Destination::Style => HeaderValue::from_static("text/css,*/*;q=0.1"),
_ => HeaderValue::from_static("*/*"),
}
};
request.headers.insert(header::ACCEPT, value);
}
fn set_default_accept_encoding(headers: &mut HeaderMap) {
if headers.contains_key(header::ACCEPT_ENCODING) {
return;
}
headers.insert(
header::ACCEPT_ENCODING,
HeaderValue::from_static("gzip, deflate, br, zstd"),
);
}
fn no_referrer_when_downgrade(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
return None;
}
strip_url_for_use_as_referrer(referrer_url, false)
}
fn strict_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
return None;
}
strip_url_for_use_as_referrer(referrer_url, true)
}
fn strict_origin_when_cross_origin(
referrer_url: ServoUrl,
current_url: ServoUrl,
) -> Option<ServoUrl> {
if referrer_url.origin() == current_url.origin() {
return strip_url_for_use_as_referrer(referrer_url, false);
}
if referrer_url.is_potentially_trustworthy() && !current_url.is_potentially_trustworthy() {
return None;
}
strip_url_for_use_as_referrer(referrer_url, true)
}
fn is_schemelessy_same_site(site_a: &ImmutableOrigin, site_b: &ImmutableOrigin) -> bool {
if !site_a.is_tuple() && !site_b.is_tuple() && site_a == site_b {
true
} else if site_a.is_tuple() && site_b.is_tuple() {
let host_a = site_a.host().map(|h| h.to_string()).unwrap_or_default();
let host_b = site_b.host().map(|h| h.to_string()).unwrap_or_default();
let host_a_reg = reg_suffix(&host_a);
let host_b_reg = reg_suffix(&host_b);
(site_a.host() == site_b.host() && host_a_reg.is_empty()) ||
(host_a_reg == host_b_reg && !host_a_reg.is_empty())
} else {
false
}
}
fn strip_url_for_use_as_referrer(mut url: ServoUrl, origin_only: bool) -> Option<ServoUrl> {
const MAX_REFERRER_URL_LENGTH: usize = 4096;
if url.is_local_scheme() {
return None;
}
{
let url = url.as_mut_url();
let _ = url.set_username("");
let _ = url.set_password(None);
url.set_fragment(None);
if origin_only || url.as_str().len() > MAX_REFERRER_URL_LENGTH {
url.set_path("");
url.set_query(None);
}
}
Some(url)
}
fn same_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
if referrer_url.origin() == current_url.origin() {
return strip_url_for_use_as_referrer(referrer_url, false);
}
None
}
fn origin_when_cross_origin(referrer_url: ServoUrl, current_url: ServoUrl) -> Option<ServoUrl> {
if referrer_url.origin() == current_url.origin() {
return strip_url_for_use_as_referrer(referrer_url, false);
}
strip_url_for_use_as_referrer(referrer_url, true)
}
pub fn determine_requests_referrer(
referrer_policy: ReferrerPolicy,
referrer_source: ServoUrl,
current_url: ServoUrl,
) -> Option<ServoUrl> {
match referrer_policy {
ReferrerPolicy::EmptyString | ReferrerPolicy::NoReferrer => None,
ReferrerPolicy::Origin => strip_url_for_use_as_referrer(referrer_source, true),
ReferrerPolicy::UnsafeUrl => strip_url_for_use_as_referrer(referrer_source, false),
ReferrerPolicy::StrictOrigin => strict_origin(referrer_source, current_url),
ReferrerPolicy::StrictOriginWhenCrossOrigin => {
strict_origin_when_cross_origin(referrer_source, current_url)
},
ReferrerPolicy::SameOrigin => same_origin(referrer_source, current_url),
ReferrerPolicy::OriginWhenCrossOrigin => {
origin_when_cross_origin(referrer_source, current_url)
},
ReferrerPolicy::NoReferrerWhenDowngrade => {
no_referrer_when_downgrade(referrer_source, current_url)
},
}
}
fn set_request_cookies(
url: &ServoUrl,
headers: &mut HeaderMap,
cookie_jar: &RwLock<CookieStorage>,
) {
let mut cookie_jar = cookie_jar.write();
cookie_jar.remove_expired_cookies_for_url(url);
if let Some(cookie_list) = cookie_jar.cookies_for_url(url, CookieSource::HTTP) &&
let Ok(cookie_list_header_value) = HeaderValue::from_bytes(cookie_list.as_bytes())
{
headers.insert(header::COOKIE, cookie_list_header_value);
}
}
fn set_cookie_for_url(cookie_jar: &RwLock<CookieStorage>, request: &ServoUrl, cookie_val: &str) {
let mut cookie_jar = cookie_jar.write();
let source = CookieSource::HTTP;
if let Some(cookie) = ServoCookie::from_cookie_string(cookie_val, request, source) {
cookie_jar.push(cookie, request, source);
}
}
fn set_cookies_from_headers(
url: &ServoUrl,
headers: &HeaderMap,
cookie_jar: &RwLock<CookieStorage>,
) {
for cookie in headers.get_all(header::SET_COOKIE) {
let cookie_bytes = cookie.as_bytes();
if !ServoCookie::is_valid_name_or_value(cookie_bytes) {
continue;
}
if let Ok(cookie_str) = std::str::from_utf8(cookie_bytes) {
set_cookie_for_url(cookie_jar, url, cookie_str);
}
}
}
fn build_tls_security_info(handshake: &TlsHandshakeInfo, hsts_enabled: bool) -> TlsSecurityInfo {
let state = if handshake.protocol_version.is_none() || handshake.cipher_suite.is_none() {
TlsSecurityState::Insecure
} else {
TlsSecurityState::Secure
};
TlsSecurityInfo {
state,
weakness_reasons: Vec::new(), protocol_version: handshake.protocol_version.clone(),
cipher_suite: handshake.cipher_suite.clone(),
kea_group_name: handshake.kea_group_name.clone(),
signature_scheme_name: handshake.signature_scheme_name.clone(),
alpn_protocol: handshake.alpn_protocol.clone(),
certificate_chain_der: handshake.certificate_chain_der.clone(),
certificate_transparency: None,
hsts: hsts_enabled,
hpkp: false,
used_ech: handshake.used_ech,
used_delegated_credentials: false,
used_ocsp: false,
used_private_dns: false,
}
}
fn auth_from_cache(
auth_cache: &RwLock<AuthCache>,
origin: &ImmutableOrigin,
) -> Option<Authorization<Basic>> {
if let Some(auth_entry) = auth_cache.read().entries.get(&origin.ascii_serialization()) {
let user_name = &auth_entry.user_name;
let password = &auth_entry.password;
Some(Authorization::basic(user_name, password))
} else {
None
}
}
pub(crate) enum BodyChunk {
Chunk(GenericSharedMemory),
Done,
Error,
}
pub(crate) type BodySink = UnboundedSender<BodyChunk>;
fn request_body_stream_closed_error(action: &str) -> NetworkError {
NetworkError::Crash(format!(
"Request body stream has already been closed while trying to {action}."
))
}
fn log_request_body_stream_closed(action: &str, error: Option<&IpcError>) {
match error {
Some(error) => {
error!("Request body stream has already been closed while trying to {action}: {error}")
},
None => error!("Request body stream has already been closed while trying to {action}."),
}
}
fn log_fetch_terminated_send_failure(terminated_with_error: bool, context: &str) {
warn!(
"Failed to notify request-body stream termination state ({terminated_with_error}) while {context} because the receiver was already dropped."
);
}
pub(crate) const FRAGMENT: &AsciiSet = &CONTROLS.add(b'|').add(b'{').add(b'}');
pub(crate) fn obtain_response_setup_router_callback(
devtools_bytes: StdArc<Mutex<Vec<u8>>>,
chunk_requester: StdArc<Mutex<Option<IpcSender<BodyChunkRequest>>>>,
sink: BodySink,
fetch_terminated: UnboundedSender<bool>,
) -> Result<(), NetworkError> {
let (body_chan, body_port) = ipc::channel().unwrap();
{
let mut lock = chunk_requester.lock();
if let Some(chunk_requester) = lock.as_mut() {
if let Err(error) = chunk_requester.send(BodyChunkRequest::Connect(body_chan)) {
log_request_body_stream_closed("connect to the request body stream", Some(&error));
return Err(request_body_stream_closed_error(
"connect to the request body stream",
));
}
if let Err(error) = chunk_requester.send(BodyChunkRequest::Chunk) {
log_request_body_stream_closed(
"request the first request body chunk",
Some(&error),
);
return Err(request_body_stream_closed_error(
"request the first request body chunk",
));
}
} else {
log_request_body_stream_closed("connect to the request body stream", None);
return Err(request_body_stream_closed_error(
"connect to the request body stream",
));
}
}
let mut sink = Some(sink);
ROUTER.add_typed_route(
body_port,
Box::new(move |message| {
info!("Received message");
let bytes = match message.unwrap() {
BodyChunkResponse::Chunk(bytes) => bytes,
BodyChunkResponse::Done => {
if fetch_terminated.send(false).is_err() {
log_fetch_terminated_send_failure(
false,
"handling request body completion",
);
}
if let Some(sink) = sink.take() {
let _ = sink.send(BodyChunk::Done);
}
return;
},
BodyChunkResponse::Error => {
if fetch_terminated.send(true).is_err() {
log_fetch_terminated_send_failure(
true,
"handling request body stream error",
);
}
if let Some(sink) = sink.take() {
let _ = sink.send(BodyChunk::Error);
}
return;
},
};
devtools_bytes.lock().extend_from_slice(&bytes);
{
let Some(sink) = sink.as_ref() else {
return;
};
let _ = sink.send(BodyChunk::Chunk(bytes));
}
}),
);
Ok(())
}
#[async_recursion]
#[allow(clippy::too_many_arguments)]
pub(crate) async fn http_fetch(
fetch_params: &mut FetchParams,
cache: &mut CorsCache,
cors_flag: bool,
cors_preflight_flag: bool,
authentication_fetch_flag: bool,
target: Target<'async_recursion>,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
*done_chan = None;
let request = &mut fetch_params.request;
let mut response: Option<Response> = None;
if request.service_workers_mode == ServiceWorkersMode::All {
if let Some(ref res) = response {
if (res.response_type == ResponseType::Opaque && request.mode != RequestMode::NoCors) ||
(res.response_type == ResponseType::OpaqueRedirect &&
request.redirect_mode != RedirectMode::Manual) ||
(res.url_list.len() > 1 && request.redirect_mode != RedirectMode::Follow) ||
res.is_network_error()
{
return Response::network_error(NetworkError::ConnectionFailure);
}
}
}
if response.is_none() {
if cors_preflight_flag {
let method_cache_match = cache.match_method(request, request.method.clone());
let method_mismatch = !method_cache_match &&
(!is_cors_safelisted_method(&request.method) || request.use_cors_preflight);
let header_mismatch = request.headers.iter().any(|(name, value)| {
!cache.match_header(request, name) &&
!is_cors_safelisted_request_header(&name, &value)
});
if method_mismatch || header_mismatch {
let preflight_response = cors_preflight_fetch(request, cache, context).await;
if let Some(error) = preflight_response.get_network_error() {
return Response::network_error(error.clone());
}
}
}
if request.redirect_mode == RedirectMode::Follow {
request.service_workers_mode = ServiceWorkersMode::None;
}
let mut fetch_result = http_network_or_cache_fetch(
fetch_params,
authentication_fetch_flag,
cors_flag,
done_chan,
context,
)
.await;
if cors_flag && cors_check(&fetch_params.request, &fetch_result).is_err() {
return Response::network_error(NetworkError::CorsGeneral);
}
if let Err(()) = tao_check(&fetch_params.request, &fetch_result) {
context.timing.inner().mark_timing_check_failed();
}
fetch_result.return_internal = false;
response = Some(fetch_result);
}
let request = &mut fetch_params.request;
let mut response = response.unwrap();
if (request.response_tainting == ResponseTainting::Opaque ||
response.response_type == ResponseType::Opaque) &&
request.client.as_ref().is_some_and(|client| {
cross_origin_resource_policy_check(
&request.origin,
client,
&response,
ForNavigation::No,
) == CrossOriginResourcePolicy::Blocked
})
{
return Response::network_error(NetworkError::CrossOriginResponse);
}
if response
.actual_response()
.status
.try_code()
.is_some_and(is_redirect_status)
{
if response.actual_response().status != StatusCode::SEE_OTHER {
}
response = match request.redirect_mode {
RedirectMode::Error => Response::network_error(NetworkError::RedirectError),
RedirectMode::Manual => {
if request.mode == RequestMode::Navigate {
let location_url =
location_url_for_response(&response, request.current_url().fragment());
response.actual_response_mut().location_url = location_url;
response
} else {
response.to_filtered(ResponseType::OpaqueRedirect)
}
},
RedirectMode::Follow => {
response.return_internal = true;
http_redirect_fetch(
fetch_params,
cache,
response,
cors_flag,
target,
done_chan,
context,
)
.await
},
};
}
response.return_internal = true;
context
.timing
.set_attribute(ResourceAttribute::RedirectCount(
fetch_params.request.redirect_count as u16,
));
response.resource_timing = context.timing.clone();
response
}
fn tao_check(request: &Request, response: &Response) -> Result<(), ()> {
let Origin::Origin(ref request_origin) = request.origin else {
unreachable!("origin cannot be \"client\" at this point");
};
let values: Vec<&str> = response
.headers
.get_all("Timing-Allow-Origin")
.iter()
.map(|header_value| header_value.to_str().unwrap_or(""))
.collect();
if values.contains(&"*") {
return Ok(());
}
if values
.iter()
.any(|header_str| *header_str == request_origin.ascii_serialization())
{
return Ok(());
}
if request.mode == RequestMode::Navigate && request.current_url().origin() != *request_origin {
return Err(());
}
if request.response_tainting == ResponseTainting::Basic {
return Ok(());
}
Err(())
}
struct RedirectEndTimer(Option<ResourceFetchTimingContainer>);
impl RedirectEndTimer {
fn neuter(&mut self) {
self.0 = None;
}
}
impl Drop for RedirectEndTimer {
fn drop(&mut self) {
let RedirectEndTimer(resource_fetch_timing_opt) = self;
resource_fetch_timing_opt.as_ref().map_or((), |t| {
t.set_attribute(ResourceAttribute::RedirectEnd(RedirectEndValue::Zero));
})
}
}
static REQUEST_BODY_HEADER_NAMES: &[HeaderName] = &[
CONTENT_ENCODING,
CONTENT_LANGUAGE,
CONTENT_LOCATION,
CONTENT_TYPE,
];
fn location_url_for_response(
response: &Response,
request_fragment: Option<&str>,
) -> Option<Result<ServoUrl, String>> {
assert!(
response
.actual_response()
.status
.try_code()
.is_some_and(is_redirect_status)
);
let mut location = response
.actual_response()
.headers
.get(header::LOCATION)
.and_then(|header_value| {
HeaderValue::to_str(header_value)
.map(|location_string| {
ServoUrl::parse_with_base(response.actual_response().url(), location_string)
.map_err(|error| error.to_string())
})
.ok()
});
if let Some(Ok(ref mut location)) = location &&
location.fragment().is_none()
{
location.set_fragment(request_fragment);
}
location
}
#[async_recursion]
pub async fn http_redirect_fetch(
fetch_params: &mut FetchParams,
cache: &mut CorsCache,
mut response: Response,
cors_flag: bool,
target: Target<'async_recursion>,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
let mut redirect_end_timer = RedirectEndTimer(Some(context.timing.clone()));
let request = &mut fetch_params.request;
assert!(response.return_internal);
let location_url = location_url_for_response(&response, request.current_url().fragment());
response.actual_response_mut().location_url = location_url.clone();
let location_url = match location_url {
None => return response,
Some(Err(err)) => {
return Response::network_error(NetworkError::ResourceLoadError(
format!("Location URL parse failure: {err}"),
));
},
Some(Ok(url)) if !matches!(url.scheme(), "http" | "https") => {
return Response::network_error(NetworkError::UnsupportedScheme);
},
Some(Ok(url)) => url,
};
context.timing.set_attributes(&[
ResourceAttribute::RedirectStart(RedirectStartValue::FetchStart),
ResourceAttribute::FetchStart,
ResourceAttribute::StartTime(ResourceTimeValue::FetchStart),
ResourceAttribute::StartTime(ResourceTimeValue::RedirectStart),
]);
if request.redirect_count >= 20 {
return Response::network_error(NetworkError::TooManyRedirects);
}
request.redirect_count += 1;
let same_origin = match request.origin {
Origin::Origin(ref origin) => *origin == location_url.origin(),
Origin::Client => panic!(
"Request origin should not be client for {}",
request.current_url()
),
};
let has_credentials = has_credentials(&location_url);
if request.mode == RequestMode::CorsMode && !same_origin && has_credentials {
return Response::network_error(NetworkError::CorsCredentials);
}
if cors_flag && location_url.origin() != request.current_url().origin() {
request.origin = Origin::Origin(ImmutableOrigin::new_opaque());
}
if cors_flag && has_credentials {
return Response::network_error(NetworkError::CorsCredentials);
}
if response.actual_response().status != StatusCode::SEE_OTHER &&
request.body.as_ref().is_some_and(|b| b.source_is_null())
{
return Response::network_error(NetworkError::ConnectionFailure);
}
if response
.actual_response()
.status
.try_code()
.is_some_and(|code| {
((code == StatusCode::MOVED_PERMANENTLY || code == StatusCode::FOUND) &&
request.method == Method::POST) ||
(code == StatusCode::SEE_OTHER &&
request.method != Method::HEAD &&
request.method != Method::GET)
})
{
request.method = Method::GET;
request.body = None;
for name in REQUEST_BODY_HEADER_NAMES {
request.headers.remove(name);
}
}
if location_url.origin() != request.current_url().origin() {
request.headers.remove(AUTHORIZATION);
}
if let Some(body) = request.body.as_mut() {
body.extract_source();
}
request
.url_list
.push(UrlWithBlobClaim::from_url_without_having_claimed_blob(
location_url,
));
set_requests_referrer_policy_on_redirect(request, response.actual_response());
let recursive_flag = request.redirect_mode != RedirectMode::Manual;
let fetch_response = main_fetch(
fetch_params,
cache,
recursive_flag,
target,
done_chan,
context,
)
.await;
context.timing.set_attribute(ResourceAttribute::RedirectEnd(
RedirectEndValue::ResponseEnd,
));
redirect_end_timer.neuter();
fetch_response
}
#[async_recursion]
#[servo_tracing::instrument(skip_all,fields(url=fetch_params.request.url().as_str()))]
async fn http_network_or_cache_fetch(
fetch_params: &mut FetchParams,
authentication_fetch_flag: bool,
cors_flag: bool,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
let http_fetch_params: &mut FetchParams;
let mut fetch_params_copy: FetchParams;
let mut response: Option<Response> = None;
let mut revalidating_flag = false;
let http_request = if fetch_params.request.traversable_for_user_prompts ==
TraversableForUserPrompts::NoTraversable &&
fetch_params.request.redirect_mode == RedirectMode::Error
{
http_fetch_params = fetch_params;
&mut http_fetch_params.request
}
else {
fetch_params_copy =
std::mem::replace(fetch_params, FetchParams::new(fetch_params.request.clone()));
http_fetch_params = &mut fetch_params_copy;
&mut http_fetch_params.request
};
let include_credentials = match http_request.credentials_mode {
CredentialsMode::Include => true,
CredentialsMode::CredentialsSameOrigin
if http_request.response_tainting == ResponseTainting::Basic =>
{
true
},
_ => false,
};
let content_length = http_request
.body
.as_ref()
.and_then(|body| body.len().map(|size| size as u64));
let mut content_length_header_value = None;
if http_request.body.is_none() && matches!(http_request.method, Method::POST | Method::PUT) {
content_length_header_value = Some(0);
}
if let Some(content_length) = content_length {
content_length_header_value = Some(content_length);
};
if let Some(content_length_header_value) = content_length_header_value {
http_request
.headers
.typed_insert(ContentLength(content_length_header_value));
}
if http_request.keep_alive &&
let Some(content_length) = content_length
{
let in_flight_keep_alive_bytes: u64 = context
.in_flight_keep_alive_records
.lock()
.get(
&http_request
.pipeline_id
.expect("Must always set a pipeline ID for keep-alive requests"),
)
.map(|records| {
records
.iter()
.map(|record| {
if record.request_id == http_request.id {
0
} else {
record.keep_alive_body_length
}
})
.sum()
})
.unwrap_or_default();
if content_length + in_flight_keep_alive_bytes > 64 * 1024 {
return Response::network_error(NetworkError::TooManyInFlightKeepAliveRequests);
}
}
match http_request.referrer {
Referrer::ReferrerUrl(ref http_request_referrer) |
Referrer::Client(ref http_request_referrer) => {
if let Ok(referer) = http_request_referrer.as_str().parse::<Referer>() {
http_request.headers.typed_insert(referer);
} else {
error!("Failed to parse {} as referrer", http_request_referrer);
}
},
_ => {},
};
append_a_request_origin_header(http_request);
append_the_fetch_metadata_headers(http_request);
if http_request.initiator == Initiator::Prefetch &&
let Ok(value) = HeaderValue::from_str("prefetch")
{
http_request.headers.insert("Sec-Purpose", value);
}
if !http_request.headers.contains_key(header::USER_AGENT) {
http_request
.headers
.typed_insert::<UserAgent>(context.user_agent.parse().unwrap());
}
append_cache_data_to_headers(http_request);
if http_request.headers.contains_key(header::RANGE) &&
let Ok(value) = HeaderValue::from_str("identity")
{
http_request.headers.insert("Accept-Encoding", value);
}
http_request.headers.remove(header::HOST);
set_default_accept_encoding(&mut http_request.headers);
let current_url = http_request.current_url();
if include_credentials {
set_request_cookies(
¤t_url,
&mut http_request.headers,
&context.state.cookie_jar,
);
if !http_request.headers.contains_key(header::AUTHORIZATION) {
let mut authorization_value = None;
if let Some(basic) = auth_from_cache(&context.state.auth_cache, ¤t_url.origin()) &&
(!http_request.use_url_credentials || !has_credentials(¤t_url))
{
authorization_value = Some(basic);
}
if authentication_fetch_flag &&
authorization_value.is_none() &&
has_credentials(¤t_url)
{
authorization_value = Some(Authorization::basic(
current_url.username(),
current_url.password().unwrap_or(""),
));
}
if let Some(basic) = authorization_value {
http_request.headers.typed_insert(basic);
}
}
}
let should_wait = {
let mut cache_guard = block_for_cache_ready(
context,
http_request,
done_chan,
&mut revalidating_flag,
&mut response,
)
.await;
if response.is_none() {
if http_request.cache_mode == CacheMode::OnlyIfCached {
return Response::network_error(NetworkError::CacheError);
}
drop(cache_guard);
let forward_response =
http_network_fetch(http_fetch_params, include_credentials, done_chan, context)
.await;
let http_request = &mut http_fetch_params.request;
let request_key = CacheKey::new(http_request);
cache_guard = context
.state
.http_cache
.get_or_guard(request_key.clone())
.await;
if forward_response.status.in_range(200..=399) && !http_request.method.is_safe() {
if let Some(guard) = cache_guard.try_as_mut() {
invalidate_cached_resources(guard);
}
context
.state
.http_cache
.invalidate_related_urls(http_request, &forward_response, &request_key)
.await;
}
if revalidating_flag && forward_response.status == StatusCode::NOT_MODIFIED {
*done_chan = None;
if let Some(guard) = cache_guard.try_as_mut() {
response = refresh(http_request, forward_response.clone(), done_chan, guard);
}
if let Some(response) = &mut response {
response.cache_state = CacheState::Validated;
}
}
if response.is_none() {
let forward_response = response.insert(forward_response);
if http_request.cache_mode != CacheMode::NoStore {
cache_guard.insert(http_request, forward_response);
}
}
false
} else {
true
}
};
if should_wait {
wait_for_inflight_requests(done_chan, &mut response).await;
}
let http_request = &mut http_fetch_params.request;
let mut response = response.unwrap();
response.url_list = http_request
.url_list
.iter()
.map(|claimed_url| claimed_url.url())
.collect();
if http_request.headers.contains_key(RANGE) {
response.range_requested = true;
}
response.request_includes_credentials = include_credentials;
if response.status.try_code() == Some(StatusCode::UNAUTHORIZED) &&
!cors_flag &&
include_credentials &&
response.headers.contains_key(WWW_AUTHENTICATE)
{
let request = &mut fetch_params.request;
if request
.body
.as_ref()
.is_some_and(|body| body.source_is_null())
{
return Response::network_error(NetworkError::ConnectionFailure);
}
if !request.use_url_credentials || authentication_fetch_flag {
let Some(credentials) = context
.state
.request_authentication(request, &response)
.await
else {
return response;
};
if let Err(err) = request
.current_url_mut()
.set_username(&credentials.username)
{
error!("error setting username for url: {:?}", err);
return response;
};
if let Err(err) = request
.current_url_mut()
.set_password(Some(&credentials.password))
{
error!("error setting password for url: {:?}", err);
return response;
};
}
*done_chan = None;
response = http_network_or_cache_fetch(
fetch_params,
true,
cors_flag,
done_chan,
context,
)
.await;
}
if response.status == StatusCode::PROXY_AUTHENTICATION_REQUIRED {
let request = &mut fetch_params.request;
if request.traversable_for_user_prompts == TraversableForUserPrompts::NoTraversable {
return Response::network_error(NetworkError::ResourceLoadError(
"Can't find Window object".into(),
));
}
let Some(credentials) = context
.state
.request_authentication(request, &response)
.await
else {
return response;
};
let entry = AuthCacheEntry {
user_name: credentials.username,
password: credentials.password,
};
{
let mut auth_cache = context.state.auth_cache.write();
let key = request.current_url().origin().ascii_serialization();
auth_cache.entries.insert(key, entry);
}
*done_chan = None;
response = http_network_or_cache_fetch(
fetch_params,
false,
cors_flag,
done_chan,
context,
)
.await;
}
if authentication_fetch_flag {
}
response
}
#[servo_tracing::instrument(skip_all)]
async fn block_for_cache_ready<'a>(
context: &'a FetchContext,
http_request: &mut Request,
done_chan: &mut DoneChannel,
revalidating_flag: &mut bool,
response: &mut Option<Response>,
) -> CachedResourcesOrGuard<'a> {
let entry_key = CacheKey::new(http_request);
let guard_result = context.state.http_cache.get_or_guard(entry_key).await;
match guard_result {
CachedResourcesOrGuard::Guard(_) => {
*done_chan = None;
},
CachedResourcesOrGuard::Value(ref cached_resources) => {
let stored_response = construct_response(http_request, done_chan, cached_resources);
if let Some(response_from_cache) = stored_response {
let response_headers = response_from_cache.response.headers.clone();
let validation_status = response_from_cache.validation_status;
let revalidation_guard = response_from_cache.revalidation_guard.clone();
let (cached_response, needs_synchronous_revalidation) =
match (http_request.cache_mode, &http_request.mode) {
(CacheMode::ForceCache, _) => (Some(response_from_cache.response), false),
(CacheMode::OnlyIfCached, &RequestMode::SameOrigin) => {
(Some(response_from_cache.response), false)
},
(CacheMode::OnlyIfCached, _) |
(CacheMode::NoStore, _) |
(CacheMode::Reload, _) => (None, false),
(_, _) => (
Some(response_from_cache.response),
validation_status ==
(ValidationStatus::Stale {
revalidate_in_background: false,
}),
),
};
if needs_synchronous_revalidation {
*revalidating_flag = true;
if let Some(http_date) = response_headers.typed_get::<LastModified>() {
let http_date: SystemTime = http_date.into();
http_request
.headers
.typed_insert(IfModifiedSince::from(http_date));
}
if let Some(entity_tag) = response_headers.get(header::ETAG) {
http_request
.headers
.insert(header::IF_NONE_MATCH, entity_tag.clone());
}
} else {
let revalidate_in_background = validation_status ==
(ValidationStatus::Stale {
revalidate_in_background: true,
});
if revalidate_in_background && cached_response.is_some() {
spawn_stale_while_revalidate(context, http_request, revalidation_guard);
}
*response = cached_response;
if let Some(response) = response {
response.cache_state = CacheState::Local;
}
}
if response.is_none() {
*done_chan = None;
}
}
},
}
guard_result
}
fn spawn_stale_while_revalidate(
context: &FetchContext,
http_request: &Request,
revalidation_guard: StdArc<AtomicBool>,
) {
if revalidation_guard
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return;
}
let mut revalidation_request = http_request.clone();
revalidation_request.cache_mode = CacheMode::NoCache;
revalidation_request.service_workers_mode = ServiceWorkersMode::None;
let context = context.clone();
debug!(
"spawning stale-while-revalidate background revalidation for {:?}",
revalidation_request.current_url()
);
spawn_task(async move {
let mut target = DiscardFetch;
let _ = fetch(revalidation_request, &mut target, &context).await;
revalidation_guard.store(false, Ordering::Release);
});
}
async fn wait_for_inflight_requests(done_chan: &mut DoneChannel, response: &mut Option<Response>) {
if let Some(ref mut ch) = *done_chan {
assert!(response.is_some());
loop {
match ch.1.recv().await {
Some(Data::ContentLength(_)) | Some(Data::Payload(_)) | Some(Data::Error(_)) => {},
Some(Data::Done) => break, Some(Data::Cancelled) => {
break;
},
None => panic!("HTTP cache should always send Done or Cancelled"),
}
}
}
*done_chan = None;
}
#[derive(PartialEq)]
enum CrossOriginResourcePolicy {
Allowed,
Blocked,
}
enum ForNavigation {
#[expect(dead_code)]
Yes,
No,
}
fn cross_origin_resource_policy_check(
origin: &Origin,
request_client: &RequestClient,
response: &Response,
for_navigation: ForNavigation,
) -> CrossOriginResourcePolicy {
let embedder_policy = &request_client.policy_container.embedder_policy;
if cross_origin_resource_policy_internal_check(
origin,
EmbedderPolicyValue::UnsafeNone,
response,
&for_navigation,
) == CrossOriginResourcePolicy::Blocked
{
return CrossOriginResourcePolicy::Blocked;
}
if cross_origin_resource_policy_internal_check(
origin,
embedder_policy.value,
response,
&for_navigation,
) == CrossOriginResourcePolicy::Allowed
{
return CrossOriginResourcePolicy::Allowed;
}
CrossOriginResourcePolicy::Blocked
}
fn cross_origin_resource_policy_internal_check(
origin: &Origin,
embedder_policy_value: EmbedderPolicyValue,
response: &Response,
for_navigation: &ForNavigation,
) -> CrossOriginResourcePolicy {
if let ForNavigation::Yes = for_navigation &&
let EmbedderPolicyValue::UnsafeNone = embedder_policy_value
{
return CrossOriginResourcePolicy::Allowed;
}
let policy = response
.headers
.get(HeaderName::from_static("cross-origin-resource-policy"))
.and_then(|h| h.to_str().ok());
let policy = policy
.filter(|&s| s == "same-origin" || s == "same-site" || s == "cross-origin")
.or(match embedder_policy_value {
EmbedderPolicyValue::UnsafeNone => None,
EmbedderPolicyValue::RequireCorp => Some("same-origin"),
});
match policy {
Some("same-origin") => {
if let Origin::Origin(request_origin) = origin &&
response
.url()
.is_some_and(|url| request_origin == &url.origin())
{
return CrossOriginResourcePolicy::Allowed;
}
CrossOriginResourcePolicy::Blocked
},
Some("same-site") => {
if let Some(response_url) = response.url() {
if let Origin::Origin(request_origin) = origin &&
is_schemelessy_same_site(request_origin, &response_url.origin()) &&
(request_origin.scheme() == Some("https") ||
response_url.scheme() != "https")
{
return CrossOriginResourcePolicy::Allowed;
}
}
CrossOriginResourcePolicy::Blocked
},
_ => CrossOriginResourcePolicy::Allowed,
}
}
struct ResponseEndTimer(Option<ResourceFetchTimingContainer>);
impl ResponseEndTimer {
fn neuter(&mut self) {
self.0 = None;
}
}
impl Drop for ResponseEndTimer {
fn drop(&mut self) {
let ResponseEndTimer(resource_fetch_timing_opt) = self;
resource_fetch_timing_opt.as_ref().map_or((), |t| {
t.set_attribute(ResourceAttribute::ResponseEnd);
})
}
}
#[servo_tracing::instrument(skip_all,fields(url=fetch_params.request.url().as_str()))]
async fn http_network_fetch(
fetch_params: &mut FetchParams,
credentials_flag: bool,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
let mut response_end_timer = ResponseEndTimer(Some(context.timing.clone()));
let request = &mut fetch_params.request;
let url = request.current_url();
let request_id = request.id.0.to_string();
if log_enabled!(log::Level::Info) {
info!("{:?} request for {}", request.method, url);
for header in request.headers.iter() {
debug!(" - {:?}", header);
}
}
let is_xhr = request.destination == Destination::None;
let (fetch_terminated_sender, mut fetch_terminated_receiver) = unbounded_channel();
let body = request.body.as_ref().map(|body| body.clone_stream());
if body.is_none() {
let _ = fetch_terminated_sender.send(false);
}
let browsing_context_id = request.target_webview_id.map(Into::into);
let (res, msg) = match &request.mode {
RequestMode::WebSocket {
protocols,
original_url: _,
} => {
let (resource_event_sender, dom_action_receiver) = {
let mut websocket_chan = context.websocket_chan.as_ref().unwrap().lock();
(
websocket_chan.sender.clone(),
websocket_chan.receiver.take().unwrap(),
)
};
let mut tls_config = create_tls_config(
context.ca_certificates.clone(),
context.ignore_certificate_errors,
context.state.override_manager.clone(),
);
tls_config.set_alpn_http1_only();
let response = match start_websocket(
context.state.clone(),
resource_event_sender,
protocols,
request,
tls_config,
dom_action_receiver,
)
.await
{
Ok(response) => response,
Err(error) => {
return Response::network_error(NetworkError::WebsocketConnectionFailure(
format!("{error:?}"),
));
},
};
let response = response.map(|r| match r {
Some(body) => Full::from(body).map_err(|_| unreachable!()).boxed(),
None => http_body_util::Empty::new()
.map_err(|_| unreachable!())
.boxed(),
});
(Decoder::detect(response, url.is_secure_scheme()), None)
},
_ => {
let (res, msg) = match crate::fetch::bun_bridge::obtain_response_bun(
&url,
&request.method,
&mut request.headers,
body,
&request.pipeline_id,
Some(&request_id),
request.destination,
is_xhr,
browsing_context_id,
context,
fetch_terminated_sender,
)
.await
{
Ok(wrapped_response) => wrapped_response,
Err(error) => return Response::network_error(error),
};
(res, msg)
},
};
if log_enabled!(log::Level::Info) {
debug!("{:?} response for {}", res.version(), url);
for header in res.headers().iter() {
debug!(" - {:?}", header);
}
}
match fetch_terminated_receiver.recv().await {
Some(true) => return Response::network_error(NetworkError::ConnectionFailure),
Some(false) => {},
_ => warn!("Failed to receive confirmation request was streamed without error."),
}
let timing = context.timing.inner().clone();
let mut response = Response::new(url.clone(), timing);
if let Some(handshake_info) = res.extensions().get::<TlsHandshakeInfo>() {
let mut hsts_enabled = url
.host_str()
.is_some_and(|host| context.state.hsts_list.read().is_host_secure(host));
if url.scheme() == "https" &&
let Some(sts) = res.headers().typed_get::<StrictTransportSecurity>()
{
hsts_enabled = sts.max_age().as_secs() > 0;
}
response.tls_security_info = Some(build_tls_security_info(handshake_info, hsts_enabled));
}
let status_text = res
.extensions()
.get::<ReasonPhrase>()
.map(ReasonPhrase::as_bytes)
.or_else(|| res.status().canonical_reason().map(str::as_bytes))
.map(Vec::from)
.unwrap_or_default();
response.status = HttpStatus::new(res.status(), status_text);
info!("got {:?} response for {:?}", res.status(), request.url());
response.headers = res.headers().clone();
response.referrer = request.referrer.to_url().cloned();
response.referrer_policy = request.referrer_policy;
let res_body = response.body.clone();
let (done_sender, done_receiver) = unbounded_channel();
*done_chan = Some((done_sender.clone(), done_receiver));
let devtools_sender = context.devtools_chan.clone();
let cancellation_listener = context.cancellation_listener.clone();
if cancellation_listener.cancelled() {
return Response::network_error(NetworkError::LoadCancelled);
}
*res_body.lock() = ResponseBody::Receiving(vec![]);
let res_body2 = res_body.clone();
if let Some(ref sender) = devtools_sender &&
let Some(m) = msg
{
send_request_to_devtools(m, sender);
}
let done_sender2 = done_sender.clone();
let done_sender3 = done_sender.clone();
let timing_ptr2 = context.timing.clone();
let timing_ptr3 = context.timing.clone();
let devtools_request = request.clone();
let url1 = devtools_request.url();
let url2 = url1.clone();
let status = response.status.clone();
let headers = response.headers.clone();
let devtools_chan = context.devtools_chan.clone();
if let Some(possible_length) = res
.headers()
.get(http::header::CONTENT_LENGTH)
.and_then(|header_value| header_value.to_str().ok())
.and_then(|s| s.parse().ok())
.map(|length| min(length, pref!(network_max_content_length) as usize))
{
let _ = done_sender.send(Data::ContentLength(possible_length));
}
spawn_task(
res.into_body()
.try_fold(res_body, move |res_body, chunk| {
if cancellation_listener.cancelled() {
*res_body.lock() = ResponseBody::Done(vec![]);
let _ = done_sender.send(Data::Cancelled);
return future::ready(Err(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"Fetch aborted",
)));
}
if let ResponseBody::Receiving(ref mut body) = *res_body.lock() {
let bytes = chunk;
body.extend_from_slice(&bytes);
let _ = done_sender.send(Data::Payload(bytes.to_vec()));
}
future::ready(Ok(res_body))
})
.and_then(move |res_body| {
debug!("successfully finished response for {:?}", url1);
let mut body = res_body.lock();
let completed_body = match *body {
ResponseBody::Receiving(ref mut body) => std::mem::take(body),
_ => vec![],
};
let devtools_response_body = completed_body.clone();
*body = ResponseBody::Done(completed_body);
send_response_values_to_devtools(
Some(headers),
status,
Some(devtools_response_body),
CacheState::None,
&devtools_request,
devtools_chan,
);
timing_ptr2.set_attribute(ResourceAttribute::ResponseEnd);
let _ = done_sender2.send(Data::Done);
future::ready(Ok(()))
})
.map_err(move |error| {
if let std::io::ErrorKind::InvalidData = error.kind() {
debug!("Content decompression error for {:?}", url2);
let _ = done_sender3.send(Data::Error(NetworkError::DecompressionError));
let mut body = res_body2.lock();
*body = ResponseBody::Done(vec![]);
}
debug!("finished response for {:?}", url2);
let mut body = res_body2.lock();
let completed_body = match *body {
ResponseBody::Receiving(ref mut body) => std::mem::take(body),
_ => vec![],
};
*body = ResponseBody::Done(completed_body);
timing_ptr3.set_attribute(ResourceAttribute::ResponseEnd);
let _ = done_sender3.send(Data::Done);
}),
);
if credentials_flag {
set_cookies_from_headers(&url, &response.headers, &context.state.cookie_jar);
}
context
.state
.hsts_list
.write()
.update_hsts_list_from_response(&url, &response.headers);
response_end_timer.neuter();
response
}
async fn cors_preflight_fetch(
request: &Request,
cache: &mut CorsCache,
context: &FetchContext,
) -> Response {
let mut preflight = RequestBuilder::new(
request.target_webview_id,
request.current_url_with_blob_claim(),
request.referrer.clone(),
)
.method(Method::OPTIONS)
.origin(match &request.origin {
Origin::Client => {
unreachable!("We shouldn't get Client origin in cors_preflight_fetch.")
},
Origin::Origin(origin) => origin.clone(),
})
.pipeline_id(request.pipeline_id)
.initiator(request.initiator)
.destination(request.destination)
.referrer_policy(request.referrer_policy)
.mode(RequestMode::CorsMode)
.response_tainting(ResponseTainting::CorsTainting)
.policy_container(match &request.policy_container {
RequestPolicyContainer::Client => {
unreachable!("We should have a policy container for request in cors_preflight_fetch")
},
RequestPolicyContainer::PolicyContainer(policy_container) => policy_container.clone(),
})
.url_list(
request
.url_list
.iter()
.map(|claimed_url| claimed_url.url())
.collect(),
)
.build();
preflight
.headers
.insert(ACCEPT, HeaderValue::from_static("*/*"));
preflight
.headers
.typed_insert::<AccessControlRequestMethod>(AccessControlRequestMethod::from(
request.method.clone(),
));
let headers = get_cors_unsafe_header_names(&request.headers);
if !headers.is_empty() {
preflight.headers.insert(
ACCESS_CONTROL_REQUEST_HEADERS,
HeaderValue::from_bytes(itertools::join(headers.iter(), ",").as_bytes())
.unwrap_or(HeaderValue::from_static("")),
);
}
let mut fetch_params = FetchParams::new(preflight);
let response =
http_network_or_cache_fetch(&mut fetch_params, false, false, &mut None, context).await;
if cors_check(request, &response).is_ok() && response.status.code().is_success() {
let mut methods = if response
.headers
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS)
{
match response.headers.typed_get::<AccessControlAllowMethods>() {
Some(methods) => methods.iter().collect(),
None => {
return Response::network_error(NetworkError::CorsAllowMethods);
},
}
} else {
vec![]
};
let header_names = if response
.headers
.contains_key(header::ACCESS_CONTROL_ALLOW_HEADERS)
{
match response.headers.typed_get::<AccessControlAllowHeaders>() {
Some(names) => names.iter().collect(),
None => {
return Response::network_error(NetworkError::CorsAllowHeaders);
},
}
} else {
vec![]
};
debug!(
"CORS check: Allowed methods: {:?}, current method: {:?}",
methods, request.method
);
if methods.is_empty() && request.use_cors_preflight {
methods = vec![request.method.clone()];
}
if methods
.iter()
.all(|method| *method.as_str() != *request.method.as_ref()) &&
!is_cors_safelisted_method(&request.method) &&
(request.credentials_mode == CredentialsMode::Include ||
methods.iter().all(|method| method.as_ref() != "*"))
{
return Response::network_error(NetworkError::CorsMethod);
}
debug!(
"CORS check: Allowed headers: {:?}, current headers: {:?}",
header_names, request.headers
);
if request.headers.iter().any(|(name, _)| {
is_cors_non_wildcard_request_header_name(name) &&
header_names.iter().all(|header_name| header_name != name)
}) {
return Response::network_error(NetworkError::CorsAuthorization);
}
let unsafe_names = get_cors_unsafe_header_names(&request.headers);
let header_names_set: HashSet<&HeaderName> = HashSet::from_iter(header_names.iter());
let header_names_contains_star = header_names
.iter()
.any(|header_name| header_name.as_str() == "*");
for unsafe_name in unsafe_names.iter() {
if !header_names_set.contains(unsafe_name) &&
(request.credentials_mode == CredentialsMode::Include ||
!header_names_contains_star)
{
return Response::network_error(NetworkError::CorsHeaders);
}
}
let max_age: Option<Duration> = response
.headers
.typed_get::<AccessControlMaxAge>()
.map(|acma| acma.into());
let max_age = max_age.unwrap_or(Duration::from_secs(5));
for method in &methods {
cache.match_method_and_update(request, method.clone(), max_age);
}
for header_name in &header_names {
cache.match_header_and_update(request, header_name, max_age);
}
return response;
}
Response::network_error(NetworkError::CorsGeneral)
}
fn cors_check(request: &Request, response: &Response) -> Result<(), ()> {
let Some(origins) =
get_value_from_header_list(ACCESS_CONTROL_ALLOW_ORIGIN.as_str(), &response.headers)
else {
return Err(());
};
let origin = origins.into_iter().map(char::from).collect::<String>();
if request.credentials_mode != CredentialsMode::Include && origin == "*" {
return Ok(());
}
if serialize_request_origin(request).to_string() != origin {
return Err(());
}
if request.credentials_mode != CredentialsMode::Include {
return Ok(());
}
let credentials = response
.headers
.typed_get::<AccessControlAllowCredentials>();
if credentials.is_some() {
return Ok(());
}
Err(())
}
fn has_credentials(url: &ServoUrl) -> bool {
!url.username().is_empty() || url.password().is_some()
}
fn is_no_store_cache(headers: &HeaderMap) -> bool {
headers.contains_key(header::IF_MODIFIED_SINCE) |
headers.contains_key(header::IF_NONE_MATCH) |
headers.contains_key(header::IF_UNMODIFIED_SINCE) |
headers.contains_key(header::IF_MATCH) |
headers.contains_key(header::IF_RANGE)
}
fn is_redirect_status(status: StatusCode) -> bool {
matches!(
status,
StatusCode::MOVED_PERMANENTLY |
StatusCode::FOUND |
StatusCode::SEE_OTHER |
StatusCode::TEMPORARY_REDIRECT |
StatusCode::PERMANENT_REDIRECT
)
}
fn serialize_request_origin(request: &Request) -> headers::Origin {
let Origin::Origin(origin) = &request.origin else {
panic!("origin cannot be \"client\" at this point in time");
};
if request.redirect_taint_for_request() != RedirectTaint::SameOrigin {
return headers::Origin::NULL;
}
serialize_origin(origin)
}
pub fn serialize_origin(origin: &ImmutableOrigin) -> headers::Origin {
match origin {
ImmutableOrigin::Opaque(_) => headers::Origin::NULL,
ImmutableOrigin::Tuple(scheme, host, port) => {
let port = match (scheme.as_ref(), port) {
("http" | "ws", 80) | ("https" | "wss", 443) | ("ftp", 21) => None,
_ => Some(*port),
};
headers::Origin::try_from_parts(scheme, &host.to_string(), port)
.unwrap_or(headers::Origin::NULL)
},
}
}
#[expect(
clippy::collapsible_match,
reason = "The current way follows the spec more closely"
)]
fn append_a_request_origin_header(request: &mut Request) {
let Origin::Origin(request_origin) = &request.origin else {
panic!("origin cannot be \"client\" at this point in time");
};
let mut serialized_origin = serialize_request_origin(request);
if request.response_tainting == ResponseTainting::CorsTainting ||
matches!(request.mode, RequestMode::WebSocket { .. })
{
request.headers.typed_insert(serialized_origin);
}
else if !matches!(request.method, Method::GET | Method::HEAD) {
if request.mode != RequestMode::CorsMode {
match request.referrer_policy {
ReferrerPolicy::NoReferrer => {
serialized_origin = headers::Origin::NULL;
},
ReferrerPolicy::NoReferrerWhenDowngrade |
ReferrerPolicy::StrictOrigin |
ReferrerPolicy::StrictOriginWhenCrossOrigin => {
if let ImmutableOrigin::Tuple(scheme, _, _) = &request_origin &&
scheme == "https" &&
request.current_url().scheme() != "https"
{
serialized_origin = headers::Origin::NULL;
}
},
ReferrerPolicy::SameOrigin => {
if *request_origin != request.current_url().origin() {
serialized_origin = headers::Origin::NULL;
}
},
_ => {
},
};
}
request.headers.typed_insert(serialized_origin);
}
}
fn append_the_fetch_metadata_headers(r: &mut Request) {
if !r.url().is_potentially_trustworthy() {
return;
}
set_the_sec_fetch_dest_header(r);
set_the_sec_fetch_mode_header(r);
set_the_sec_fetch_site_header(r);
set_the_sec_fetch_user_header(r);
}
fn append_cache_data_to_headers(http_request: &mut Request) {
match http_request.cache_mode {
CacheMode::Default if is_no_store_cache(&http_request.headers) => {
http_request.cache_mode = CacheMode::NoStore;
},
CacheMode::NoCache if !http_request.headers.contains_key(header::CACHE_CONTROL) => {
http_request
.headers
.typed_insert(CacheControl::new().with_max_age(Duration::from_secs(0)));
},
CacheMode::Reload | CacheMode::NoStore => {
if !http_request.headers.contains_key(header::PRAGMA) {
http_request.headers.typed_insert(Pragma::no_cache());
}
if !http_request.headers.contains_key(header::CACHE_CONTROL) {
http_request
.headers
.typed_insert(CacheControl::new().with_no_cache());
}
},
_ => {},
}
}
fn set_the_sec_fetch_dest_header(r: &mut Request) {
debug_assert!(r.url().is_potentially_trustworthy());
let header = r.destination;
r.headers.typed_insert(SecFetchDest(header));
}
fn set_the_sec_fetch_mode_header(r: &mut Request) {
debug_assert!(r.url().is_potentially_trustworthy());
let header = &r.mode;
r.headers.typed_insert(SecFetchMode::from(header));
}
fn set_the_sec_fetch_site_header(r: &mut Request) {
let Origin::Origin(request_origin) = &r.origin else {
panic!("request origin cannot be \"client\" at this point")
};
debug_assert!(r.url().is_potentially_trustworthy());
let mut header = SecFetchSite::SameOrigin;
if header != SecFetchSite::None {
for url in &r.url_list {
if url.origin() == *request_origin {
continue;
}
header = SecFetchSite::CrossSite;
if !is_same_site(request_origin, &url.origin()) {
break;
}
header = SecFetchSite::SameSite;
}
}
r.headers.typed_insert(header);
}
fn set_the_sec_fetch_user_header(r: &mut Request) {
debug_assert!(r.url().is_potentially_trustworthy());
if !r.is_navigation_request() {
return;
}
let header = SecFetchUser;
r.headers.typed_insert(header);
}
fn set_requests_referrer_policy_on_redirect(request: &mut Request, response: &Response) {
let referrer_policy: ReferrerPolicy = response
.headers
.typed_get::<headers::ReferrerPolicy>()
.into();
if referrer_policy != ReferrerPolicy::EmptyString {
request.referrer_policy = referrer_policy;
}
}