use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{io, mem, str};
use base64::Engine as _;
use base64::engine::general_purpose;
use content_security_policy as csp;
use crossbeam_channel::Sender;
use devtools_traits::DevtoolsControlMsg;
use embedder_traits::resources::{self, Resource};
use headers::{AccessControlExposeHeaders, ContentType, HeaderMapExt};
use http::header::{self, HeaderMap, HeaderName, RANGE};
use http::{HeaderValue, Method, StatusCode};
use ipc_channel::ipc::{self, IpcSender};
use log::{debug, trace, warn};
use malloc_size_of_derive::MallocSizeOf;
use mime::{self, Mime};
use net_traits::fetch::headers::{determine_nosniff, extract_mime_type_as_mime};
use net_traits::filemanager_thread::{FileTokenCheck, RelativePos};
use net_traits::http_status::HttpStatus;
use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer};
use net_traits::request::{
BodyChunkRequest, BodyChunkResponse, CredentialsMode, Destination, Initiator,
InsecureRequestsPolicy, Origin, ParserMetadata, RedirectMode, Referrer, Request, RequestBody,
RequestId, RequestMode, ResponseTainting, is_cors_safelisted_method,
is_cors_safelisted_request_header,
};
use net_traits::response::{Response, ResponseBody, ResponseType, TerminationReason};
use net_traits::{
FetchTaskTarget, NetworkError, ReferrerPolicy, ResourceAttribute, ResourceFetchTiming,
ResourceTimeValue, ResourceTimingType, WebSocketDomAction, WebSocketNetworkEvent,
set_default_accept_language,
};
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use rustls_pki_types::CertificateDer;
use serde::{Deserialize, Serialize};
use servo_arc::Arc as ServoArc;
use servo_base::generic_channel::CallbackSetter;
use servo_base::id::PipelineId;
use servo_url::{Host, ImmutableOrigin, ServoUrl};
use tokio::sync::Mutex as TokioMutex;
use tokio::sync::mpsc::{UnboundedReceiver as TokioReceiver, UnboundedSender as TokioSender};
use crate::connector::CACertificates;
use crate::fetch::cors_cache::CorsCache;
use crate::fetch::fetch_params::{
ConsumePreloadedResources, FetchParams, SharedPreloadedResources,
};
use crate::filemanager_thread::FileManager;
use crate::http_loader::{
HttpState, determine_requests_referrer, http_fetch, send_early_httprequest_to_devtools,
send_response_to_devtools, send_security_info_to_devtools, set_default_accept,
};
use crate::protocols::{ProtocolRegistry, is_url_potentially_trustworthy};
use crate::request_interceptor::RequestInterceptor;
use crate::subresource_integrity::is_response_integrity_valid;
pub type Target<'a> = &'a mut (dyn FetchTaskTarget + Send);
#[derive(Clone, Deserialize, Serialize)]
pub enum Data {
Payload(Vec<u8>),
Done,
Cancelled,
Error(NetworkError),
}
pub struct WebSocketChannel {
pub sender: IpcSender<WebSocketNetworkEvent>,
pub receiver: Option<CallbackSetter<WebSocketDomAction>>,
}
impl WebSocketChannel {
pub fn new(
sender: IpcSender<WebSocketNetworkEvent>,
receiver: Option<CallbackSetter<WebSocketDomAction>>,
) -> Self {
Self { sender, receiver }
}
}
#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)]
pub struct InFlightKeepAliveRecord {
pub(crate) request_id: RequestId,
pub(crate) keep_alive_body_length: u64,
}
pub type SharedInflightKeepAliveRecords =
Arc<Mutex<FxHashMap<PipelineId, Vec<InFlightKeepAliveRecord>>>>;
#[derive(Clone)]
pub struct FetchContext {
pub state: Arc<HttpState>,
pub user_agent: String,
pub devtools_chan: Option<Sender<DevtoolsControlMsg>>,
pub filemanager: FileManager,
pub file_token: FileTokenCheck,
pub request_interceptor: Arc<TokioMutex<RequestInterceptor>>,
pub cancellation_listener: Arc<CancellationListener>,
pub timing: ServoArc<Mutex<ResourceFetchTiming>>,
pub protocols: Arc<ProtocolRegistry>,
pub websocket_chan: Option<Arc<Mutex<WebSocketChannel>>>,
pub ca_certificates: CACertificates<'static>,
pub ignore_certificate_errors: bool,
pub preloaded_resources: SharedPreloadedResources,
pub in_flight_keep_alive_records: SharedInflightKeepAliveRecords,
}
#[derive(Default)]
pub struct CancellationListener {
cancelled: AtomicBool,
}
impl CancellationListener {
pub(crate) fn cancelled(&self) -> bool {
self.cancelled.load(Ordering::Relaxed)
}
pub(crate) fn cancel(&self) {
self.cancelled.store(true, Ordering::Relaxed)
}
}
pub(crate) struct AutoRequestBodyStreamCloser {
body: Option<RequestBody>,
}
impl AutoRequestBodyStreamCloser {
pub(crate) fn new(body: Option<&RequestBody>) -> Self {
Self {
body: body.cloned(),
}
}
pub(crate) fn disarm(&mut self) {
self.body = None;
}
}
impl Drop for AutoRequestBodyStreamCloser {
fn drop(&mut self) {
if let Some(body) = self.body.take() {
body.close_stream();
}
}
}
pub(crate) fn transfers_request_body_stream_to_later_manual_redirect(
request: &Request,
response: &Response,
) -> bool {
request.mode == RequestMode::Navigate &&
request.redirect_mode == RedirectMode::Manual &&
request.body.is_some() &&
!response.is_network_error() &&
response
.actual_response()
.status
.try_code()
.is_some_and(|status| status.is_redirection())
}
pub type DoneChannel = Option<(TokioSender<Data>, TokioReceiver<Data>)>;
pub async fn fetch(request: Request, target: Target<'_>, context: &FetchContext) -> Response {
{
let mut timing_guard = context.timing.lock();
timing_guard.set_attribute(ResourceAttribute::FetchStart);
timing_guard.set_attribute(ResourceAttribute::StartTime(ResourceTimeValue::FetchStart));
}
fetch_with_cors_cache(request, &mut CorsCache::default(), target, context).await
}
pub async fn fetch_with_cors_cache(
request: Request,
cache: &mut CorsCache,
target: Target<'_>,
context: &FetchContext,
) -> Response {
let mut fetch_params = FetchParams::new(request);
let mut request_body_stream_closer =
AutoRequestBodyStreamCloser::new(fetch_params.request.body.as_ref());
let request = &mut fetch_params.request;
request.populate_request_from_client();
if
matches!(request.current_url().scheme(), "http" | "https")
&& matches!(request.mode, RequestMode::SameOrigin | RequestMode::CorsMode | RequestMode::NoCors)
&& matches!(request.method, Method::GET)
&& (!request.unsafe_request || request.headers.is_empty())
{
if let Some(client) = request.client.as_ref() {
assert!(request.origin == client.origin);
if let Some(candidate) =
client.consume_preloaded_resource(request, context.preloaded_resources.clone())
{
fetch_params.preload_response_candidate = candidate;
}
}
}
set_default_accept(request);
set_default_accept_language(&mut request.headers);
let should_track_in_flight_record = request.keep_alive && request.is_subresource_request();
let pipeline_id = request.pipeline_id;
if should_track_in_flight_record {
let record = InFlightKeepAliveRecord {
request_id: request.id,
keep_alive_body_length: request.keep_alive_body_length(),
};
let mut in_flight_records = context.in_flight_keep_alive_records.lock();
in_flight_records
.entry(pipeline_id.expect("Must always set a pipeline ID for keep-alive requests"))
.or_default()
.push(record);
};
let request_id = request.id;
let response = main_fetch(&mut fetch_params, cache, false, target, &mut None, context).await;
if transfers_request_body_stream_to_later_manual_redirect(&fetch_params.request, &response) {
request_body_stream_closer.disarm();
}
if should_track_in_flight_record {
context
.in_flight_keep_alive_records
.lock()
.get_mut(&pipeline_id.expect("Must always set a pipeline ID for keep-alive requests"))
.expect("Must always have initialized tracked requests before starting fetch")
.retain(|record| record.request_id != request_id);
}
response
}
pub(crate) fn convert_request_to_csp_request(request: &Request) -> Option<csp::Request> {
let origin = match &request.origin {
Origin::Client => return None,
Origin::Origin(origin) => origin,
};
let csp_request = csp::Request {
url: request.url().into_url(),
current_url: request.current_url().into_url(),
origin: origin.clone().into_url_origin(),
redirect_count: request.redirect_count,
destination: request.destination,
initiator: match request.initiator {
Initiator::Download => csp::Initiator::Download,
Initiator::ImageSet => csp::Initiator::ImageSet,
Initiator::Manifest => csp::Initiator::Manifest,
Initiator::Prefetch => csp::Initiator::Prefetch,
_ => csp::Initiator::None,
},
nonce: request.cryptographic_nonce_metadata.clone(),
integrity_metadata: request.integrity_metadata.clone(),
parser_metadata: match request.parser_metadata {
ParserMetadata::ParserInserted => csp::ParserMetadata::ParserInserted,
ParserMetadata::NotParserInserted => csp::ParserMetadata::NotParserInserted,
ParserMetadata::Default => csp::ParserMetadata::None,
},
};
Some(csp_request)
}
pub fn should_request_be_blocked_by_csp(
csp_request: &csp::Request,
policy_container: &PolicyContainer,
) -> (csp::CheckResult, Vec<csp::Violation>) {
policy_container
.csp_list
.as_ref()
.map(|c| c.should_request_be_blocked(csp_request))
.unwrap_or((csp::CheckResult::Allowed, Vec::new()))
}
pub fn report_violations_for_request_by_csp(
csp_request: &csp::Request,
policy_container: &PolicyContainer,
) -> Vec<csp::Violation> {
policy_container
.csp_list
.as_ref()
.map(|c| c.report_violations_for_request(csp_request))
.unwrap_or_default()
}
fn should_response_be_blocked_by_csp(
csp_request: &csp::Request,
response: &Response,
policy_container: &PolicyContainer,
) -> (csp::CheckResult, Vec<csp::Violation>) {
if response.is_network_error() {
return (csp::CheckResult::Allowed, Vec::new());
}
let csp_response = csp::Response {
url: response
.actual_response()
.url()
.cloned()
.map(|mut url| {
match csp_request.url.scheme() {
"ws" | "wss" => {
url.as_mut_url()
.set_scheme(csp_request.url.scheme())
.expect("failed to set URL scheme");
},
_ => {},
};
url
})
.expect("response must have a url")
.into_url(),
redirect_count: csp_request.redirect_count,
};
policy_container
.csp_list
.as_ref()
.map(|c| c.should_response_to_request_be_blocked(csp_request, &csp_response))
.unwrap_or((csp::CheckResult::Allowed, Vec::new()))
}
pub async fn main_fetch(
fetch_params: &mut FetchParams,
cache: &mut CorsCache,
recursive_flag: bool,
target: Target<'_>,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
let request = &mut fetch_params.request;
send_early_httprequest_to_devtools(request, context);
let mut response = None;
if let Some(ref details) = request.crash {
response = Some(Response::network_error(NetworkError::Crash(
details.clone(),
)));
}
if request.local_urls_only &&
!matches!(
request.current_url().scheme(),
"about" | "blob" | "data" | "filesystem"
)
{
response = Some(Response::network_error(NetworkError::UnsupportedScheme));
}
let policy_container = match &request.policy_container {
RequestPolicyContainer::Client => unreachable!(),
RequestPolicyContainer::PolicyContainer(container) => container.to_owned(),
};
let csp_request = convert_request_to_csp_request(request);
if let Some(csp_request) = csp_request.as_ref() {
let violations = report_violations_for_request_by_csp(csp_request, &policy_container);
if !violations.is_empty() {
target.process_csp_violations(request, violations);
}
};
if should_upgrade_request_to_potentially_trustworthy(request, context) ||
should_upgrade_mixed_content_request(request, &context.protocols)
{
trace!(
"upgrading {} targeting {:?}",
request.current_url(),
request.destination
);
if let Some(new_scheme) = match request.current_url().scheme() {
"http" => Some("https"),
"ws" => Some("wss"),
_ => None,
} {
request
.current_url_mut()
.as_mut_url()
.set_scheme(new_scheme)
.unwrap();
}
} else {
trace!(
"not upgrading {} targeting {:?} with {:?}",
request.current_url(),
request.destination,
request.insecure_requests_policy
);
}
if let Some(csp_request) = csp_request.as_ref() {
let (check_result, violations) =
should_request_be_blocked_by_csp(csp_request, &policy_container);
if !violations.is_empty() {
target.process_csp_violations(request, violations);
}
if check_result == csp::CheckResult::Blocked {
warn!("Request blocked by CSP");
response = Some(Response::network_error(NetworkError::ContentSecurityPolicy))
}
};
if should_request_be_blocked_due_to_a_bad_port(&request.current_url()) {
response = Some(Response::network_error(NetworkError::InvalidPort));
}
if should_request_be_blocked_as_mixed_content(request, &context.protocols) {
response = Some(Response::network_error(NetworkError::MixedContent));
}
if request.referrer_policy == ReferrerPolicy::EmptyString {
request.referrer_policy = policy_container.get_referrer_policy();
}
let referrer_url = match mem::replace(&mut request.referrer, Referrer::NoReferrer) {
Referrer::NoReferrer => None,
Referrer::ReferrerUrl(referrer_source) | Referrer::Client(referrer_source) => {
request.headers.remove(header::REFERER);
determine_requests_referrer(
request.referrer_policy,
referrer_source,
request.current_url(),
)
},
};
request.referrer = referrer_url.map_or(Referrer::NoReferrer, Referrer::ReferrerUrl);
context
.state
.hsts_list
.read()
.apply_hsts_rules(request.current_url_mut());
let current_url = request.current_url();
let current_scheme = current_url.scheme();
context
.request_interceptor
.lock()
.await
.intercept_request(request, &mut response, context)
.await;
let mut response = match response {
Some(res) => res,
None => {
let same_origin = if let Origin::Origin(ref origin) = request.origin {
*origin == current_url.origin()
} else {
false
};
if let Some((response, preload_id)) =
fetch_params.preload_response_candidate.response().await
{
response.get_resource_timing().lock().preloaded = true;
context
.preloaded_resources
.lock()
.unwrap()
.remove(&preload_id);
response
}
else if (same_origin && request.response_tainting == ResponseTainting::Basic) ||
current_scheme == "data" ||
context.protocols.is_fetchable(current_scheme) ||
matches!(
request.mode,
RequestMode::Navigate | RequestMode::WebSocket { .. }
)
{
request.response_tainting = ResponseTainting::Basic;
scheme_fetch(fetch_params, cache, target, done_chan, context).await
} else if request.mode == RequestMode::SameOrigin {
Response::network_error(NetworkError::CrossOriginResponse)
} else if request.mode == RequestMode::NoCors {
if request.redirect_mode != RedirectMode::Follow {
Response::network_error(NetworkError::RedirectError)
} else {
request.response_tainting = ResponseTainting::Opaque;
scheme_fetch(fetch_params, cache, target, done_chan, context).await
}
} else if !matches!(current_scheme, "http" | "https") {
Response::network_error(NetworkError::UnsupportedScheme)
} else if request.use_cors_preflight ||
(request.unsafe_request &&
(!is_cors_safelisted_method(&request.method) ||
request.headers.iter().any(|(name, value)| {
!is_cors_safelisted_request_header(&name, &value)
})))
{
request.response_tainting = ResponseTainting::CorsTainting;
let response = http_fetch(
fetch_params,
cache,
true,
true,
false,
target,
done_chan,
context,
)
.await;
if response.is_network_error() {
}
response
} else {
request.response_tainting = ResponseTainting::CorsTainting;
http_fetch(
fetch_params,
cache,
true,
false,
false,
target,
done_chan,
context,
)
.await
}
},
};
if recursive_flag {
return response;
}
let request = &mut fetch_params.request;
let mut response = if !response.is_network_error() && response.internal_response.is_none() {
if request.response_tainting == ResponseTainting::CorsTainting {
let header_names: Option<Vec<HeaderName>> = response
.headers
.typed_get::<AccessControlExposeHeaders>()
.map(|v| v.iter().collect());
match header_names {
Some(ref list)
if request.credentials_mode != CredentialsMode::Include &&
list.iter().any(|header| header == "*") =>
{
response.cors_exposed_header_name_list = response
.headers
.iter()
.map(|(name, _)| name.as_str().to_owned())
.collect();
},
Some(list) => {
response.cors_exposed_header_name_list =
list.iter().map(|h| h.as_str().to_owned()).collect();
},
_ => (),
}
}
let response_type = match request.response_tainting {
ResponseTainting::Basic => ResponseType::Basic,
ResponseTainting::CorsTainting => ResponseType::Cors,
ResponseTainting::Opaque => ResponseType::Opaque,
};
response.to_filtered(response_type)
} else {
response
};
let internal_error = {
let response_is_network_error = response.is_network_error();
let should_replace_with_nosniff_error = !response_is_network_error &&
should_be_blocked_due_to_nosniff(request.destination, &response.headers);
let should_replace_with_mime_type_error = !response_is_network_error &&
should_be_blocked_due_to_mime_type(request.destination, &response.headers);
let should_replace_with_mixed_content = !response_is_network_error &&
should_response_be_blocked_as_mixed_content(request, &response, &context.protocols);
let should_replace_with_csp_error = csp_request.is_some_and(|csp_request| {
let (check_result, violations) =
should_response_be_blocked_by_csp(&csp_request, &response, &policy_container);
if !violations.is_empty() {
target.process_csp_violations(request, violations);
}
check_result == csp::CheckResult::Blocked
});
let mut network_error_response = response
.get_network_error()
.cloned()
.map(Response::network_error);
let response_type = response.response_type.clone(); let internal_response = if let Some(error_response) = network_error_response.as_mut() {
error_response
} else {
response.actual_response_mut()
};
if internal_response.url_list.is_empty() {
internal_response.url_list.clone_from(&request.url_list)
}
internal_response.redirect_taint = request.redirect_taint_for_request();
let mut blocked_error_response;
let internal_response = if should_replace_with_nosniff_error {
blocked_error_response = Response::network_error(NetworkError::Nosniff);
&blocked_error_response
} else if should_replace_with_mime_type_error {
blocked_error_response =
Response::network_error(NetworkError::MimeType("Blocked by MIME type".into()));
&blocked_error_response
} else if should_replace_with_mixed_content {
blocked_error_response = Response::network_error(NetworkError::MixedContent);
&blocked_error_response
} else if should_replace_with_csp_error {
blocked_error_response = Response::network_error(NetworkError::ContentSecurityPolicy);
&blocked_error_response
} else {
internal_response
};
let internal_response = if !internal_response.is_network_error() &&
response_type == ResponseType::Opaque &&
internal_response.status.code() == StatusCode::PARTIAL_CONTENT &&
internal_response.range_requested &&
!request.headers.contains_key(RANGE)
{
blocked_error_response =
Response::network_error(NetworkError::PartialResponseToNonRangeRequestError);
&blocked_error_response
} else {
internal_response
};
let not_network_error = !response_is_network_error && !internal_response.is_network_error();
if not_network_error &&
(is_null_body_status(&internal_response.status) ||
matches!(request.method, Method::HEAD | Method::CONNECT))
{
let mut body = internal_response.body.lock();
*body = ResponseBody::Empty;
}
internal_response.get_network_error().cloned()
};
let mut response = if let Some(error) = internal_error {
Response::network_error(error)
} else {
response
};
let mut response_loaded = false;
let mut response = if !response.is_network_error() && !request.integrity_metadata.is_empty() {
wait_for_response(request, &mut response, target, done_chan, context).await;
response_loaded = true;
let integrity_metadata = &request.integrity_metadata;
if response.termination_reason.is_none() &&
!is_response_integrity_valid(integrity_metadata, &response)
{
Response::network_error(NetworkError::SubresourceIntegrity)
} else {
response
}
} else {
response
};
if request.synchronous {
target.process_response(request, &response);
if !response_loaded {
wait_for_response(request, &mut response, target, done_chan, context).await;
}
target.process_response_eof(request, &response);
return response;
}
if request.body.is_some() && matches!(current_scheme, "http" | "https") {
target.process_request_body(request);
}
target.process_response(request, &response);
send_response_to_devtools(request, context, &response, None);
send_security_info_to_devtools(request, context, &response);
if !response_loaded {
wait_for_response(request, &mut response, target, done_chan, context).await;
}
target.process_response_eof(request, &response);
send_response_to_devtools(request, context, &response, None);
context
.state
.http_cache
.update_awaiting_consumers(request, &response)
.await;
response
}
async fn wait_for_response(
request: &Request,
response: &mut Response,
target: Target<'_>,
done_chan: &mut DoneChannel,
context: &FetchContext,
) {
if let Some(ref mut ch) = *done_chan {
let mut devtools_body = context.devtools_chan.as_ref().map(|_| Vec::new());
loop {
match ch.1.recv().await {
Some(Data::Payload(vec)) => {
if let Some(body) = devtools_body.as_mut() {
body.extend(&vec);
}
target.process_response_chunk(request, vec);
},
Some(Data::Error(network_error)) => {
if network_error == NetworkError::DecompressionError {
response.termination_reason = Some(TerminationReason::Fatal);
}
response.set_network_error(network_error);
break;
},
Some(Data::Done) => {
send_response_to_devtools(request, context, response, devtools_body);
break;
},
Some(Data::Cancelled) => {
response.aborted.store(true, Ordering::Release);
break;
},
_ => {
panic!("fetch worker should always send Done before terminating");
},
}
}
} else {
match *response.actual_response().body.lock() {
ResponseBody::Done(ref vec) if !vec.is_empty() => {
target.process_response_chunk(request, vec.clone());
if context.devtools_chan.is_some() {
send_response_to_devtools(request, context, response, Some(vec.clone()));
}
},
ResponseBody::Done(_) | ResponseBody::Empty => {},
_ => unreachable!(),
}
}
}
pub enum RangeRequestBounds {
Final(RelativePos),
Pending(u64),
}
impl RangeRequestBounds {
pub fn get_final(&self, len: Option<u64>) -> Result<RelativePos, &'static str> {
match self {
RangeRequestBounds::Final(pos) => {
if let Some(len) = len {
if pos.start <= len as i64 {
return Ok(*pos);
}
}
Err("Tried to process RangeRequestBounds::Final without len")
},
RangeRequestBounds::Pending(offset) => Ok(RelativePos::from_opts(
if let Some(len) = len {
Some((len - u64::min(len, *offset)) as i64)
} else {
Some(0)
},
None,
)),
}
}
}
fn create_blank_reply(url: ServoUrl, timing_type: ResourceTimingType) -> Response {
let mut response = Response::new(url, ResourceFetchTiming::new(timing_type));
response
.headers
.typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
*response.body.lock() = ResponseBody::Done(vec![]);
response.status = HttpStatus::default();
response
}
fn create_about_memory(url: ServoUrl, timing_type: ResourceTimingType) -> Response {
let mut response = Response::new(url, ResourceFetchTiming::new(timing_type));
response
.headers
.typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
*response.body.lock() = ResponseBody::Done(resources::read_bytes(Resource::AboutMemoryHTML));
response.status = HttpStatus::default();
response
}
fn handle_allowcert_request(request: &mut Request, context: &FetchContext) -> io::Result<()> {
let error = |string| Err(io::Error::other(string));
let body = match request.body.as_mut() {
Some(body) => body,
None => return error("No body found"),
};
let stream = body.clone_stream();
let mut stream = stream.lock();
let (body_chan, body_port) = ipc::channel().unwrap();
let Some(chunk_requester) = stream.as_mut() else {
log::error!(
"Could not connect to the request body stream because it has already been closed."
);
return Err(std::io::Error::other("Could not send BodyChunkRequest"));
};
chunk_requester
.send(BodyChunkRequest::Connect(body_chan))
.map_err(|error| {
log::error!(
"Could not connect to the request body stream because it has already been closed: {error}"
);
std::io::Error::other("Could not connect to request body stream")
})?;
chunk_requester
.send(BodyChunkRequest::Chunk)
.map_err(|error| {
log::error!(
"Could not request the first request body chunk because the body stream has already been closed: {error}"
);
std::io::Error::other("Could not request request body chunk")
})?;
let body_bytes = match body_port.recv().ok() {
Some(BodyChunkResponse::Chunk(bytes)) => bytes,
_ => return error("Certificate not sent in a single chunk"),
};
let split_idx = match body_bytes.iter().position(|b| *b == b'&') {
Some(split_idx) => split_idx,
None => return error("Could not find ampersand in data"),
};
let (secret, cert_base64) = body_bytes.split_at(split_idx);
let secret = str::from_utf8(secret).ok().and_then(|s| s.parse().ok());
if secret != Some(*net_traits::PRIVILEGED_SECRET) {
return error("Invalid secret sent. Ignoring request");
}
let cert_bytes = match general_purpose::STANDARD_NO_PAD.decode(&cert_base64[1..]) {
Ok(bytes) => bytes,
Err(_) => return error("Could not decode certificate base64"),
};
context
.state
.override_manager
.add_override(&CertificateDer::from_slice(&cert_bytes).into_owned());
Ok(())
}
async fn scheme_fetch(
fetch_params: &mut FetchParams,
cache: &mut CorsCache,
target: Target<'_>,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
let request = &mut fetch_params.request;
let url = request.current_url();
let scheme = url.scheme();
match scheme {
"about" if url.path() == "blank" => create_blank_reply(url, request.timing_type()),
"about" if url.path() == "memory" => create_about_memory(url, request.timing_type()),
"chrome" if url.path() == "allowcert" => {
if let Err(error) = handle_allowcert_request(request, context) {
warn!("Could not handle allowcert request: {error}");
}
create_blank_reply(url, request.timing_type())
},
"http" | "https" => {
http_fetch(
fetch_params,
cache,
false,
false,
false,
target,
done_chan,
context,
)
.await
},
_ => match context.protocols.get(scheme) {
Some(handler) => handler.load(request, done_chan, context).await,
None => Response::network_error(NetworkError::UnsupportedScheme),
},
}
}
fn is_null_body_status(status: &HttpStatus) -> bool {
matches!(
status.try_code(),
Some(StatusCode::SWITCHING_PROTOCOLS) |
Some(StatusCode::NO_CONTENT) |
Some(StatusCode::RESET_CONTENT) |
Some(StatusCode::NOT_MODIFIED)
)
}
pub fn should_be_blocked_due_to_nosniff(
destination: Destination,
response_headers: &HeaderMap,
) -> bool {
if !determine_nosniff(response_headers) {
return false;
}
let mime_type = extract_mime_type_as_mime(response_headers);
#[inline]
fn is_javascript_mime_type(mime_type: &Mime) -> bool {
let javascript_mime_types: [Mime; 16] = [
"application/ecmascript".parse().unwrap(),
"application/javascript".parse().unwrap(),
"application/x-ecmascript".parse().unwrap(),
"application/x-javascript".parse().unwrap(),
"text/ecmascript".parse().unwrap(),
"text/javascript".parse().unwrap(),
"text/javascript1.0".parse().unwrap(),
"text/javascript1.1".parse().unwrap(),
"text/javascript1.2".parse().unwrap(),
"text/javascript1.3".parse().unwrap(),
"text/javascript1.4".parse().unwrap(),
"text/javascript1.5".parse().unwrap(),
"text/jscript".parse().unwrap(),
"text/livescript".parse().unwrap(),
"text/x-ecmascript".parse().unwrap(),
"text/x-javascript".parse().unwrap(),
];
javascript_mime_types
.iter()
.any(|mime| mime.type_() == mime_type.type_() && mime.subtype() == mime_type.subtype())
}
match mime_type {
Some(ref mime_type) if destination.is_script_like() => !is_javascript_mime_type(mime_type),
Some(ref mime_type) if destination == Destination::Style => {
mime_type.type_() != mime::TEXT && mime_type.subtype() != mime::CSS
},
None if destination == Destination::Style || destination.is_script_like() => true,
_ => false,
}
}
fn should_be_blocked_due_to_mime_type(
destination: Destination,
response_headers: &HeaderMap,
) -> bool {
let mime_type: mime::Mime = match extract_mime_type_as_mime(response_headers) {
Some(mime_type) => mime_type,
None => return false,
};
destination.is_script_like() &&
match mime_type.type_() {
mime::AUDIO | mime::VIDEO | mime::IMAGE => true,
mime::TEXT if mime_type.subtype() == mime::CSV => true,
_ => false,
}
}
pub fn should_request_be_blocked_due_to_a_bad_port(url: &ServoUrl) -> bool {
let is_http_scheme = matches!(url.scheme(), "http" | "https");
let is_bad_port = url.port().is_some_and(is_bad_port);
if is_http_scheme && is_bad_port {
return true;
}
false
}
pub fn should_request_be_blocked_as_mixed_content(
request: &Request,
protocol_registry: &ProtocolRegistry,
) -> bool {
if do_settings_prohibit_mixed_security_contexts(request) ==
MixedSecurityProhibited::NotProhibited
{
return false;
}
if is_url_potentially_trustworthy(protocol_registry, &request.current_url()) {
return false;
}
if request.destination == Destination::Document {
return false;
}
true
}
pub fn should_response_be_blocked_as_mixed_content(
request: &Request,
response: &Response,
protocol_registry: &ProtocolRegistry,
) -> bool {
if do_settings_prohibit_mixed_security_contexts(request) ==
MixedSecurityProhibited::NotProhibited
{
return false;
}
if response
.actual_response()
.url()
.is_some_and(|response_url| is_url_potentially_trustworthy(protocol_registry, response_url))
{
return false;
}
if request.destination == Destination::Document {
return false;
}
true
}
fn is_bad_port(port: u16) -> bool {
static BAD_PORTS: [u16; 78] = [
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101,
102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389,
427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636,
993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 5060, 5061, 6000, 6566, 6665, 6666, 6667,
6668, 6669, 6697, 10080,
];
BAD_PORTS.binary_search(&port).is_ok()
}
pub fn is_form_submission_request(request: &Request) -> bool {
let content_type = request.headers.typed_get::<ContentType>();
content_type.is_some_and(|ct| {
let mime: Mime = ct.into();
mime.type_() == mime::APPLICATION && mime.subtype() == mime::WWW_FORM_URLENCODED
})
}
fn should_upgrade_request_to_potentially_trustworthy(
request: &mut Request,
context: &FetchContext,
) -> bool {
fn should_upgrade_navigation_request(request: &Request) -> bool {
if is_form_submission_request(request) {
return true;
}
if request
.client
.as_ref()
.is_some_and(|client| client.is_nested_browsing_context)
{
return true;
}
false
}
if request.is_navigation_request() {
if !is_url_potentially_trustworthy(&context.protocols, &request.current_url()) ||
request
.current_url()
.host_str()
.is_none_or(|host| context.state.hsts_list.read().is_host_secure(host))
{
debug!("Appending the Upgrade-Insecure-Requests header to request’s header list");
request
.headers
.insert("Upgrade-Insecure-Requests", HeaderValue::from_static("1"));
}
if !should_upgrade_navigation_request(request) {
return false;
}
}
request
.client
.as_ref()
.is_some_and(|client| client.insecure_requests_policy == InsecureRequestsPolicy::Upgrade)
}
#[derive(Debug, PartialEq)]
pub enum MixedSecurityProhibited {
Prohibited,
NotProhibited,
}
fn do_settings_prohibit_mixed_security_contexts(request: &Request) -> MixedSecurityProhibited {
if let Origin::Origin(ref origin) = request.origin {
let is_origin_data_url_worker = matches!(
*origin,
ImmutableOrigin::Opaque(servo_url::OpaqueOrigin::SecureWorkerFromDataUrl(_))
);
if origin.is_potentially_trustworthy() || is_origin_data_url_worker {
return MixedSecurityProhibited::Prohibited;
}
}
if request.has_trustworthy_ancestor_origin {
return MixedSecurityProhibited::Prohibited;
}
MixedSecurityProhibited::NotProhibited
}
fn should_upgrade_mixed_content_request(
request: &Request,
protocol_registry: &ProtocolRegistry,
) -> bool {
let url = request.url();
if is_url_potentially_trustworthy(protocol_registry, &url) {
return false;
}
match url.host() {
Some(Host::Ipv4(_)) | Some(Host::Ipv6(_)) => return false,
_ => (),
}
if do_settings_prohibit_mixed_security_contexts(request) ==
MixedSecurityProhibited::NotProhibited
{
return false;
}
if !matches!(
request.destination,
Destination::Audio | Destination::Image | Destination::Video
) {
return false;
}
if request.destination == Destination::Image && request.initiator == Initiator::ImageSet {
return false;
}
true
}