servo/components/net/http_loader.rs

1693 lines
55 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::connector::{create_http_client, Connector};
use crate::cookie;
use crate::cookie_storage::CookieStorage;
use crate::decoder::Decoder;
use crate::fetch::cors_cache::CorsCache;
use crate::fetch::methods::{
is_cors_safelisted_method, is_cors_safelisted_request_header, main_fetch,
};
use crate::fetch::methods::{Data, DoneChannel, FetchContext, Target};
use crate::hsts::HstsList;
use crate::http_cache::HttpCache;
use crate::resource_thread::AuthCache;
use crossbeam_channel::{unbounded, Sender};
use devtools_traits::{
ChromeToDevtoolsControlMsg, DevtoolsControlMsg, HttpRequest as DevtoolsHttpRequest,
};
use devtools_traits::{HttpResponse as DevtoolsHttpResponse, NetworkEvent};
use headers::authorization::Basic;
use headers::{AccessControlAllowCredentials, AccessControlAllowHeaders, HeaderMapExt};
use headers::{
AccessControlAllowMethods, AccessControlRequestHeaders, AccessControlRequestMethod,
Authorization,
};
use headers::{AccessControlAllowOrigin, AccessControlMaxAge};
use headers::{CacheControl, ContentEncoding, ContentLength};
use headers::{
Host, IfModifiedSince, LastModified, Origin as HyperOrigin, Pragma, Referer, UserAgent,
};
use http::header::{self, HeaderName, HeaderValue};
use http::uri::Authority;
use http::{HeaderMap, Request as HyperRequest};
use hyper::{Body, Client, Method, Response as HyperResponse, StatusCode};
use hyper_serde::Serde;
use msg::constellation_msg::{HistoryStateId, PipelineId};
use net_traits::quality::{quality_to_value, Quality, QualityItem};
use net_traits::request::{CacheMode, CredentialsMode, Destination, Origin};
use net_traits::request::{RedirectMode, Referrer, Request, RequestMode};
use net_traits::request::{ResponseTainting, ServiceWorkersMode};
use net_traits::response::{HttpsState, Response, ResponseBody, ResponseType};
use net_traits::{CookieSource, FetchMetadata, NetworkError, ReferrerPolicy};
use net_traits::{RedirectEndValue, RedirectStartValue, ResourceAttribute, ResourceFetchTiming};
use openssl::ssl::SslConnectorBuilder;
use servo_url::{ImmutableOrigin, ServoUrl};
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::iter::FromIterator;
use std::mem;
use std::ops::Deref;
use std::str::FromStr;
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, SystemTime};
use time::{self, Tm};
use tokio::prelude::{future, Future, Stream};
use tokio::runtime::Runtime;
lazy_static! {
pub static ref HANDLE: Mutex<Runtime> = { Mutex::new(Runtime::new().unwrap()) };
}
pub struct HttpState {
pub hsts_list: RwLock<HstsList>,
pub cookie_jar: RwLock<CookieStorage>,
pub http_cache: RwLock<HttpCache>,
pub auth_cache: RwLock<AuthCache>,
pub history_states: RwLock<HashMap<HistoryStateId, Vec<u8>>>,
pub client: Client<Connector, Body>,
}
impl HttpState {
pub fn new(ssl_connector_builder: SslConnectorBuilder) -> HttpState {
HttpState {
hsts_list: RwLock::new(HstsList::new()),
cookie_jar: RwLock::new(CookieStorage::new(150)),
auth_cache: RwLock::new(AuthCache::new()),
history_states: RwLock::new(HashMap::new()),
http_cache: RwLock::new(HttpCache::new()),
client: create_http_client(ssl_connector_builder, HANDLE.lock().unwrap().executor()),
}
}
}
fn precise_time_ms() -> u64 {
time::precise_time_ns() / (1000 * 1000)
}
// Step 3 of https://fetch.spec.whatwg.org/#concept-fetch.
pub fn set_default_accept(destination: Destination, headers: &mut HeaderMap) {
if headers.contains_key(header::ACCEPT) {
return;
}
let value = match destination {
// Step 3.2.
Destination::Document => vec![
QualityItem::new(mime::TEXT_HTML, Quality::from_u16(1000)),
QualityItem::new(
"application/xhtml+xml".parse().unwrap(),
Quality::from_u16(1000),
),
QualityItem::new("application/xml".parse().unwrap(), Quality::from_u16(900)),
QualityItem::new(mime::STAR_STAR, Quality::from_u16(800)),
],
// Step 3.3.
Destination::Image => vec![
QualityItem::new(mime::IMAGE_PNG, Quality::from_u16(1000)),
QualityItem::new(mime::IMAGE_SVG, Quality::from_u16(1000)),
QualityItem::new(mime::IMAGE_STAR, Quality::from_u16(800)),
QualityItem::new(mime::STAR_STAR, Quality::from_u16(500)),
],
// Step 3.3.
Destination::Style => vec![
QualityItem::new(mime::TEXT_CSS, Quality::from_u16(1000)),
QualityItem::new(mime::STAR_STAR, Quality::from_u16(100)),
],
// Step 3.1.
_ => vec![QualityItem::new(mime::STAR_STAR, Quality::from_u16(1000))],
};
// Step 3.4.
// TODO(eijebong): Change this once typed headers are done
headers.insert(header::ACCEPT, quality_to_value(value));
}
fn set_default_accept_encoding(headers: &mut HeaderMap) {
if headers.contains_key(header::ACCEPT_ENCODING) {
return;
}
// TODO(eijebong): Change this once typed headers are done
headers.insert(
header::ACCEPT_ENCODING,
HeaderValue::from_static("gzip, deflate, br"),
);
}
pub fn set_default_accept_language(headers: &mut HeaderMap) {
if headers.contains_key(header::ACCEPT_LANGUAGE) {
return;
}
// TODO(eijebong): Change this once typed headers are done
headers.insert(
header::ACCEPT_LANGUAGE,
HeaderValue::from_static("en-US, en; q=0.5"),
);
}
/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-state-no-referrer-when-downgrade>
fn no_referrer_when_downgrade_header(referrer_url: ServoUrl, url: ServoUrl) -> Option<ServoUrl> {
if referrer_url.scheme() == "https" && url.scheme() != "https" {
return None;
}
return strip_url(referrer_url, false);
}
/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-strict-origin>
fn strict_origin(referrer_url: ServoUrl, url: ServoUrl) -> Option<ServoUrl> {
if referrer_url.scheme() == "https" && url.scheme() != "https" {
return None;
}
strip_url(referrer_url, true)
}
/// <https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-strict-origin-when-cross-origin>
fn strict_origin_when_cross_origin(referrer_url: ServoUrl, url: ServoUrl) -> Option<ServoUrl> {
if referrer_url.scheme() == "https" && url.scheme() != "https" {
return None;
}
let cross_origin = referrer_url.origin() != url.origin();
strip_url(referrer_url, cross_origin)
}
/// <https://w3c.github.io/webappsec-referrer-policy/#strip-url>
fn strip_url(mut referrer_url: ServoUrl, origin_only: bool) -> Option<ServoUrl> {
if referrer_url.scheme() == "https" || referrer_url.scheme() == "http" {
{
let referrer = referrer_url.as_mut_url();
referrer.set_username("").unwrap();
referrer.set_password(None).unwrap();
referrer.set_fragment(None);
if origin_only {
referrer.set_path("");
referrer.set_query(None);
}
}
return Some(referrer_url);
}
return None;
}
/// <https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer>
/// Steps 4-6.
pub fn determine_request_referrer(
headers: &mut HeaderMap,
referrer_policy: ReferrerPolicy,
referrer_source: ServoUrl,
current_url: ServoUrl,
) -> Option<ServoUrl> {
assert!(!headers.contains_key(header::REFERER));
// FIXME(#14505): this does not seem to be the correct way of checking for
// same-origin requests.
let cross_origin = referrer_source.origin() != current_url.origin();
// FIXME(#14506): some of these cases are expected to consider whether the
// request's client is "TLS-protected", whatever that means.
match referrer_policy {
ReferrerPolicy::NoReferrer => None,
ReferrerPolicy::Origin => strip_url(referrer_source, true),
ReferrerPolicy::SameOrigin => {
if cross_origin {
None
} else {
strip_url(referrer_source, false)
}
},
ReferrerPolicy::UnsafeUrl => strip_url(referrer_source, false),
ReferrerPolicy::OriginWhenCrossOrigin => strip_url(referrer_source, cross_origin),
ReferrerPolicy::StrictOrigin => strict_origin(referrer_source, current_url),
ReferrerPolicy::StrictOriginWhenCrossOrigin => {
strict_origin_when_cross_origin(referrer_source, current_url)
},
ReferrerPolicy::NoReferrerWhenDowngrade => {
no_referrer_when_downgrade_header(referrer_source, current_url)
},
}
}
pub fn set_request_cookies(
url: &ServoUrl,
headers: &mut HeaderMap,
cookie_jar: &RwLock<CookieStorage>,
) {
let mut cookie_jar = cookie_jar.write().unwrap();
if let Some(cookie_list) = cookie_jar.cookies_for_url(url, CookieSource::HTTP) {
headers.insert(
header::COOKIE,
HeaderValue::from_bytes(cookie_list.as_bytes()).unwrap(),
);
}
}
fn set_cookie_for_url(cookie_jar: &RwLock<CookieStorage>, request: &ServoUrl, cookie_val: &str) {
let mut cookie_jar = cookie_jar.write().unwrap();
let source = CookieSource::HTTP;
if let Some(cookie) = cookie::Cookie::from_cookie_string(cookie_val.into(), 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) {
if let Ok(cookie_str) = cookie.to_str() {
set_cookie_for_url(&cookie_jar, &url, &cookie_str);
}
}
}
fn prepare_devtools_request(
request_id: String,
url: ServoUrl,
method: Method,
headers: HeaderMap,
body: Option<Vec<u8>>,
pipeline_id: PipelineId,
now: Tm,
connect_time: u64,
send_time: u64,
is_xhr: bool,
) -> ChromeToDevtoolsControlMsg {
let request = DevtoolsHttpRequest {
url: url,
method: method,
headers: headers,
body: body,
pipeline_id: pipeline_id,
startedDateTime: now,
timeStamp: now.to_timespec().sec,
connect_time: connect_time,
send_time: send_time,
is_xhr: is_xhr,
};
let net_event = NetworkEvent::HttpRequest(request);
ChromeToDevtoolsControlMsg::NetworkEvent(request_id, net_event)
}
fn send_request_to_devtools(
msg: ChromeToDevtoolsControlMsg,
devtools_chan: &Sender<DevtoolsControlMsg>,
) {
devtools_chan
.send(DevtoolsControlMsg::FromChrome(msg))
.unwrap();
}
fn send_response_to_devtools(
devtools_chan: &Sender<DevtoolsControlMsg>,
request_id: String,
headers: Option<HeaderMap>,
status: Option<(u16, Vec<u8>)>,
pipeline_id: PipelineId,
) {
let response = DevtoolsHttpResponse {
headers: headers,
status: status,
body: None,
pipeline_id: pipeline_id,
};
let net_event_response = NetworkEvent::HttpResponse(response);
let msg = ChromeToDevtoolsControlMsg::NetworkEvent(request_id, net_event_response);
let _ = devtools_chan.send(DevtoolsControlMsg::FromChrome(msg));
}
fn auth_from_cache(
auth_cache: &RwLock<AuthCache>,
origin: &ImmutableOrigin,
) -> Option<Authorization<Basic>> {
if let Some(ref auth_entry) = auth_cache
.read()
.unwrap()
.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
}
}
fn obtain_response(
client: &Client<Connector, Body>,
url: &ServoUrl,
method: &Method,
request_headers: &HeaderMap,
data: &Option<Vec<u8>>,
load_data_method: &Method,
pipeline_id: &Option<PipelineId>,
iters: u32,
request_id: Option<&str>,
is_xhr: bool,
context: &FetchContext,
) -> Box<
dyn Future<
Item = (HyperResponse<Decoder>, Option<ChromeToDevtoolsControlMsg>),
Error = NetworkError,
>,
> {
let mut headers = request_headers.clone();
// Avoid automatically sending request body if a redirect has occurred.
//
// TODO - This is the wrong behaviour according to the RFC. However, I'm not
// sure how much "correctness" vs. real-world is important in this case.
//
// https://tools.ietf.org/html/rfc7231#section-6.4
let is_redirected_request = iters != 1;
let request_body;
match data {
&Some(ref d) if !is_redirected_request => {
headers.typed_insert(ContentLength(d.len() as u64));
request_body = d.clone();
},
_ => {
if *load_data_method != Method::GET && *load_data_method != Method::HEAD {
headers.typed_insert(ContentLength(0))
}
request_body = vec![];
},
}
context
.timing
.lock()
.unwrap()
.set_attribute(ResourceAttribute::DomainLookupStart);
// TODO(#21261) connect_start: set if a persistent connection is *not* used and the last non-redirected
// fetch passes the timing allow check
let connect_start = precise_time_ms();
context
.timing
.lock()
.unwrap()
.set_attribute(ResourceAttribute::ConnectStart(connect_start));
// https://url.spec.whatwg.org/#percent-encoded-bytes
let request = HyperRequest::builder()
.method(method)
.uri(
url.clone()
.into_url()
.as_ref()
.replace("|", "%7C")
.replace("{", "%7B")
.replace("}", "%7D"),
)
.body(request_body.clone().into());
let mut request = match request {
Ok(request) => request,
Err(e) => return Box::new(future::result(Err(NetworkError::from_http_error(&e)))),
};
*request.headers_mut() = headers.clone();
let connect_end = precise_time_ms();
context
.timing
.lock()
.unwrap()
.set_attribute(ResourceAttribute::ConnectEnd(connect_end));
let request_id = request_id.map(|v| v.to_owned());
let pipeline_id = pipeline_id.clone();
let closure_url = url.clone();
let method = method.clone();
let send_start = precise_time_ms();
Box::new(
client
.request(request)
.and_then(move |res| {
let send_end = precise_time_ms();
// TODO(#21271) response_start: immediately after receiving first byte of response
let msg = if let Some(request_id) = request_id {
if let Some(pipeline_id) = pipeline_id {
Some(prepare_devtools_request(
request_id,
closure_url,
method.clone(),
headers,
Some(request_body.clone()),
pipeline_id,
time::now(),
connect_end - connect_start,
send_end - send_start,
is_xhr,
))
// TODO: ^This is not right, connect_start is taken before contructing the
// request and connect_end at the end of it. send_start is takend before the
// connection too. I'm not sure it's currently possible to get the time at the
// point between the connection and the start of a request.
} else {
debug!("Not notifying devtools (no pipeline_id)");
None
}
} else {
debug!("Not notifying devtools (no request_id)");
None
};
Ok((Decoder::detect(res), msg))
})
.map_err(move |e| NetworkError::from_hyper_error(&e)),
)
}
/// [HTTP fetch](https://fetch.spec.whatwg.org#http-fetch)
pub fn http_fetch(
request: &mut Request,
cache: &mut CorsCache,
cors_flag: bool,
cors_preflight_flag: bool,
authentication_fetch_flag: bool,
target: Target,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
// This is a new async fetch, reset the channel we are waiting on
*done_chan = None;
// Step 1
let mut response: Option<Response> = None;
// Step 2
// nothing to do, since actual_response is a function on response
// Step 3
if request.service_workers_mode == ServiceWorkersMode::All {
// TODO: Substep 1
// Set response to the result of invoking handle fetch for request.
// Substep 2
if let Some(ref res) = response {
// Subsubstep 1
// TODO: transmit body for request
// Subsubstep 2
// nothing to do, since actual_response is a function on response
// Subsubstep 3
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::Internal("Request failed".into()));
}
// Subsubstep 4
// TODO: set response's CSP list on actual_response
}
}
// Step 4
if response.is_none() {
// Substep 1
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)
});
// Sub-substep 1
if method_mismatch || header_mismatch {
let preflight_result = cors_preflight_fetch(&request, cache, context);
// Sub-substep 2
if let Some(e) = preflight_result.get_network_error() {
return Response::network_error(e.clone());
}
}
}
// Substep 2
if request.redirect_mode == RedirectMode::Follow {
request.service_workers_mode = ServiceWorkersMode::None;
}
// Generally, we use a persistent connection, so we will also set other PerformanceResourceTiming
// attributes to this as well (domain_lookup_start, domain_lookup_end, connect_start, connect_end,
// secure_connection_start)
// TODO(#21254) also set startTime equal to either fetch_start or redirect_start
// (https://w3c.github.io/resource-timing/#dfn-starttime)
context
.timing
.lock()
.unwrap()
.set_attribute(ResourceAttribute::RequestStart);
let mut fetch_result = http_network_or_cache_fetch(
request,
authentication_fetch_flag,
cors_flag,
done_chan,
context,
);
// Substep 4
if cors_flag && cors_check(&request, &fetch_result).is_err() {
return Response::network_error(NetworkError::Internal("CORS check failed".into()));
}
fetch_result.return_internal = false;
response = Some(fetch_result);
}
// response is guaranteed to be something by now
let mut response = response.unwrap();
// Step 5
if response
.actual_response()
.status
.as_ref()
.map_or(false, is_redirect_status)
{
// Substep 1.
if response
.actual_response()
.status
.as_ref()
.map_or(true, |s| s.0 != StatusCode::SEE_OTHER)
{
// TODO: send RST_STREAM frame
}
// Substep 2-3.
let location = response
.actual_response()
.headers
.get(header::LOCATION)
.and_then(|v| {
HeaderValue::to_str(v)
.map(|l| {
ServoUrl::parse_with_base(response.actual_response().url(), &l)
.map_err(|err| err.description().into())
})
.ok()
});
// Substep 4.
response.actual_response_mut().location_url = location;
// Substep 5.
response = match request.redirect_mode {
RedirectMode::Error => {
Response::network_error(NetworkError::Internal("Redirect mode error".into()))
},
RedirectMode::Manual => response.to_filtered(ResponseType::OpaqueRedirect),
RedirectMode::Follow => {
// set back to default
response.return_internal = true;
http_redirect_fetch(
request, cache, response, cors_flag, target, done_chan, context,
)
},
};
}
// set back to default
response.return_internal = true;
context
.timing
.lock()
.unwrap()
.set_attribute(ResourceAttribute::RedirectCount(
request.redirect_count as u16,
));
response.resource_timing = context.timing.lock().unwrap().clone();
// Step 6
response
}
// Convenience struct that implements Drop, for setting redirectEnd on function return
struct RedirectEndTimer(Option<Arc<Mutex<ResourceFetchTiming>>>);
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.lock()
.unwrap()
.set_attribute(ResourceAttribute::RedirectEnd(RedirectEndValue::Zero));
})
}
}
/// [HTTP redirect fetch](https://fetch.spec.whatwg.org#http-redirect-fetch)
pub fn http_redirect_fetch(
request: &mut Request,
cache: &mut CorsCache,
response: Response,
cors_flag: bool,
target: Target,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
let mut redirect_end_timer = RedirectEndTimer(Some(context.timing.clone()));
// Step 1
assert!(response.return_internal);
let location_url = response.actual_response().location_url.clone();
let location_url = match location_url {
// Step 2
None => return response,
// Step 3
Some(Err(err)) => {
return Response::network_error(NetworkError::Internal(
"Location URL parse failure: ".to_owned() + &err,
));
},
// Step 4
Some(Ok(ref url)) if !matches!(url.scheme(), "http" | "https") => {
return Response::network_error(NetworkError::Internal(
"Location URL not an HTTP(S) scheme".into(),
));
},
Some(Ok(url)) => url,
};
// Step 1 of https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-fetchstart
// TODO: check origin and timing allow check
context
.timing
.lock()
.unwrap()
.set_attribute(ResourceAttribute::RedirectStart(
RedirectStartValue::FetchStart,
));
context
.timing
.lock()
.unwrap()
.set_attribute(ResourceAttribute::FetchStart);
// Step 5
if request.redirect_count >= 20 {
return Response::network_error(NetworkError::Internal("Too many redirects".into()));
}
// Step 6
request.redirect_count += 1;
// Step 7
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::Internal(
"Cross-origin credentials check failed".into(),
));
}
// Step 8
if cors_flag && has_credentials {
return Response::network_error(NetworkError::Internal("Credentials check failed".into()));
}
// Step 9
if response
.actual_response()
.status
.as_ref()
.map_or(true, |s| s.0 != StatusCode::SEE_OTHER) &&
request.body.as_ref().map_or(false, |b| b.is_empty())
{
return Response::network_error(NetworkError::Internal("Request body is not done".into()));
}
// Step 10
if cors_flag && location_url.origin() != request.current_url().origin() {
request.origin = Origin::Origin(ImmutableOrigin::new_opaque());
}
// Step 11
if response
.actual_response()
.status
.as_ref()
.map_or(false, |(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.body = None;
}
// Step 12
if let Some(_) = request.body {
// TODO: extract request's body's source
}
// Step 13
request.url_list.push(location_url);
// Step 14
// TODO implement referrer policy
// Step 15
let recursive_flag = request.redirect_mode != RedirectMode::Manual;
let fetch_response = main_fetch(
request,
cache,
cors_flag,
recursive_flag,
target,
done_chan,
context,
);
// TODO: timing allow check
context
.timing
.lock()
.unwrap()
.set_attribute(ResourceAttribute::RedirectEnd(
RedirectEndValue::ResponseEnd,
));
redirect_end_timer.neuter();
fetch_response
}
fn try_immutable_origin_to_hyper_origin(url_origin: &ImmutableOrigin) -> Option<HyperOrigin> {
match *url_origin {
ImmutableOrigin::Opaque(_) => Some(HyperOrigin::NULL),
ImmutableOrigin::Tuple(ref scheme, ref host, ref port) => {
let port = match (scheme.as_ref(), port) {
("http", 80) | ("https", 443) => None,
_ => Some(*port),
};
HyperOrigin::try_from_parts(&scheme, &host.to_string(), port).ok()
},
}
}
/// [HTTP network or cache fetch](https://fetch.spec.whatwg.org#http-network-or-cache-fetch)
fn http_network_or_cache_fetch(
request: &mut Request,
authentication_fetch_flag: bool,
cors_flag: bool,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
// Step 2
let mut response: Option<Response> = None;
// Step 4
let mut revalidating_flag = false;
// TODO: Implement Window enum for Request
let request_has_no_window = true;
// Step 5.1
let mut http_request;
let http_request = if request_has_no_window && request.redirect_mode == RedirectMode::Error {
request
} else {
// Step 5.2
// TODO Implement body source
http_request = request.clone();
&mut http_request
};
// Step 5.3
let credentials_flag = match http_request.credentials_mode {
CredentialsMode::Include => true,
CredentialsMode::CredentialsSameOrigin
if http_request.response_tainting == ResponseTainting::Basic =>
{
true
},
_ => false,
};
let content_length_value = match http_request.body {
None => match http_request.method {
// Step 5.5
Method::POST | Method::PUT => Some(0),
// Step 5.4
_ => None,
},
// Step 5.6
Some(ref http_request_body) => Some(http_request_body.len() as u64),
};
// Step 5.7
if let Some(content_length_value) = content_length_value {
http_request
.headers
.typed_insert(ContentLength(content_length_value));
if http_request.keep_alive {
// Step 5.8 TODO: needs request's client object
}
}
// Step 5.9
match http_request.referrer {
Referrer::NoReferrer => (),
Referrer::ReferrerUrl(ref http_request_referrer) => http_request
.headers
.typed_insert::<Referer>(http_request_referrer.to_string().parse().unwrap()),
Referrer::Client =>
// it should be impossible for referrer to be anything else during fetching
// https://fetch.spec.whatwg.org/#concept-request-referrer
{
unreachable!()
},
};
// Step 5.10
if cors_flag || (http_request.method != Method::GET && http_request.method != Method::HEAD) {
debug_assert_ne!(http_request.origin, Origin::Client);
if let Origin::Origin(ref url_origin) = http_request.origin {
if let Some(hyper_origin) = try_immutable_origin_to_hyper_origin(url_origin) {
http_request.headers.typed_insert(hyper_origin)
}
}
}
// Step 5.11
if !http_request.headers.contains_key(header::USER_AGENT) {
let user_agent = context.user_agent.clone().into_owned();
http_request
.headers
.typed_insert::<UserAgent>(user_agent.parse().unwrap());
}
match http_request.cache_mode {
// Step 5.12
CacheMode::Default if is_no_store_cache(&http_request.headers) => {
http_request.cache_mode = CacheMode::NoStore;
},
// Step 5.13
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)));
},
// Step 5.14
CacheMode::Reload | CacheMode::NoStore => {
// Substep 1
if !http_request.headers.contains_key(header::PRAGMA) {
http_request.headers.typed_insert(Pragma::no_cache());
}
// Substep 2
if !http_request.headers.contains_key(header::CACHE_CONTROL) {
http_request
.headers
.typed_insert(CacheControl::new().with_no_cache());
}
},
_ => {},
}
// Step 5.15
// TODO: if necessary append `Accept-Encoding`/`identity` to headers
// Step 5.16
let current_url = http_request.current_url();
let host = Host::from(
format!(
"{}{}",
current_url.host_str().unwrap(),
current_url
.port()
.map(|v| format!(":{}", v))
.unwrap_or("".into())
)
.parse::<Authority>()
.unwrap(),
);
http_request.headers.typed_insert(host);
// unlike http_loader, we should not set the accept header
// here, according to the fetch spec
set_default_accept_encoding(&mut http_request.headers);
// Step 5.17
// TODO some of this step can't be implemented yet
if credentials_flag {
// Substep 1
// TODO http://mxr.mozilla.org/servo/source/components/net/http_loader.rs#504
// XXXManishearth http_loader has block_cookies: support content blocking here too
set_request_cookies(
&current_url,
&mut http_request.headers,
&context.state.cookie_jar,
);
// Substep 2
if !http_request.headers.contains_key(header::AUTHORIZATION) {
// Substep 3
let mut authorization_value = None;
// Substep 4
if let Some(basic) = auth_from_cache(&context.state.auth_cache, &current_url.origin()) {
if !http_request.use_url_credentials || !has_credentials(&current_url) {
authorization_value = Some(basic);
}
}
// Substep 5
if authentication_fetch_flag && authorization_value.is_none() {
if has_credentials(&current_url) {
authorization_value = Some(Authorization::basic(
current_url.username(),
current_url.password().unwrap_or(""),
));
}
}
// Substep 6
if let Some(basic) = authorization_value {
http_request.headers.typed_insert(basic);
}
}
}
// Step 5.18
// TODO If theres a proxy-authentication entry, use it as appropriate.
// Step 5.19
if let Ok(http_cache) = context.state.http_cache.read() {
if let Some(response_from_cache) = http_cache.construct_response(&http_request, done_chan) {
let response_headers = response_from_cache.response.headers.clone();
// Substep 1, 2, 3, 4
let (cached_response, needs_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),
response_from_cache.needs_validation,
),
};
if needs_revalidation {
revalidating_flag = true;
// Substep 5
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 {
// Substep 6
response = cached_response;
}
}
}
fn wait_for_cached_response(done_chan: &mut DoneChannel, response: &mut Option<Response>) {
if let Some(ref ch) = *done_chan {
// The cache constructed a response with a body of ResponseBody::Receiving.
// We wait for the response in the cache to "finish",
// with a body of either Done or Cancelled.
loop {
match ch
.1
.recv()
.expect("HTTP cache should always send Done or Cancelled")
{
Data::Payload(_) => {},
Data::Done => break, // Return the full response as if it was initially cached as such.
Data::Cancelled => {
// The response was cancelled while the fetch was ongoing.
// Set response to None, which will trigger a network fetch below.
*response = None;
break;
},
}
}
}
// Set done_chan back to None, it's cache-related usefulness ends here.
*done_chan = None;
}
wait_for_cached_response(done_chan, &mut response);
// Step 6
// TODO: https://infra.spec.whatwg.org/#if-aborted
// Step 7
if response.is_none() {
// Substep 1
if http_request.cache_mode == CacheMode::OnlyIfCached {
return Response::network_error(NetworkError::Internal(
"Couldn't find response in cache".into(),
));
}
}
// More Step 7
if response.is_none() {
// Substep 2
let forward_response =
http_network_fetch(http_request, credentials_flag, done_chan, context);
// Substep 3
if let Some((200..=399, _)) = forward_response.raw_status {
if !http_request.method.is_safe() {
if let Ok(mut http_cache) = context.state.http_cache.write() {
http_cache.invalidate(&http_request, &forward_response);
}
}
}
// Substep 4
if revalidating_flag &&
forward_response
.status
.as_ref()
.map_or(false, |s| s.0 == StatusCode::NOT_MODIFIED)
{
if let Ok(mut http_cache) = context.state.http_cache.write() {
response = http_cache.refresh(&http_request, forward_response.clone(), done_chan);
wait_for_cached_response(done_chan, &mut response);
}
}
// Substep 5
if response.is_none() {
if http_request.cache_mode != CacheMode::NoStore {
// Subsubstep 2, doing it first to avoid a clone of forward_response.
if let Ok(mut http_cache) = context.state.http_cache.write() {
http_cache.store(&http_request, &forward_response);
}
}
// Subsubstep 1
response = Some(forward_response);
}
}
let mut response = response.unwrap();
// Step 8
// TODO: if necessary set response's range-requested flag
// Step 9
// TODO: handle CORS not set and cross-origin blocked
// Step 10
// FIXME: Figure out what to do with request window objects
if let (Some((StatusCode::UNAUTHORIZED, _)), false, true) =
(response.status.as_ref(), cors_flag, credentials_flag)
{
// Substep 1
// TODO: Spec says requires testing on multiple WWW-Authenticate headers
// Substep 2
if http_request.body.is_some() {
// TODO Implement body source
}
// Substep 3
if !http_request.use_url_credentials || authentication_fetch_flag {
// FIXME: Prompt the user for username and password from the window
// Wrong, but will have to do until we are able to prompt the user
// otherwise this creates an infinite loop
// We basically pretend that the user declined to enter credentials
return response;
}
// Substep 4
response = http_network_or_cache_fetch(
http_request,
true, /* authentication flag */
cors_flag,
done_chan,
context,
);
}
// Step 11
if let Some((StatusCode::PROXY_AUTHENTICATION_REQUIRED, _)) = response.status.as_ref() {
// Step 1
if request_has_no_window {
return Response::network_error(NetworkError::Internal(
"Can't find Window object".into(),
));
}
// Step 2
// TODO: Spec says requires testing on Proxy-Authenticate headers
// Step 3
// FIXME: Prompt the user for proxy authentication credentials
// Wrong, but will have to do until we are able to prompt the user
// otherwise this creates an infinite loop
// We basically pretend that the user declined to enter credentials
return response;
// Step 4
// return http_network_or_cache_fetch(request, authentication_fetch_flag,
// cors_flag, done_chan, context);
}
// Step 12
if authentication_fetch_flag {
// TODO Create the authentication entry for request and the given realm
}
// Step 13
response
}
// Convenience struct that implements Done, for setting responseEnd on function return
struct ResponseEndTimer(Option<Arc<Mutex<ResourceFetchTiming>>>);
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.lock()
.unwrap()
.set_attribute(ResourceAttribute::ResponseEnd);
})
}
}
/// [HTTP network fetch](https://fetch.spec.whatwg.org/#http-network-fetch)
fn http_network_fetch(
request: &Request,
credentials_flag: bool,
done_chan: &mut DoneChannel,
context: &FetchContext,
) -> Response {
let mut response_end_timer = ResponseEndTimer(Some(context.timing.clone()));
// Step 1
// nothing to do here, since credentials_flag is already a boolean
// Step 2
// TODO be able to create connection using current url's origin and credentials
// Step 3
// TODO be able to tell if the connection is a failure
// Step 4
// TODO: check whether the connection is HTTP/2
// Step 5
let url = request.current_url();
let request_id = context
.devtools_chan
.as_ref()
.map(|_| uuid::Uuid::new_v4().to_simple().to_string());
if log_enabled!(log::Level::Info) {
info!("request for {} ({:?})", url, request.method);
for header in request.headers.iter() {
info!(" - {:?}", header);
}
}
// XHR uses the default destination; other kinds of fetches (which haven't been implemented yet)
// do not. Once we support other kinds of fetches we'll need to be more fine grained here
// since things like image fetches are classified differently by devtools
let is_xhr = request.destination == Destination::None;
let response_future = obtain_response(
&context.state.client,
&url,
&request.method,
&request.headers,
&request.body,
&request.method,
&request.pipeline_id,
request.redirect_count + 1,
request_id.as_ref().map(Deref::deref),
is_xhr,
context,
);
let pipeline_id = request.pipeline_id;
// This will only get the headers, the body is read later
let (res, msg) = match response_future.wait() {
Ok(wrapped_response) => wrapped_response,
Err(error) => return Response::network_error(error),
};
if log_enabled!(log::Level::Info) {
info!("response for {}", url);
for header in res.headers().iter() {
info!(" - {:?}", header);
}
}
let timing = context.timing.lock().unwrap().clone();
let mut response = Response::new(url.clone(), timing);
response.status = Some((
res.status(),
res.status().canonical_reason().unwrap_or("").into(),
));
response.raw_status = Some((
res.status().as_u16(),
res.status().canonical_reason().unwrap_or("").into(),
));
response.headers = res.headers().clone();
response.referrer = request.referrer.to_url().cloned();
response.referrer_policy = request.referrer_policy.clone();
let res_body = response.body.clone();
// We're about to spawn a future to be waited on here
let (done_sender, done_receiver) = unbounded();
*done_chan = Some((done_sender.clone(), done_receiver));
let meta = match response
.metadata()
.expect("Response metadata should exist at this stage")
{
FetchMetadata::Unfiltered(m) => m,
FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
};
let devtools_sender = context.devtools_chan.clone();
let meta_status = meta.status;
let meta_headers = meta.headers;
let cancellation_listener = context.cancellation_listener.clone();
if cancellation_listener.lock().unwrap().cancelled() {
return Response::network_error(NetworkError::Internal("Fetch aborted".into()));
}
*res_body.lock().unwrap() = ResponseBody::Receiving(vec![]);
let res_body2 = res_body.clone();
if let Some(ref sender) = devtools_sender {
if let Some(m) = msg {
send_request_to_devtools(m, &sender);
}
// --- Tell devtools that we got a response
// Send an HttpResponse message to devtools with the corresponding request_id
if let Some(pipeline_id) = pipeline_id {
send_response_to_devtools(
&sender,
request_id.unwrap(),
meta_headers.map(Serde::into_inner),
meta_status,
pipeline_id,
);
}
}
let done_sender2 = done_sender.clone();
let done_sender3 = done_sender.clone();
let timing_ptr2 = context.timing.clone();
let timing_ptr3 = context.timing.clone();
HANDLE.lock().unwrap().spawn(
res.into_body()
.map_err(|_| ())
.fold(res_body, move |res_body, chunk| {
if cancellation_listener.lock().unwrap().cancelled() {
*res_body.lock().unwrap() = ResponseBody::Done(vec![]);
let _ = done_sender.send(Data::Cancelled);
return future::failed(());
}
if let ResponseBody::Receiving(ref mut body) = *res_body.lock().unwrap() {
let bytes = chunk.into_bytes();
body.extend_from_slice(&*bytes);
let _ = done_sender.send(Data::Payload(bytes.to_vec()));
}
future::ok(res_body)
})
.and_then(move |res_body| {
let mut body = res_body.lock().unwrap();
let completed_body = match *body {
ResponseBody::Receiving(ref mut body) => mem::replace(body, vec![]),
_ => vec![],
};
*body = ResponseBody::Done(completed_body);
timing_ptr2
.lock()
.unwrap()
.set_attribute(ResourceAttribute::ResponseEnd);
let _ = done_sender2.send(Data::Done);
future::ok(())
})
.map_err(move |_| {
let mut body = res_body2.lock().unwrap();
let completed_body = match *body {
ResponseBody::Receiving(ref mut body) => mem::replace(body, vec![]),
_ => vec![],
};
*body = ResponseBody::Done(completed_body);
timing_ptr3
.lock()
.unwrap()
.set_attribute(ResourceAttribute::ResponseEnd);
let _ = done_sender3.send(Data::Done);
}),
);
// TODO these substeps aren't possible yet
// Substep 1
// Substep 2
// TODO Determine if response was retrieved over HTTPS
// TODO Servo needs to decide what ciphers are to be treated as "deprecated"
response.https_state = HttpsState::None;
// TODO Read request
// Step 6-11
// (needs stream bodies)
// Step 12
// TODO when https://bugzilla.mozilla.org/show_bug.cgi?id=1030660
// is resolved, this step will become uneccesary
// TODO this step
if let Some(encoding) = response.headers.typed_get::<ContentEncoding>() {
if encoding.contains("gzip") {
} else if encoding.contains("compress") {
}
};
// Step 13
// TODO this step isn't possible yet (CSP)
// Step 14, update the cached response, done via the shared response body.
// TODO this step isn't possible yet
// Step 15
if credentials_flag {
set_cookies_from_headers(&url, &response.headers, &context.state.cookie_jar);
}
// TODO these steps
// Step 16
// Substep 1
// Substep 2
// Sub-substep 1
// Sub-substep 2
// Sub-substep 3
// Sub-substep 4
// Substep 3
// Step 16
// Ensure we don't override "responseEnd" on successful return of this function
response_end_timer.neuter();
response
}
/// [CORS preflight fetch](https://fetch.spec.whatwg.org#cors-preflight-fetch)
fn cors_preflight_fetch(
request: &Request,
cache: &mut CorsCache,
context: &FetchContext,
) -> Response {
// Step 1
let mut preflight = Request::new(
request.current_url(),
Some(request.origin.clone()),
request.pipeline_id,
);
preflight.method = Method::OPTIONS;
preflight.initiator = request.initiator.clone();
preflight.destination = request.destination.clone();
preflight.origin = request.origin.clone();
preflight.referrer = request.referrer.clone();
preflight.referrer_policy = request.referrer_policy;
// Step 2
preflight
.headers
.typed_insert::<AccessControlRequestMethod>(AccessControlRequestMethod::from(
request.method.clone(),
));
// Step 3
let mut headers = request
.headers
.iter()
.filter(|(name, value)| !is_cors_safelisted_request_header(&name, &value))
.map(|(name, _)| name.as_str())
.collect::<Vec<&str>>();
headers.sort();
let headers = headers
.iter()
.map(|name| HeaderName::from_str(name).unwrap())
.collect::<Vec<HeaderName>>();
// Step 4
if !headers.is_empty() {
preflight
.headers
.typed_insert(AccessControlRequestHeaders::from_iter(headers));
}
// Step 5
let response = http_network_or_cache_fetch(&mut preflight, false, false, &mut None, context);
// Step 6
if cors_check(&request, &response).is_ok() &&
response
.status
.as_ref()
.map_or(false, |(status, _)| status.is_success())
{
// Substep 1, 2
let mut methods = if response
.headers
.contains_key(header::ACCESS_CONTROL_ALLOW_METHODS)
{
match response.headers.typed_get::<AccessControlAllowMethods>() {
Some(methods) => methods.iter().collect(),
// Substep 4
None => {
return Response::network_error(NetworkError::Internal(
"CORS ACAM check failed".into(),
));
},
}
} else {
vec![]
};
// Substep 3
let header_names = if response
.headers
.contains_key(header::ACCESS_CONTROL_ALLOW_HEADERS)
{
match response.headers.typed_get::<AccessControlAllowHeaders>() {
Some(names) => names.iter().collect(),
// Substep 4
None => {
return Response::network_error(NetworkError::Internal(
"CORS ACAH check failed".into(),
));
},
}
} else {
vec![]
};
// Substep 5
if (methods.iter().any(|m| m.as_ref() == "*") ||
header_names.iter().any(|hn| hn.as_str() == "*")) &&
request.credentials_mode == CredentialsMode::Include
{
return Response::network_error(NetworkError::Internal(
"CORS ACAH/ACAM and request credentials mode mismatch".into(),
));
}
// Substep 6
if methods.is_empty() && request.use_cors_preflight {
methods = vec![request.method.clone()];
}
// Substep 7
debug!(
"CORS check: Allowed methods: {:?}, current method: {:?}",
methods, request.method
);
if methods.iter().all(|method| *method != request.method) &&
!is_cors_safelisted_method(&request.method) &&
methods.iter().all(|m| m.as_ref() != "*")
{
return Response::network_error(NetworkError::Internal(
"CORS method check failed".into(),
));
}
// Substep 8
if request.headers.iter().any(|(name, _)| {
name == header::AUTHORIZATION && header_names.iter().all(|hn| hn != name)
}) {
return Response::network_error(NetworkError::Internal(
"CORS authorization check failed".into(),
));
}
// Substep 9
debug!(
"CORS check: Allowed headers: {:?}, current headers: {:?}",
header_names, request.headers
);
let set: HashSet<&HeaderName> = HashSet::from_iter(header_names.iter());
if request.headers.iter().any(|(name, value)| {
!set.contains(name) && !is_cors_safelisted_request_header(&name, &value)
}) {
return Response::network_error(NetworkError::Internal(
"CORS headers check failed".into(),
));
}
// Substep 10, 11
let max_age: Duration = response
.headers
.typed_get::<AccessControlMaxAge>()
.map(|acma| acma.into())
.unwrap_or(Duration::from_secs(0));
let max_age = max_age.as_secs() as u32;
// Substep 12
// TODO: Need to define what an imposed limit on max-age is
// Substep 13 ignored, we do have a CORS cache
// Substep 14, 15
for method in &methods {
cache.match_method_and_update(&*request, method.clone(), max_age);
}
// Substep 16, 17
for header_name in &header_names {
cache.match_header_and_update(&*request, &*header_name, max_age);
}
// Substep 18
return response;
}
// Step 7
Response::network_error(NetworkError::Internal("CORS check failed".into()))
}
/// [CORS check](https://fetch.spec.whatwg.org#concept-cors-check)
fn cors_check(request: &Request, response: &Response) -> Result<(), ()> {
// Step 1
let origin = response.headers.typed_get::<AccessControlAllowOrigin>();
// Step 2
let origin = origin.ok_or(())?;
// Step 3
if request.credentials_mode != CredentialsMode::Include &&
origin == AccessControlAllowOrigin::ANY
{
return Ok(());
}
// Step 4
let origin = match origin.origin() {
Some(origin) => origin,
// if it's Any or Null at this point, there's nothing to do but return Err(())
None => return Err(()),
};
match request.origin {
Origin::Origin(ref o) if o.ascii_serialization() == origin.to_string().trim() => {},
_ => return Err(()),
}
// Step 5
if request.credentials_mode != CredentialsMode::Include {
return Ok(());
}
// Step 6
let credentials = response
.headers
.typed_get::<AccessControlAllowCredentials>();
// Step 7
if credentials.is_some() {
return Ok(());
}
// Step 8
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)
}
/// <https://fetch.spec.whatwg.org/#redirect-status>
pub fn is_redirect_status(status: &(StatusCode, String)) -> bool {
match status.0 {
StatusCode::MOVED_PERMANENTLY |
StatusCode::FOUND |
StatusCode::SEE_OTHER |
StatusCode::TEMPORARY_REDIRECT |
StatusCode::PERMANENT_REDIRECT => true,
_ => false,
}
}