#![deny(unsafe_code)]
use std::fmt::{self, Debug, Display};
use std::sync::{LazyLock, OnceLock};
use std::thread::{self, JoinHandle};
use content_security_policy::{self as csp};
use cookie::Cookie;
use crossbeam_channel::{Receiver, Sender, unbounded};
use headers::{ContentType, HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader};
use http::{HeaderMap, HeaderValue, StatusCode, header};
use hyper_serde::Serde;
use hyper_util::client::legacy::Error as HyperError;
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::router::ROUTER;
use malloc_size_of::malloc_size_of_is_0;
use malloc_size_of_derive::MallocSizeOf;
use mime::Mime;
use profile_traits::mem::ReportsChan;
use rand::{RngCore, rng};
use request::RequestId;
use rustc_hash::FxHashMap;
use rustls_pki_types::CertificateDer;
use serde::{Deserialize, Serialize};
use servo_base::cross_process_instant::CrossProcessInstant;
use servo_base::generic_channel::{
self, CallbackSetter, GenericCallback, GenericOneshotSender, GenericSend, GenericSender,
SendResult,
};
use servo_base::id::{CookieStoreId, HistoryStateId, PipelineId};
use servo_url::{ImmutableOrigin, ServoUrl};
use crate::fetch::headers::determine_nosniff;
use crate::filemanager_thread::FileManagerThreadMsg;
use crate::http_status::HttpStatus;
use crate::mime_classifier::{ApacheBugFlag, MimeClassifier};
use crate::request::{PreloadId, Request, RequestBuilder};
use crate::response::{HttpsState, Response, ResponseInit};
pub mod blob_url_store;
pub mod filemanager_thread;
pub mod http_status;
pub mod image_cache;
pub mod mime_classifier;
pub mod policy_container;
pub mod pub_domains;
pub mod quality;
pub mod request;
pub mod response;
pub const DOCUMENT_ACCEPT_HEADER_VALUE: HeaderValue =
HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
pub mod fetch {
pub mod headers;
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub enum LoadContext {
Browsing,
Image,
AudioVideo,
Plugin,
Style,
Script,
Font,
TextTrack,
CacheManifest,
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct CustomResponse {
#[ignore_malloc_size_of = "Defined in hyper"]
#[serde(
deserialize_with = "::hyper_serde::deserialize",
serialize_with = "::hyper_serde::serialize"
)]
pub headers: HeaderMap,
#[ignore_malloc_size_of = "Defined in hyper"]
#[serde(
deserialize_with = "::hyper_serde::deserialize",
serialize_with = "::hyper_serde::serialize"
)]
pub raw_status: (StatusCode, String),
pub body: Vec<u8>,
}
impl CustomResponse {
pub fn new(
headers: HeaderMap,
raw_status: (StatusCode, String),
body: Vec<u8>,
) -> CustomResponse {
CustomResponse {
headers,
raw_status,
body,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CustomResponseMediator {
pub response_chan: IpcSender<Option<CustomResponse>>,
pub load_url: ServoUrl,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize)]
pub enum ReferrerPolicy {
EmptyString,
NoReferrer,
NoReferrerWhenDowngrade,
Origin,
SameOrigin,
OriginWhenCrossOrigin,
UnsafeUrl,
StrictOrigin,
#[default]
StrictOriginWhenCrossOrigin,
}
impl ReferrerPolicy {
pub fn from_with_legacy(value: &str) -> Self {
match value.to_ascii_lowercase().as_str() {
"never" => ReferrerPolicy::NoReferrer,
"default" => ReferrerPolicy::StrictOriginWhenCrossOrigin,
"always" => ReferrerPolicy::UnsafeUrl,
"origin-when-crossorigin" => ReferrerPolicy::OriginWhenCrossOrigin,
_ => ReferrerPolicy::from(value),
}
}
pub fn parse_header_for_response(headers: &Option<Serde<HeaderMap>>) -> Self {
headers
.as_ref()
.and_then(|headers| headers.typed_get::<ReferrerPolicyHeader>())
.into()
}
}
impl From<&str> for ReferrerPolicy {
fn from(value: &str) -> Self {
match value.to_ascii_lowercase().as_str() {
"no-referrer" => ReferrerPolicy::NoReferrer,
"no-referrer-when-downgrade" => ReferrerPolicy::NoReferrerWhenDowngrade,
"origin" => ReferrerPolicy::Origin,
"same-origin" => ReferrerPolicy::SameOrigin,
"strict-origin" => ReferrerPolicy::StrictOrigin,
"strict-origin-when-cross-origin" => ReferrerPolicy::StrictOriginWhenCrossOrigin,
"origin-when-cross-origin" => ReferrerPolicy::OriginWhenCrossOrigin,
"unsafe-url" => ReferrerPolicy::UnsafeUrl,
_ => ReferrerPolicy::EmptyString,
}
}
}
impl Display for ReferrerPolicy {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = match self {
ReferrerPolicy::EmptyString => "",
ReferrerPolicy::NoReferrer => "no-referrer",
ReferrerPolicy::NoReferrerWhenDowngrade => "no-referrer-when-downgrade",
ReferrerPolicy::Origin => "origin",
ReferrerPolicy::SameOrigin => "same-origin",
ReferrerPolicy::OriginWhenCrossOrigin => "origin-when-cross-origin",
ReferrerPolicy::UnsafeUrl => "unsafe-url",
ReferrerPolicy::StrictOrigin => "strict-origin",
ReferrerPolicy::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin",
};
write!(formatter, "{string}")
}
}
impl From<Option<ReferrerPolicyHeader>> for ReferrerPolicy {
fn from(header: Option<ReferrerPolicyHeader>) -> Self {
header.map_or(ReferrerPolicy::EmptyString, |policy| match policy {
ReferrerPolicyHeader::NO_REFERRER => ReferrerPolicy::NoReferrer,
ReferrerPolicyHeader::NO_REFERRER_WHEN_DOWNGRADE => {
ReferrerPolicy::NoReferrerWhenDowngrade
},
ReferrerPolicyHeader::SAME_ORIGIN => ReferrerPolicy::SameOrigin,
ReferrerPolicyHeader::ORIGIN => ReferrerPolicy::Origin,
ReferrerPolicyHeader::ORIGIN_WHEN_CROSS_ORIGIN => ReferrerPolicy::OriginWhenCrossOrigin,
ReferrerPolicyHeader::UNSAFE_URL => ReferrerPolicy::UnsafeUrl,
ReferrerPolicyHeader::STRICT_ORIGIN => ReferrerPolicy::StrictOrigin,
ReferrerPolicyHeader::STRICT_ORIGIN_WHEN_CROSS_ORIGIN => {
ReferrerPolicy::StrictOriginWhenCrossOrigin
},
})
}
}
impl From<ReferrerPolicy> for ReferrerPolicyHeader {
fn from(referrer_policy: ReferrerPolicy) -> Self {
match referrer_policy {
ReferrerPolicy::NoReferrer => ReferrerPolicyHeader::NO_REFERRER,
ReferrerPolicy::NoReferrerWhenDowngrade => {
ReferrerPolicyHeader::NO_REFERRER_WHEN_DOWNGRADE
},
ReferrerPolicy::SameOrigin => ReferrerPolicyHeader::SAME_ORIGIN,
ReferrerPolicy::Origin => ReferrerPolicyHeader::ORIGIN,
ReferrerPolicy::OriginWhenCrossOrigin => ReferrerPolicyHeader::ORIGIN_WHEN_CROSS_ORIGIN,
ReferrerPolicy::UnsafeUrl => ReferrerPolicyHeader::UNSAFE_URL,
ReferrerPolicy::StrictOrigin => ReferrerPolicyHeader::STRICT_ORIGIN,
ReferrerPolicy::EmptyString | ReferrerPolicy::StrictOriginWhenCrossOrigin => {
ReferrerPolicyHeader::STRICT_ORIGIN_WHEN_CROSS_ORIGIN
},
}
}
}
#[expect(clippy::large_enum_variant)]
#[derive(Debug, Deserialize, Serialize)]
pub enum FetchResponseMsg {
ProcessRequestBody(RequestId),
ProcessResponse(RequestId, Result<FetchMetadata, NetworkError>),
ProcessResponseChunk(RequestId, DebugVec),
ProcessResponseEOF(RequestId, Result<(), NetworkError>, ResourceFetchTiming),
ProcessCspViolations(RequestId, Vec<csp::Violation>),
}
#[derive(Deserialize, PartialEq, Serialize, MallocSizeOf)]
pub struct DebugVec(pub Vec<u8>);
impl From<Vec<u8>> for DebugVec {
fn from(v: Vec<u8>) -> Self {
Self(v)
}
}
impl std::ops::Deref for DebugVec {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::fmt::Debug for DebugVec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("[...; {}]", self.0.len()))
}
}
impl FetchResponseMsg {
pub fn request_id(&self) -> RequestId {
match self {
FetchResponseMsg::ProcessRequestBody(id) |
FetchResponseMsg::ProcessResponse(id, ..) |
FetchResponseMsg::ProcessResponseChunk(id, ..) |
FetchResponseMsg::ProcessResponseEOF(id, ..) |
FetchResponseMsg::ProcessCspViolations(id, ..) => *id,
}
}
}
pub trait FetchTaskTarget {
fn process_request_body(&mut self, request: &Request);
fn process_response(&mut self, request: &Request, response: &Response);
fn process_response_chunk(&mut self, request: &Request, chunk: Vec<u8>);
fn process_response_eof(&mut self, request: &Request, response: &Response);
fn process_csp_violations(&mut self, request: &Request, violations: Vec<csp::Violation>);
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum FilteredMetadata {
Basic(Metadata),
Cors(Metadata),
Opaque,
OpaqueRedirect(ServoUrl),
}
#[expect(clippy::large_enum_variant)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum FetchMetadata {
Unfiltered(Metadata),
Filtered {
filtered: FilteredMetadata,
unsafe_: Metadata,
},
}
impl FetchMetadata {
pub fn metadata(&self) -> &Metadata {
match self {
Self::Unfiltered(metadata) => metadata,
Self::Filtered { unsafe_, .. } => unsafe_,
}
}
pub fn is_cors_cross_origin(&self) -> bool {
if let Self::Filtered { filtered, .. } = self {
match filtered {
FilteredMetadata::Basic(_) | FilteredMetadata::Cors(_) => false,
FilteredMetadata::Opaque | FilteredMetadata::OpaqueRedirect(_) => true,
}
} else {
false
}
}
}
impl FetchTaskTarget for IpcSender<FetchResponseMsg> {
fn process_request_body(&mut self, request: &Request) {
let _ = self.send(FetchResponseMsg::ProcessRequestBody(request.id));
}
fn process_response(&mut self, request: &Request, response: &Response) {
let _ = self.send(FetchResponseMsg::ProcessResponse(
request.id,
response.metadata(),
));
}
fn process_response_chunk(&mut self, request: &Request, chunk: Vec<u8>) {
let _ = self.send(FetchResponseMsg::ProcessResponseChunk(
request.id,
chunk.into(),
));
}
fn process_response_eof(&mut self, request: &Request, response: &Response) {
let result = response
.get_network_error()
.map_or_else(|| Ok(()), |network_error| Err(network_error.clone()));
let timing = response.get_resource_timing().lock().clone();
let _ = self.send(FetchResponseMsg::ProcessResponseEOF(
request.id, result, timing,
));
}
fn process_csp_violations(&mut self, request: &Request, violations: Vec<csp::Violation>) {
let _ = self.send(FetchResponseMsg::ProcessCspViolations(
request.id, violations,
));
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum TlsSecurityState {
#[default]
Insecure,
Weak,
Broken,
Secure,
}
impl Display for TlsSecurityState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = match self {
TlsSecurityState::Insecure => "insecure",
TlsSecurityState::Weak => "weak",
TlsSecurityState::Broken => "broken",
TlsSecurityState::Secure => "secure",
};
f.write_str(text)
}
}
#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize)]
pub struct TlsSecurityInfo {
#[serde(default)]
pub state: TlsSecurityState,
pub weakness_reasons: Vec<String>,
pub protocol_version: Option<String>,
pub cipher_suite: Option<String>,
pub kea_group_name: Option<String>,
pub signature_scheme_name: Option<String>,
pub alpn_protocol: Option<String>,
pub certificate_chain_der: Vec<Vec<u8>>,
pub certificate_transparency: Option<String>,
pub hsts: bool,
pub hpkp: bool,
pub used_ech: bool,
pub used_delegated_credentials: bool,
pub used_ocsp: bool,
pub used_private_dns: bool,
}
impl FetchTaskTarget for IpcSender<WebSocketNetworkEvent> {
fn process_request_body(&mut self, _: &Request) {}
fn process_response(&mut self, _: &Request, response: &Response) {
if response.is_network_error() {
let _ = self.send(WebSocketNetworkEvent::Fail);
}
}
fn process_response_chunk(&mut self, _: &Request, _: Vec<u8>) {}
fn process_response_eof(&mut self, _: &Request, _: &Response) {}
fn process_csp_violations(&mut self, _: &Request, violations: Vec<csp::Violation>) {
let _ = self.send(WebSocketNetworkEvent::ReportCSPViolations(violations));
}
}
pub struct DiscardFetch;
impl FetchTaskTarget for DiscardFetch {
fn process_request_body(&mut self, _: &Request) {}
fn process_response(&mut self, _: &Request, _: &Response) {}
fn process_response_chunk(&mut self, _: &Request, _: Vec<u8>) {}
fn process_response_eof(&mut self, _: &Request, _: &Response) {}
fn process_csp_violations(&mut self, _: &Request, _: Vec<csp::Violation>) {}
}
pub trait AsyncRuntime: Send {
fn shutdown(&mut self);
}
pub type CoreResourceThread = GenericSender<CoreResourceMsg>;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ResourceThreads {
pub core_thread: CoreResourceThread,
}
impl ResourceThreads {
pub fn new(core_thread: CoreResourceThread) -> ResourceThreads {
ResourceThreads { core_thread }
}
pub fn cache_entries(&self) -> Vec<CacheEntryDescriptor> {
let (sender, receiver) = generic_channel::channel().unwrap();
let _ = self
.core_thread
.send(CoreResourceMsg::GetCacheEntries(sender));
receiver.recv().unwrap()
}
pub fn clear_cache(&self) {
let (sender, receiver) = generic_channel::channel().unwrap();
let _ = self
.core_thread
.send(CoreResourceMsg::ClearCache(Some(sender)));
let _ = receiver.recv();
}
pub fn cookies(&self) -> Vec<SiteDescriptor> {
let (sender, receiver) = generic_channel::channel().unwrap();
let _ = self.core_thread.send(CoreResourceMsg::ListCookies(sender));
receiver.recv().unwrap()
}
pub fn clear_cookies_for_sites(&self, sites: &[&str]) {
let sites = sites.iter().map(|site| site.to_string()).collect();
let (sender, receiver) = generic_channel::channel().unwrap();
let _ = self
.core_thread
.send(CoreResourceMsg::DeleteCookiesForSites(sites, sender));
let _ = receiver.recv();
}
pub fn clear_cookies(&self) {
let (sender, receiver) = ipc::channel().unwrap();
let _ = self
.core_thread
.send(CoreResourceMsg::DeleteCookies(None, Some(sender)));
let _ = receiver.recv();
}
pub fn cookies_for_url(&self, url: ServoUrl, source: CookieSource) -> Vec<Cookie<'static>> {
let (sender, receiver) = generic_channel::channel().unwrap();
let _ = self
.core_thread
.send(CoreResourceMsg::GetCookiesForUrl(url, sender, source));
receiver
.recv()
.unwrap()
.into_iter()
.map(|cookie| cookie.into_inner())
.collect()
}
pub fn set_cookie_for_url(&self, url: ServoUrl, cookie: Cookie<'static>, source: CookieSource) {
let _ = self.core_thread.send(CoreResourceMsg::SetCookieForUrl(
url,
Serde(cookie),
source,
None,
));
}
pub fn set_cookie_for_url_sync(
&self,
url: ServoUrl,
cookie: Cookie<'static>,
source: CookieSource,
) {
let (sender, receiver) = generic_channel::channel().unwrap();
let _ = self.core_thread.send(CoreResourceMsg::SetCookieForUrl(
url,
Serde(cookie),
source,
Some(sender),
));
let _ = receiver.recv();
}
}
impl GenericSend<CoreResourceMsg> for ResourceThreads {
fn send(&self, msg: CoreResourceMsg) -> SendResult {
self.core_thread.send(msg)
}
fn sender(&self) -> GenericSender<CoreResourceMsg> {
self.core_thread.clone()
}
}
malloc_size_of_is_0!(ResourceThreads);
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub enum IncludeSubdomains {
Included,
NotIncluded,
}
#[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
pub enum MessageData {
Text(String),
Binary(Vec<u8>),
}
#[derive(Debug, Deserialize, Serialize, MallocSizeOf)]
pub enum WebSocketDomAction {
SendMessage(MessageData),
Close(Option<u16>, Option<String>),
}
#[derive(Debug, Deserialize, Serialize)]
pub enum WebSocketNetworkEvent {
ReportCSPViolations(Vec<csp::Violation>),
ConnectionEstablished { protocol_in_use: Option<String> },
MessageReceived(MessageData),
Close(Option<u16>, String),
Fail,
}
#[derive(Debug, Deserialize, Serialize)]
pub enum FetchChannels {
ResponseMsg(IpcSender<FetchResponseMsg>),
WebSocket {
event_sender: IpcSender<WebSocketNetworkEvent>,
action_receiver: CallbackSetter<WebSocketDomAction>,
},
Prefetch,
}
#[derive(Debug, Deserialize, Serialize)]
pub enum CoreResourceMsg {
Fetch(RequestBuilder, FetchChannels),
Cancel(Vec<RequestId>),
FetchRedirect(RequestBuilder, ResponseInit, IpcSender<FetchResponseMsg>),
SetCookieForUrl(
ServoUrl,
Serde<Cookie<'static>>,
CookieSource,
Option<GenericSender<()>>,
),
SetCookiesForUrl(ServoUrl, Vec<Serde<Cookie<'static>>>, CookieSource),
SetCookieForUrlAsync(
CookieStoreId,
ServoUrl,
Serde<Cookie<'static>>,
CookieSource,
),
GetCookieStringForUrl(ServoUrl, GenericSender<Option<String>>, CookieSource),
GetCookiesForUrl(
ServoUrl,
GenericSender<Vec<Serde<Cookie<'static>>>>,
CookieSource,
),
GetCookiesDataForUrl(
ServoUrl,
GenericSender<Vec<Serde<Cookie<'static>>>>,
CookieSource,
),
GetCookieDataForUrlAsync(CookieStoreId, ServoUrl, Option<String>),
GetAllCookieDataForUrlAsync(CookieStoreId, ServoUrl, Option<String>),
DeleteCookiesForSites(Vec<String>, GenericSender<()>),
DeleteCookies(Option<ServoUrl>, Option<IpcSender<()>>),
DeleteCookie(ServoUrl, String),
DeleteCookieAsync(CookieStoreId, ServoUrl, String),
NewCookieListener(
CookieStoreId,
GenericCallback<CookieAsyncResponse>,
ServoUrl,
),
RemoveCookieListener(CookieStoreId),
ListCookies(GenericSender<Vec<SiteDescriptor>>),
GetHistoryState(HistoryStateId, GenericSender<Option<Vec<u8>>>),
SetHistoryState(HistoryStateId, Vec<u8>),
RemoveHistoryStates(Vec<HistoryStateId>),
GetCacheEntries(GenericSender<Vec<CacheEntryDescriptor>>),
ClearCache(Option<GenericSender<()>>),
NetworkMediator(IpcSender<CustomResponseMediator>, ImmutableOrigin),
ToFileManager(FileManagerThreadMsg),
StorePreloadedResponse(PreloadId, Response),
TotalSizeOfInFlightKeepAliveRecords(PipelineId, GenericSender<u64>),
Exit(GenericOneshotSender<()>),
CollectMemoryReport(ReportsChan),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SiteDescriptor {
pub name: String,
}
impl SiteDescriptor {
pub fn new(name: String) -> Self {
SiteDescriptor { name }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CacheEntryDescriptor {
pub key: String,
}
impl CacheEntryDescriptor {
pub fn new(key: String) -> Self {
Self { key }
}
}
#[expect(clippy::large_enum_variant)]
enum ToFetchThreadMessage {
Cancel(Vec<RequestId>, CoreResourceThread),
StartFetch(
RequestBuilder,
Option<ResponseInit>,
BoxedFetchCallback,
CoreResourceThread,
),
FetchResponse(FetchResponseMsg),
Exit,
}
pub type BoxedFetchCallback = Box<dyn FnMut(FetchResponseMsg) + Send + 'static>;
struct FetchThread {
active_fetches: FxHashMap<RequestId, BoxedFetchCallback>,
receiver: Receiver<ToFetchThreadMessage>,
to_fetch_sender: IpcSender<FetchResponseMsg>,
}
impl FetchThread {
fn spawn() -> (Sender<ToFetchThreadMessage>, JoinHandle<()>) {
let (sender, receiver) = unbounded();
let (to_fetch_sender, from_fetch_sender) = ipc::channel().unwrap();
let sender_clone = sender.clone();
ROUTER.add_typed_route(
from_fetch_sender,
Box::new(move |message| {
let message: FetchResponseMsg = message.unwrap();
let _ = sender_clone.send(ToFetchThreadMessage::FetchResponse(message));
}),
);
let join_handle = thread::Builder::new()
.name("FetchThread".to_owned())
.spawn(move || {
let mut fetch_thread = FetchThread {
active_fetches: FxHashMap::default(),
receiver,
to_fetch_sender,
};
fetch_thread.run();
})
.expect("Thread spawning failed");
(sender, join_handle)
}
fn run(&mut self) {
loop {
match self.receiver.recv().unwrap() {
ToFetchThreadMessage::StartFetch(
request_builder,
response_init,
callback,
core_resource_thread,
) => {
let request_builder_id = request_builder.id;
let message = match response_init {
Some(response_init) => CoreResourceMsg::FetchRedirect(
request_builder,
response_init,
self.to_fetch_sender.clone(),
),
None => CoreResourceMsg::Fetch(
request_builder,
FetchChannels::ResponseMsg(self.to_fetch_sender.clone()),
),
};
core_resource_thread.send(message).unwrap();
let preexisting_fetch =
self.active_fetches.insert(request_builder_id, callback);
assert!(preexisting_fetch.is_none());
},
ToFetchThreadMessage::FetchResponse(fetch_response_msg) => {
let request_id = fetch_response_msg.request_id();
let fetch_finished =
matches!(fetch_response_msg, FetchResponseMsg::ProcessResponseEOF(..));
self.active_fetches
.get_mut(&request_id)
.expect("Got fetch response for unknown fetch")(
fetch_response_msg
);
if fetch_finished {
self.active_fetches.remove(&request_id);
}
},
ToFetchThreadMessage::Cancel(request_ids, core_resource_thread) => {
let _ = core_resource_thread.send(CoreResourceMsg::Cancel(request_ids));
},
ToFetchThreadMessage::Exit => break,
}
}
}
}
static FETCH_THREAD: OnceLock<Sender<ToFetchThreadMessage>> = OnceLock::new();
pub fn start_fetch_thread() -> JoinHandle<()> {
let (sender, join_handle) = FetchThread::spawn();
FETCH_THREAD
.set(sender)
.expect("Fetch thread should be set only once on start-up");
join_handle
}
pub fn exit_fetch_thread() {
let _ = FETCH_THREAD
.get()
.expect("Fetch thread should always be initialized on start-up")
.send(ToFetchThreadMessage::Exit);
}
pub fn fetch_async(
core_resource_thread: &CoreResourceThread,
request: RequestBuilder,
response_init: Option<ResponseInit>,
callback: BoxedFetchCallback,
) {
let _ = FETCH_THREAD
.get()
.expect("Fetch thread should always be initialized on start-up")
.send(ToFetchThreadMessage::StartFetch(
request,
response_init,
callback,
core_resource_thread.clone(),
));
}
pub fn cancel_async_fetch(request_ids: Vec<RequestId>, core_resource_thread: &CoreResourceThread) {
let _ = FETCH_THREAD
.get()
.expect("Fetch thread should always be initialized on start-up")
.send(ToFetchThreadMessage::Cancel(
request_ids,
core_resource_thread.clone(),
));
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct ResourceCorsData {
pub preflight: bool,
pub origin: ServoUrl,
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct ResourceFetchTiming {
pub domain_lookup_start: Option<CrossProcessInstant>,
pub timing_check_passed: bool,
pub timing_type: ResourceTimingType,
pub redirect_count: u16,
pub request_start: Option<CrossProcessInstant>,
pub secure_connection_start: Option<CrossProcessInstant>,
pub response_start: Option<CrossProcessInstant>,
pub fetch_start: Option<CrossProcessInstant>,
pub response_end: Option<CrossProcessInstant>,
pub redirect_start: Option<CrossProcessInstant>,
pub redirect_end: Option<CrossProcessInstant>,
pub connect_start: Option<CrossProcessInstant>,
pub connect_end: Option<CrossProcessInstant>,
pub start_time: Option<CrossProcessInstant>,
pub preloaded: bool,
}
pub enum RedirectStartValue {
Zero,
FetchStart,
}
pub enum RedirectEndValue {
Zero,
ResponseEnd,
}
pub enum ResourceTimeValue {
Zero,
Now,
FetchStart,
RedirectStart,
}
pub enum ResourceAttribute {
RedirectCount(u16),
DomainLookupStart,
RequestStart,
ResponseStart,
RedirectStart(RedirectStartValue),
RedirectEnd(RedirectEndValue),
FetchStart,
ConnectStart(CrossProcessInstant),
ConnectEnd(CrossProcessInstant),
SecureConnectionStart,
ResponseEnd,
StartTime(ResourceTimeValue),
}
#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)]
pub enum ResourceTimingType {
Resource,
Navigation,
Error,
None,
}
impl ResourceFetchTiming {
pub fn new(timing_type: ResourceTimingType) -> ResourceFetchTiming {
ResourceFetchTiming {
timing_type,
timing_check_passed: true,
domain_lookup_start: None,
redirect_count: 0,
secure_connection_start: None,
request_start: None,
response_start: None,
fetch_start: None,
redirect_start: None,
redirect_end: None,
connect_start: None,
connect_end: None,
response_end: None,
start_time: None,
preloaded: false,
}
}
pub fn set_attribute(&mut self, attribute: ResourceAttribute) {
let should_attribute_always_be_updated = matches!(
attribute,
ResourceAttribute::FetchStart |
ResourceAttribute::ResponseEnd |
ResourceAttribute::StartTime(_)
);
if !self.timing_check_passed && !should_attribute_always_be_updated {
return;
}
let now = Some(CrossProcessInstant::now());
match attribute {
ResourceAttribute::DomainLookupStart => self.domain_lookup_start = now,
ResourceAttribute::RedirectCount(count) => self.redirect_count = count,
ResourceAttribute::RequestStart => self.request_start = now,
ResourceAttribute::ResponseStart => self.response_start = now,
ResourceAttribute::RedirectStart(val) => match val {
RedirectStartValue::Zero => self.redirect_start = None,
RedirectStartValue::FetchStart => {
if self.redirect_start.is_none() {
self.redirect_start = self.fetch_start
}
},
},
ResourceAttribute::RedirectEnd(val) => match val {
RedirectEndValue::Zero => self.redirect_end = None,
RedirectEndValue::ResponseEnd => self.redirect_end = self.response_end,
},
ResourceAttribute::FetchStart => self.fetch_start = now,
ResourceAttribute::ConnectStart(instant) => self.connect_start = Some(instant),
ResourceAttribute::ConnectEnd(instant) => self.connect_end = Some(instant),
ResourceAttribute::SecureConnectionStart => self.secure_connection_start = now,
ResourceAttribute::ResponseEnd => self.response_end = now,
ResourceAttribute::StartTime(val) => match val {
ResourceTimeValue::RedirectStart
if self.redirect_start.is_none() || !self.timing_check_passed => {},
_ => self.start_time = self.get_time_value(val),
},
}
}
fn get_time_value(&self, time: ResourceTimeValue) -> Option<CrossProcessInstant> {
match time {
ResourceTimeValue::Zero => None,
ResourceTimeValue::Now => Some(CrossProcessInstant::now()),
ResourceTimeValue::FetchStart => self.fetch_start,
ResourceTimeValue::RedirectStart => self.redirect_start,
}
}
pub fn mark_timing_check_failed(&mut self) {
self.timing_check_passed = false;
self.domain_lookup_start = None;
self.redirect_count = 0;
self.request_start = None;
self.response_start = None;
self.redirect_start = None;
self.connect_start = None;
self.connect_end = None;
}
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct Metadata {
pub final_url: ServoUrl,
pub location_url: Option<Result<ServoUrl, String>>,
#[ignore_malloc_size_of = "Defined in hyper"]
pub content_type: Option<Serde<ContentType>>,
pub charset: Option<String>,
#[ignore_malloc_size_of = "Defined in hyper"]
pub headers: Option<Serde<HeaderMap>>,
pub status: HttpStatus,
pub https_state: HttpsState,
pub referrer: Option<ServoUrl>,
pub referrer_policy: ReferrerPolicy,
pub timing: Option<ResourceFetchTiming>,
pub redirected: bool,
pub tls_security_info: Option<TlsSecurityInfo>,
}
impl Metadata {
pub fn default(url: ServoUrl) -> Self {
Metadata {
final_url: url,
location_url: None,
content_type: None,
charset: None,
headers: None,
status: HttpStatus::default(),
https_state: HttpsState::None,
referrer: None,
referrer_policy: ReferrerPolicy::EmptyString,
timing: None,
redirected: false,
tls_security_info: None,
}
}
pub fn set_content_type(&mut self, content_type: Option<&Mime>) {
if self.headers.is_none() {
self.headers = Some(Serde(HeaderMap::new()));
}
if let Some(mime) = content_type {
self.headers
.as_mut()
.unwrap()
.typed_insert(ContentType::from(mime.clone()));
if let Some(charset) = mime.get_param(mime::CHARSET) {
self.charset = Some(charset.to_string());
}
self.content_type = Some(Serde(ContentType::from(mime.clone())));
}
}
pub fn set_referrer_policy(&mut self, referrer_policy: ReferrerPolicy) {
if referrer_policy == ReferrerPolicy::EmptyString {
return;
}
if self.headers.is_none() {
self.headers = Some(Serde(HeaderMap::new()));
}
self.referrer_policy = referrer_policy;
self.headers
.as_mut()
.unwrap()
.typed_insert::<ReferrerPolicyHeader>(referrer_policy.into());
}
pub fn resource_content_type_metadata(&self, load_context: LoadContext, data: &[u8]) -> Mime {
let no_sniff = self
.headers
.as_deref()
.is_some_and(determine_nosniff)
.into();
let mime = self
.content_type
.clone()
.map(|content_type| content_type.into_inner().into());
MimeClassifier::default().classify(
load_context,
no_sniff,
ApacheBugFlag::from_content_type(mime.as_ref()),
&mime,
data,
)
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub enum CookieSource {
HTTP,
NonHTTP,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CookieChange {
changed: Vec<Serde<Cookie<'static>>>,
deleted: Vec<Serde<Cookie<'static>>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum CookieData {
Change(CookieChange),
Get(Option<Serde<Cookie<'static>>>),
GetAll(Vec<Serde<Cookie<'static>>>),
Set(Result<(), ()>),
Delete(Result<(), ()>),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CookieAsyncResponse {
pub data: CookieData,
}
#[derive(Clone, Deserialize, Eq, MallocSizeOf, PartialEq, Serialize)]
pub enum NetworkError {
LoadCancelled,
SslValidation(String, Vec<u8>),
Crash(String),
UnsupportedScheme,
CorsGeneral,
CrossOriginResponse,
CorsCredentials,
CorsAllowMethods,
CorsAllowHeaders,
CorsMethod,
CorsAuthorization,
CorsHeaders,
ConnectionFailure,
RedirectError,
TooManyRedirects,
TooManyInFlightKeepAliveRequests,
InvalidMethod,
ResourceLoadError(String),
ContentSecurityPolicy,
Nosniff,
MimeType(String),
SubresourceIntegrity,
MixedContent,
CacheError,
InvalidPort,
WebsocketConnectionFailure(String),
LocalDirectoryError,
PartialResponseToNonRangeRequestError,
ProtocolHandlerSubstitutionError,
BlobURLStoreError(String),
HttpError(String),
DecompressionError,
}
impl fmt::Debug for NetworkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NetworkError::UnsupportedScheme => write!(f, "Unsupported scheme"),
NetworkError::CorsGeneral => write!(f, "CORS check failed"),
NetworkError::CrossOriginResponse => write!(f, "Cross-origin response"),
NetworkError::CorsCredentials => write!(f, "Cross-origin credentials check failed"),
NetworkError::CorsAllowMethods => write!(f, "CORS ACAM check failed"),
NetworkError::CorsAllowHeaders => write!(f, "CORS ACAH check failed"),
NetworkError::CorsMethod => write!(f, "CORS method check failed"),
NetworkError::CorsAuthorization => write!(f, "CORS authorization check failed"),
NetworkError::CorsHeaders => write!(f, "CORS headers check failed"),
NetworkError::ConnectionFailure => write!(f, "Request failed"),
NetworkError::RedirectError => write!(f, "Redirect failed"),
NetworkError::TooManyRedirects => write!(f, "Too many redirects"),
NetworkError::TooManyInFlightKeepAliveRequests => {
write!(f, "Too many in flight keep-alive requests")
},
NetworkError::InvalidMethod => write!(f, "Unexpected method"),
NetworkError::ResourceLoadError(s) => write!(f, "{}", s),
NetworkError::ContentSecurityPolicy => write!(f, "Blocked by Content-Security-Policy"),
NetworkError::Nosniff => write!(f, "Blocked by nosniff"),
NetworkError::MimeType(s) => write!(f, "{}", s),
NetworkError::SubresourceIntegrity => {
write!(f, "Subresource integrity validation failed")
},
NetworkError::MixedContent => write!(f, "Blocked as mixed content"),
NetworkError::CacheError => write!(f, "Couldn't find response in cache"),
NetworkError::InvalidPort => write!(f, "Request attempted on bad port"),
NetworkError::LocalDirectoryError => write!(f, "Local directory access failed"),
NetworkError::LoadCancelled => write!(f, "Load cancelled"),
NetworkError::SslValidation(s, _) => write!(f, "SSL validation error: {}", s),
NetworkError::Crash(s) => write!(f, "Crash: {}", s),
NetworkError::PartialResponseToNonRangeRequestError => write!(
f,
"Refusing to provide partial response from earlier ranged request to API that did not make a range request"
),
NetworkError::ProtocolHandlerSubstitutionError => {
write!(f, "Failed to parse substituted protocol handler url")
},
NetworkError::BlobURLStoreError(s) => write!(f, "Blob URL store error: {}", s),
NetworkError::WebsocketConnectionFailure(s) => {
write!(f, "Websocket connection failure: {}", s)
},
NetworkError::HttpError(s) => write!(f, "HTTP failure: {}", s),
NetworkError::DecompressionError => write!(f, "Decompression error"),
}
}
}
impl NetworkError {
pub fn is_permanent_failure(&self) -> bool {
matches!(
self,
NetworkError::ContentSecurityPolicy |
NetworkError::MixedContent |
NetworkError::SubresourceIntegrity |
NetworkError::Nosniff |
NetworkError::InvalidPort |
NetworkError::CorsGeneral |
NetworkError::CrossOriginResponse |
NetworkError::CorsCredentials |
NetworkError::CorsAllowMethods |
NetworkError::CorsAllowHeaders |
NetworkError::CorsMethod |
NetworkError::CorsAuthorization |
NetworkError::CorsHeaders |
NetworkError::UnsupportedScheme
)
}
pub fn from_hyper_error(error: &HyperError, certificate: Option<CertificateDer>) -> Self {
let error_string = error.to_string();
match certificate {
Some(certificate) => NetworkError::SslValidation(error_string, certificate.to_vec()),
_ => NetworkError::HttpError(error_string),
}
}
}
pub fn trim_http_whitespace(mut slice: &[u8]) -> &[u8] {
const HTTP_WS_BYTES: &[u8] = b"\x09\x0A\x0D\x20";
loop {
match slice.split_first() {
Some((first, remainder)) if HTTP_WS_BYTES.contains(first) => slice = remainder,
_ => break,
}
}
loop {
match slice.split_last() {
Some((last, remainder)) if HTTP_WS_BYTES.contains(last) => slice = remainder,
_ => break,
}
}
slice
}
pub fn http_percent_encode(bytes: &[u8]) -> String {
const HTTP_VALUE: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'%')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b',')
.add(b'/')
.add(b':')
.add(b';')
.add(b'<')
.add(b'-')
.add(b'>')
.add(b'?')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'{')
.add(b'}');
percent_encoding::percent_encode(bytes, HTTP_VALUE).to_string()
}
pub fn get_current_locale() -> &'static (String, HeaderValue) {
static CURRENT_LOCALE: OnceLock<(String, HeaderValue)> = OnceLock::new();
CURRENT_LOCALE.get_or_init(|| {
let locale_override = servo_config::pref!(intl_locale_override);
let locale = if locale_override.is_empty() {
sys_locale::get_locale().unwrap_or_else(|| "en-US".into())
} else {
locale_override
};
let header_value = HeaderValue::from_str(&locale)
.ok()
.unwrap_or_else(|| HeaderValue::from_static("en-US"));
(locale, header_value)
})
}
pub fn set_default_accept_language(headers: &mut HeaderMap) {
if headers.contains_key(header::ACCEPT_LANGUAGE) {
return;
}
headers.insert(header::ACCEPT_LANGUAGE, get_current_locale().1.clone());
}
pub static PRIVILEGED_SECRET: LazyLock<u32> = LazyLock::new(|| rng().next_u32());