mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
It also updates the FetchResponseListener to process CSP violations to ensure that iframe elements (amongst others) properly generate the CSP events. These iframe elements are used in the Trusted Types tests themselves and weren't propagating the violations before. However, the tests themselves are still not passing since they also use Websockets, which currently aren't using the fetch machinery itself. That is fixed as part of [1]. [1]: https://github.com/servo/servo/issues/35028 --------- Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com> Signed-off-by: Josh Matthews <josh@joshmatthews.net> Co-authored-by: Josh Matthews <josh@joshmatthews.net>
1453 lines
50 KiB
Rust
1453 lines
50 KiB
Rust
/* 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/. */
|
|
|
|
#![cfg(not(target_os = "windows"))]
|
|
|
|
use std::fs;
|
|
use std::iter::FromIterator;
|
|
use std::path::Path;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::sync::{Arc, Mutex, Weak};
|
|
use std::time::{Duration, SystemTime};
|
|
|
|
use base::id::TEST_PIPELINE_ID;
|
|
use content_security_policy as csp;
|
|
use crossbeam_channel::{Sender, unbounded};
|
|
use devtools_traits::{HttpRequest as DevtoolsHttpRequest, HttpResponse as DevtoolsHttpResponse};
|
|
use headers::{
|
|
AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowMethods,
|
|
AccessControlAllowOrigin, AccessControlMaxAge, CacheControl, ContentLength, ContentType,
|
|
Expires, HeaderMapExt, LastModified, Pragma, StrictTransportSecurity, UserAgent,
|
|
};
|
|
use http::header::{self, HeaderMap, HeaderName, HeaderValue};
|
|
use http::{Method, StatusCode};
|
|
use http_body_util::combinators::BoxBody;
|
|
use hyper::body::{Bytes, Incoming};
|
|
use hyper::{Request as HyperRequest, Response as HyperResponse};
|
|
use mime::{self, Mime};
|
|
use net::fetch::cors_cache::CorsCache;
|
|
use net::fetch::methods::{self, FetchContext};
|
|
use net::filemanager_thread::FileManager;
|
|
use net::hsts::HstsEntry;
|
|
use net::protocols::ProtocolRegistry;
|
|
use net::request_interceptor::RequestInterceptor;
|
|
use net::resource_thread::CoreResourceThreadPool;
|
|
use net_traits::filemanager_thread::FileTokenCheck;
|
|
use net_traits::http_status::HttpStatus;
|
|
use net_traits::request::{
|
|
Destination, RedirectMode, Referrer, Request, RequestBuilder, RequestMode,
|
|
};
|
|
use net_traits::response::{CacheState, Response, ResponseBody, ResponseType};
|
|
use net_traits::{
|
|
FetchTaskTarget, IncludeSubdomains, NetworkError, ReferrerPolicy, ResourceFetchTiming,
|
|
ResourceTimingType,
|
|
};
|
|
use servo_arc::Arc as ServoArc;
|
|
use servo_url::ServoUrl;
|
|
use uuid::Uuid;
|
|
|
|
use crate::http_loader::{expect_devtools_http_request, expect_devtools_http_response};
|
|
use crate::{
|
|
DEFAULT_USER_AGENT, create_embedder_proxy, create_embedder_proxy_and_receiver,
|
|
create_http_state, fetch, fetch_with_context, fetch_with_cors_cache, make_body, make_server,
|
|
make_ssl_server, new_fetch_context,
|
|
};
|
|
|
|
// TODO write a struct that impls Handler for storing test values
|
|
|
|
#[test]
|
|
fn test_fetch_response_is_not_network_error() {
|
|
static MESSAGE: &'static [u8] = b"";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
|
|
if fetch_response.is_network_error() {
|
|
panic!("fetch response shouldn't be a network error");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_on_bad_port_is_network_error() {
|
|
let url = ServoUrl::parse("http://www.example.org:6667").unwrap();
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
let fetch_response = fetch(request, None);
|
|
assert!(fetch_response.is_network_error());
|
|
let fetch_error = fetch_response.get_network_error().unwrap();
|
|
assert_eq!(
|
|
fetch_error,
|
|
&NetworkError::Internal("Request attempted on bad port".into())
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_response_body_matches_const_message() {
|
|
static MESSAGE: &'static [u8] = b"Hello World!";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
|
|
assert!(!fetch_response.is_network_error());
|
|
assert_eq!(fetch_response.response_type, ResponseType::Basic);
|
|
|
|
match *fetch_response.body.lock().unwrap() {
|
|
ResponseBody::Done(ref body) => {
|
|
assert_eq!(&**body, MESSAGE);
|
|
},
|
|
_ => panic!(),
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_aboutblank() {
|
|
let url = ServoUrl::parse("about:blank").unwrap();
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
|
|
let fetch_response = fetch(request, None);
|
|
// We should see an opaque-filtered response.
|
|
assert_eq!(fetch_response.response_type, ResponseType::Opaque);
|
|
assert!(!fetch_response.is_network_error());
|
|
assert_eq!(fetch_response.headers.len(), 0);
|
|
let resp_body = fetch_response.body.lock().unwrap();
|
|
assert_eq!(*resp_body, ResponseBody::Empty);
|
|
|
|
// The underlying response behind the filter should
|
|
// have a 0-byte body.
|
|
let actual_response = fetch_response.actual_response();
|
|
assert!(!actual_response.is_network_error());
|
|
let resp_body = actual_response.body.lock().unwrap();
|
|
assert_eq!(*resp_body, ResponseBody::Done(vec![]));
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_blob() {
|
|
use net_traits::blob_url_store::BlobBuf;
|
|
|
|
struct FetchResponseCollector {
|
|
sender: Sender<Response>,
|
|
buffer: Vec<u8>,
|
|
expected: Vec<u8>,
|
|
}
|
|
|
|
impl FetchTaskTarget for FetchResponseCollector {
|
|
fn process_request_body(&mut self, _: &Request) {}
|
|
fn process_request_eof(&mut self, _: &Request) {}
|
|
fn process_response(&mut self, _: &Request, _: &Response) {}
|
|
fn process_response_chunk(&mut self, _: &Request, chunk: Vec<u8>) {
|
|
self.buffer.extend_from_slice(chunk.as_slice());
|
|
}
|
|
/// Fired when the response is fully fetched
|
|
fn process_response_eof(&mut self, _: &Request, response: &Response) {
|
|
assert_eq!(self.buffer, self.expected);
|
|
let _ = self.sender.send(response.clone());
|
|
}
|
|
fn process_csp_violations(&mut self, _: &Request, _: Vec<csp::Violation>) {}
|
|
}
|
|
|
|
let context = new_fetch_context(None, None, None);
|
|
|
|
let bytes = b"content";
|
|
let blob_buf = BlobBuf {
|
|
filename: Some("test.txt".into()),
|
|
type_string: "text/plain".into(),
|
|
size: bytes.len() as u64,
|
|
bytes: bytes.to_vec(),
|
|
};
|
|
|
|
let origin = ServoUrl::parse("http://www.example.org/").unwrap();
|
|
|
|
let id = Uuid::new_v4();
|
|
context.filemanager.lock().unwrap().promote_memory(
|
|
id.clone(),
|
|
blob_buf,
|
|
true,
|
|
"http://www.example.org".into(),
|
|
);
|
|
let url = ServoUrl::parse(&format!("blob:{}{}", origin.as_str(), id.simple())).unwrap();
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(origin.origin())
|
|
.build();
|
|
|
|
let (sender, receiver) = unbounded();
|
|
|
|
let mut target = FetchResponseCollector {
|
|
sender,
|
|
buffer: vec![],
|
|
expected: bytes.to_vec(),
|
|
};
|
|
|
|
crate::HANDLE.block_on(methods::fetch(request, &mut target, &context));
|
|
|
|
let fetch_response = receiver.recv().unwrap();
|
|
assert!(!fetch_response.is_network_error());
|
|
|
|
assert_eq!(fetch_response.headers.len(), 2);
|
|
|
|
let content_type: Mime = fetch_response
|
|
.headers
|
|
.typed_get::<ContentType>()
|
|
.unwrap()
|
|
.into();
|
|
assert_eq!(content_type, mime::TEXT_PLAIN);
|
|
|
|
let content_length: ContentLength = fetch_response.headers.typed_get().unwrap();
|
|
assert_eq!(content_length.0, bytes.len() as u64);
|
|
|
|
assert_eq!(
|
|
*fetch_response.body.lock().unwrap(),
|
|
ResponseBody::Receiving(vec![])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file() {
|
|
let path = Path::new("../../resources/servo.css")
|
|
.canonicalize()
|
|
.unwrap();
|
|
let url = ServoUrl::from_file_path(path.clone()).unwrap();
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
|
|
let pool = CoreResourceThreadPool::new(1, "CoreResourceTestPool".to_string());
|
|
let pool_handle = Arc::new(pool);
|
|
let mut context = new_fetch_context(None, None, Some(Arc::downgrade(&pool_handle)));
|
|
let fetch_response = fetch_with_context(request, &mut context);
|
|
|
|
// We should see an opaque-filtered response.
|
|
assert_eq!(fetch_response.response_type, ResponseType::Opaque);
|
|
|
|
assert!(!fetch_response.is_network_error());
|
|
assert_eq!(fetch_response.headers.len(), 0);
|
|
let resp_body = fetch_response.body.lock().unwrap();
|
|
assert_eq!(*resp_body, ResponseBody::Empty);
|
|
|
|
// The underlying response behind the filter should
|
|
// have the file's MIME type and contents.
|
|
let actual_response = fetch_response.actual_response();
|
|
assert!(!actual_response.is_network_error());
|
|
assert_eq!(actual_response.headers.len(), 1);
|
|
let content_type: Mime = actual_response
|
|
.headers
|
|
.typed_get::<ContentType>()
|
|
.unwrap()
|
|
.into();
|
|
assert_eq!(content_type, mime::TEXT_CSS);
|
|
|
|
let resp_body = actual_response.body.lock().unwrap();
|
|
let file = fs::read(path).unwrap();
|
|
|
|
match *resp_body {
|
|
ResponseBody::Done(ref val) => {
|
|
assert_eq!(val, &file);
|
|
},
|
|
_ => panic!(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_ftp() {
|
|
let url = ServoUrl::parse("ftp://not-supported").unwrap();
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
let fetch_response = fetch(request, None);
|
|
assert!(fetch_response.is_network_error());
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_bogus_scheme() {
|
|
let url = ServoUrl::parse("bogus://whatever").unwrap();
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
let fetch_response = fetch(request, None);
|
|
assert!(fetch_response.is_network_error());
|
|
}
|
|
|
|
#[test]
|
|
fn test_cors_preflight_fetch() {
|
|
static ACK: &'static [u8] = b"ACK";
|
|
let state = Arc::new(AtomicUsize::new(0));
|
|
let handler =
|
|
move |request: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
if request.method() == Method::OPTIONS &&
|
|
state.clone().fetch_add(1, Ordering::SeqCst) == 0
|
|
{
|
|
assert!(
|
|
request
|
|
.headers()
|
|
.contains_key(header::ACCESS_CONTROL_REQUEST_METHOD)
|
|
);
|
|
assert!(
|
|
!request
|
|
.headers()
|
|
.contains_key(header::ACCESS_CONTROL_REQUEST_HEADERS)
|
|
);
|
|
assert!(
|
|
!request
|
|
.headers()
|
|
.get(header::REFERER)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.contains("a.html")
|
|
);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowCredentials);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowMethods::from_iter(vec![Method::GET]));
|
|
} else {
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
|
*response.body_mut() = make_body(ACK.to_vec());
|
|
}
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
let target_url = url.clone().join("a.html").unwrap();
|
|
let mut request = RequestBuilder::new(None, url, Referrer::ReferrerUrl(target_url)).build();
|
|
request.referrer_policy = ReferrerPolicy::Origin;
|
|
request.use_cors_preflight = true;
|
|
request.mode = RequestMode::CorsMode;
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
|
|
assert!(!fetch_response.is_network_error());
|
|
match *fetch_response.body.lock().unwrap() {
|
|
ResponseBody::Done(ref body) => assert_eq!(&**body, ACK),
|
|
_ => panic!(),
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn test_cors_preflight_cache_fetch() {
|
|
static ACK: &'static [u8] = b"ACK";
|
|
let state = Arc::new(AtomicUsize::new(0));
|
|
let counter = state.clone();
|
|
let mut cache = CorsCache::default();
|
|
let handler =
|
|
move |request: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
if request.method() == Method::OPTIONS &&
|
|
state.clone().fetch_add(1, Ordering::SeqCst) == 0
|
|
{
|
|
assert!(
|
|
request
|
|
.headers()
|
|
.contains_key(header::ACCESS_CONTROL_REQUEST_METHOD)
|
|
);
|
|
assert!(
|
|
!request
|
|
.headers()
|
|
.contains_key(header::ACCESS_CONTROL_REQUEST_HEADERS)
|
|
);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowCredentials);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowMethods::from_iter(vec![Method::GET]));
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlMaxAge::from(Duration::new(6000, 0)));
|
|
} else {
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
|
*response.body_mut() = make_body(ACK.to_vec());
|
|
}
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
let mut request = RequestBuilder::new(None, url, Referrer::NoReferrer).build();
|
|
request.use_cors_preflight = true;
|
|
request.mode = RequestMode::CorsMode;
|
|
let wrapped_request0 = request.clone();
|
|
let wrapped_request1 = request.clone();
|
|
let wrapped_request2 = request.clone();
|
|
let wrapped_request3 = request;
|
|
|
|
let fetch_response0 = fetch_with_cors_cache(wrapped_request0, &mut cache);
|
|
let fetch_response1 = fetch_with_cors_cache(wrapped_request1, &mut cache);
|
|
let _ = server.close();
|
|
|
|
assert!(!fetch_response0.is_network_error() && !fetch_response1.is_network_error());
|
|
|
|
// The response from the CORS-preflight cache was used
|
|
assert_eq!(1, counter.load(Ordering::SeqCst));
|
|
|
|
// The entry exists in the CORS-preflight cache
|
|
assert_eq!(true, cache.match_method(&wrapped_request2, Method::GET));
|
|
assert_eq!(true, cache.match_method(&wrapped_request3, Method::GET));
|
|
|
|
match *fetch_response0.body.lock().unwrap() {
|
|
ResponseBody::Done(ref body) => assert_eq!(&**body, ACK),
|
|
_ => panic!(),
|
|
};
|
|
match *fetch_response1.body.lock().unwrap() {
|
|
ResponseBody::Done(ref body) => assert_eq!(&**body, ACK),
|
|
_ => panic!(),
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn test_cors_preflight_fetch_network_error() {
|
|
static ACK: &'static [u8] = b"ACK";
|
|
let state = Arc::new(AtomicUsize::new(0));
|
|
let handler =
|
|
move |request: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
if request.method() == Method::OPTIONS &&
|
|
state.clone().fetch_add(1, Ordering::SeqCst) == 0
|
|
{
|
|
assert!(
|
|
request
|
|
.headers()
|
|
.contains_key(header::ACCESS_CONTROL_REQUEST_METHOD)
|
|
);
|
|
assert!(
|
|
!request
|
|
.headers()
|
|
.contains_key(header::ACCESS_CONTROL_REQUEST_HEADERS)
|
|
);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowCredentials);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowMethods::from_iter(vec![Method::GET]));
|
|
} else {
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
|
*response.body_mut() = make_body(ACK.to_vec());
|
|
}
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
let mut request = RequestBuilder::new(None, url, Referrer::NoReferrer).build();
|
|
request.method = Method::from_bytes(b"CHICKEN").unwrap();
|
|
request.use_cors_preflight = true;
|
|
request.mode = RequestMode::CorsMode;
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
|
|
assert!(fetch_response.is_network_error());
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_response_is_basic_filtered() {
|
|
static MESSAGE: &'static [u8] = b"";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
response
|
|
.headers_mut()
|
|
.insert(header::SET_COOKIE, HeaderValue::from_static(""));
|
|
// this header is obsoleted, so hyper doesn't implement it, but it's still covered by the spec
|
|
response.headers_mut().insert(
|
|
HeaderName::from_static("set-cookie2"),
|
|
HeaderValue::from_bytes(&vec![]).unwrap(),
|
|
);
|
|
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
|
|
assert!(!fetch_response.is_network_error());
|
|
assert_eq!(fetch_response.response_type, ResponseType::Basic);
|
|
|
|
let headers = fetch_response.headers;
|
|
assert!(!headers.contains_key(header::SET_COOKIE));
|
|
assert!(
|
|
headers
|
|
.get(HeaderName::from_static("set-cookie2"))
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_response_is_cors_filtered() {
|
|
static MESSAGE: &'static [u8] = b"";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
// this is mandatory for the Cors Check to pass
|
|
// TODO test using different url encodings with this value ie. punycode
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowOrigin::ANY);
|
|
|
|
// these are the headers that should be kept after filtering
|
|
response.headers_mut().typed_insert(CacheControl::new());
|
|
response.headers_mut().insert(
|
|
header::CONTENT_LANGUAGE,
|
|
HeaderValue::from_bytes(&vec![]).unwrap(),
|
|
);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(ContentType::from(mime::TEXT_HTML));
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(Expires::from(SystemTime::now() + Duration::new(86400, 0)));
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(LastModified::from(SystemTime::now()));
|
|
response.headers_mut().typed_insert(Pragma::no_cache());
|
|
|
|
// these headers should not be kept after filtering, even though they are given a pass
|
|
response
|
|
.headers_mut()
|
|
.insert(header::SET_COOKIE, HeaderValue::from_static(""));
|
|
response.headers_mut().insert(
|
|
HeaderName::from_static("set-cookie2"),
|
|
HeaderValue::from_bytes(&vec![]).unwrap(),
|
|
);
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(AccessControlAllowHeaders::from_iter(vec![
|
|
HeaderName::from_static("set-cookie"),
|
|
HeaderName::from_static("set-cookie2"),
|
|
]));
|
|
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
// an origin mis-match will stop it from defaulting to a basic filtered response
|
|
let mut request = RequestBuilder::new(None, url, Referrer::NoReferrer).build();
|
|
request.mode = RequestMode::CorsMode;
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
|
|
assert!(!fetch_response.is_network_error());
|
|
assert_eq!(fetch_response.response_type, ResponseType::Cors);
|
|
|
|
let headers = fetch_response.headers;
|
|
assert!(headers.contains_key(header::CACHE_CONTROL));
|
|
assert!(headers.contains_key(header::CONTENT_LANGUAGE));
|
|
assert!(headers.contains_key(header::CONTENT_TYPE));
|
|
assert!(headers.contains_key(header::EXPIRES));
|
|
assert!(headers.contains_key(header::LAST_MODIFIED));
|
|
assert!(headers.contains_key(header::PRAGMA));
|
|
|
|
assert!(!headers.contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN));
|
|
assert!(!headers.contains_key(header::SET_COOKIE));
|
|
assert!(
|
|
headers
|
|
.get(HeaderName::from_static("set-cookie2"))
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_response_is_opaque_filtered() {
|
|
static MESSAGE: &'static [u8] = b"";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
// an origin mis-match will fall through to an Opaque filtered response
|
|
let request = RequestBuilder::new(None, url, Referrer::NoReferrer).build();
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
|
|
assert!(!fetch_response.is_network_error());
|
|
assert_eq!(fetch_response.response_type, ResponseType::Opaque);
|
|
|
|
assert!(fetch_response.url().is_none());
|
|
assert!(fetch_response.url_list.is_empty());
|
|
// this also asserts that status message is "the empty byte sequence"
|
|
assert!(fetch_response.status.is_error());
|
|
assert_eq!(fetch_response.headers, HeaderMap::new());
|
|
match *fetch_response.body.lock().unwrap() {
|
|
ResponseBody::Empty => {},
|
|
_ => panic!(),
|
|
}
|
|
match fetch_response.cache_state {
|
|
CacheState::None => {},
|
|
_ => panic!(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_response_is_opaque_redirect_filtered() {
|
|
static MESSAGE: &'static [u8] = b"";
|
|
let handler =
|
|
move |request: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
let redirects = request
|
|
.uri()
|
|
.path()
|
|
.split("/")
|
|
.collect::<String>()
|
|
.parse::<u32>()
|
|
.unwrap_or(0);
|
|
|
|
if redirects == 1 {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
} else {
|
|
*response.status_mut() = StatusCode::FOUND;
|
|
response
|
|
.headers_mut()
|
|
.insert(header::LOCATION, HeaderValue::from_static("1"));
|
|
}
|
|
};
|
|
|
|
let (server, url) = make_server(handler);
|
|
|
|
let mut request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
request.redirect_mode = RedirectMode::Manual;
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
|
|
assert!(!fetch_response.is_network_error());
|
|
assert_eq!(fetch_response.response_type, ResponseType::OpaqueRedirect);
|
|
|
|
// this also asserts that status message is "the empty byte sequence"
|
|
assert!(fetch_response.status.is_error());
|
|
assert_eq!(fetch_response.headers, HeaderMap::new());
|
|
match *fetch_response.body.lock().unwrap() {
|
|
ResponseBody::Empty => {},
|
|
_ => panic!(),
|
|
}
|
|
match fetch_response.cache_state {
|
|
CacheState::None => {},
|
|
_ => panic!(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_with_local_urls_only() {
|
|
// If flag `local_urls_only` is set, fetching a non-local URL must result in network error.
|
|
|
|
static MESSAGE: &'static [u8] = b"";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, server_url) = make_server(handler);
|
|
|
|
let do_fetch = |url: ServoUrl| {
|
|
let mut request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
|
|
// Set the flag.
|
|
request.local_urls_only = true;
|
|
|
|
fetch(request, None)
|
|
};
|
|
|
|
let local_url = ServoUrl::parse("about:blank").unwrap();
|
|
let local_response = do_fetch(local_url);
|
|
let server_response = do_fetch(server_url);
|
|
|
|
let _ = server.close();
|
|
|
|
assert!(!local_response.is_network_error());
|
|
assert!(server_response.is_network_error());
|
|
}
|
|
// NOTE(emilio): If this test starts failing:
|
|
//
|
|
// openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
|
// -keyout resources/privatekey_for_testing.key \
|
|
// -out resources/self_signed_certificate_for_testing.crt
|
|
//
|
|
// And make sure to specify `localhost` as the server name.
|
|
#[test]
|
|
fn test_fetch_with_hsts() {
|
|
static MESSAGE: &'static [u8] = b"";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
|
|
let (server, url) = make_ssl_server(handler);
|
|
|
|
let embedder_proxy = create_embedder_proxy();
|
|
|
|
let mut context = FetchContext {
|
|
state: Arc::new(create_http_state(None)),
|
|
user_agent: DEFAULT_USER_AGENT.into(),
|
|
devtools_chan: None,
|
|
filemanager: Arc::new(Mutex::new(FileManager::new(
|
|
embedder_proxy.clone(),
|
|
Weak::new(),
|
|
))),
|
|
file_token: FileTokenCheck::NotRequired,
|
|
request_interceptor: Arc::new(Mutex::new(RequestInterceptor::new(embedder_proxy))),
|
|
cancellation_listener: Arc::new(Default::default()),
|
|
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
|
|
ResourceTimingType::Navigation,
|
|
))),
|
|
protocols: Arc::new(ProtocolRegistry::default()),
|
|
};
|
|
|
|
// The server certificate is self-signed, so we need to add an override
|
|
// so that the connection works properly.
|
|
for certificate in server.certificates.as_ref().unwrap().iter() {
|
|
context.state.override_manager.add_override(certificate);
|
|
}
|
|
|
|
{
|
|
let mut list = context.state.hsts_list.write().unwrap();
|
|
list.push(
|
|
HstsEntry::new("localhost".to_owned(), IncludeSubdomains::NotIncluded, None).unwrap(),
|
|
);
|
|
}
|
|
let mut request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
// Set the flag.
|
|
request.local_urls_only = false;
|
|
let response = fetch_with_context(request, &mut context);
|
|
server.close();
|
|
assert_eq!(
|
|
response.internal_response.unwrap().url().unwrap().scheme(),
|
|
"https"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_load_adds_host_to_hsts_list_when_url_is_https() {
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
response
|
|
.headers_mut()
|
|
.typed_insert(StrictTransportSecurity::excluding_subdomains(
|
|
Duration::from_secs(31536000),
|
|
));
|
|
*response.body_mut() = make_body(b"Yay!".to_vec());
|
|
};
|
|
|
|
let (server, mut url) = make_ssl_server(handler);
|
|
url.as_mut_url().set_scheme("https").unwrap();
|
|
|
|
let embedder_proxy = create_embedder_proxy();
|
|
|
|
let mut context = FetchContext {
|
|
state: Arc::new(create_http_state(None)),
|
|
user_agent: DEFAULT_USER_AGENT.into(),
|
|
devtools_chan: None,
|
|
filemanager: Arc::new(Mutex::new(FileManager::new(
|
|
embedder_proxy.clone(),
|
|
Weak::new(),
|
|
))),
|
|
file_token: FileTokenCheck::NotRequired,
|
|
request_interceptor: Arc::new(Mutex::new(RequestInterceptor::new(embedder_proxy))),
|
|
cancellation_listener: Arc::new(Default::default()),
|
|
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
|
|
ResourceTimingType::Navigation,
|
|
))),
|
|
protocols: Arc::new(ProtocolRegistry::default()),
|
|
};
|
|
|
|
// The server certificate is self-signed, so we need to add an override
|
|
// so that the connection works properly.
|
|
for certificate in server.certificates.as_ref().unwrap().iter() {
|
|
context.state.override_manager.add_override(certificate);
|
|
}
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.method(Method::GET)
|
|
.body(None)
|
|
.destination(Destination::Document)
|
|
.origin(url.clone().origin())
|
|
.pipeline_id(Some(TEST_PIPELINE_ID))
|
|
.build();
|
|
|
|
let response = fetch_with_context(request, &mut context);
|
|
|
|
let _ = server.close();
|
|
|
|
assert!(
|
|
response
|
|
.internal_response
|
|
.unwrap()
|
|
.status
|
|
.code()
|
|
.is_success()
|
|
);
|
|
assert!(
|
|
context
|
|
.state
|
|
.hsts_list
|
|
.read()
|
|
.unwrap()
|
|
.is_host_secure(url.host_str().unwrap())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_self_signed() {
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(b"Yay!".to_vec());
|
|
};
|
|
|
|
let (server, mut url) = make_ssl_server(handler);
|
|
url.as_mut_url().set_scheme("https").unwrap();
|
|
|
|
let embedder_proxy = create_embedder_proxy();
|
|
|
|
let mut context = FetchContext {
|
|
state: Arc::new(create_http_state(None)),
|
|
user_agent: DEFAULT_USER_AGENT.into(),
|
|
devtools_chan: None,
|
|
filemanager: Arc::new(Mutex::new(FileManager::new(
|
|
embedder_proxy.clone(),
|
|
Weak::new(),
|
|
))),
|
|
file_token: FileTokenCheck::NotRequired,
|
|
request_interceptor: Arc::new(Mutex::new(RequestInterceptor::new(embedder_proxy))),
|
|
cancellation_listener: Arc::new(Default::default()),
|
|
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
|
|
ResourceTimingType::Navigation,
|
|
))),
|
|
protocols: Arc::new(ProtocolRegistry::default()),
|
|
};
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.method(Method::GET)
|
|
.body(None)
|
|
.destination(Destination::Document)
|
|
.origin(url.clone().origin())
|
|
.pipeline_id(Some(TEST_PIPELINE_ID))
|
|
.build();
|
|
|
|
let response = fetch_with_context(request, &mut context);
|
|
|
|
assert!(matches!(
|
|
response.get_network_error(),
|
|
Some(NetworkError::SslValidation(..))
|
|
));
|
|
|
|
// The server certificate is self-signed, so we need to add an override
|
|
// so that the connection works properly.
|
|
for certificate in server.certificates.as_ref().unwrap().iter() {
|
|
context.state.override_manager.add_override(certificate);
|
|
}
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.method(Method::GET)
|
|
.body(None)
|
|
.destination(Destination::Document)
|
|
.origin(url.clone().origin())
|
|
.pipeline_id(Some(TEST_PIPELINE_ID))
|
|
.build();
|
|
|
|
let response = fetch_with_context(request, &mut context);
|
|
|
|
assert!(response.status.code().is_success());
|
|
|
|
let _ = server.close();
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_with_sri_network_error() {
|
|
static MESSAGE: &'static [u8] = b"alert('Hello, Network Error');";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
let mut request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
// To calulate hash use :
|
|
// echo -n "alert('Hello, Network Error');" | openssl dgst -sha384 -binary | openssl base64 -A
|
|
request.integrity_metadata =
|
|
"sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO".to_owned();
|
|
// Set the flag.
|
|
request.local_urls_only = false;
|
|
|
|
let response = fetch(request, None);
|
|
|
|
let _ = server.close();
|
|
assert!(response.is_network_error());
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_with_sri_sucess() {
|
|
static MESSAGE: &'static [u8] = b"alert('Hello, world.');";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
let mut request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
// To calulate hash use :
|
|
// echo -n "alert('Hello, Network Error');" | openssl dgst -sha384 -binary | openssl base64 -A
|
|
request.integrity_metadata =
|
|
"sha384-H8BRh8j48O9oYatfu5AZzq6A9RINhZO5H16dQZngK7T62em8MUt1FLm52t+eX6xO".to_owned();
|
|
// Set the flag.
|
|
request.local_urls_only = false;
|
|
|
|
let response = fetch(request, None);
|
|
|
|
let _ = server.close();
|
|
assert_eq!(response_is_done(&response), true);
|
|
}
|
|
|
|
/// `fetch` should return a network error if there is a header `X-Content-Type-Options: nosniff`
|
|
#[test]
|
|
fn test_fetch_blocked_nosniff() {
|
|
#[inline]
|
|
fn test_nosniff_request(destination: Destination, mime: Mime, should_error: bool) {
|
|
const MESSAGE: &'static [u8] = b"";
|
|
const HEADER: &'static str = "x-content-type-options";
|
|
const VALUE: &'static [u8] = b"nosniff";
|
|
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
let mime_header = ContentType::from(mime.clone());
|
|
response.headers_mut().typed_insert(mime_header);
|
|
assert!(response.headers().contains_key(header::CONTENT_TYPE));
|
|
// Add the nosniff header
|
|
response.headers_mut().insert(
|
|
HeaderName::from_static(HEADER),
|
|
HeaderValue::from_bytes(VALUE).unwrap(),
|
|
);
|
|
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
|
|
let (server, url) = make_server(handler);
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.destination(destination)
|
|
.build();
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
|
|
assert_eq!(fetch_response.is_network_error(), should_error);
|
|
}
|
|
|
|
let tests = vec![
|
|
(Destination::Script, mime::TEXT_JAVASCRIPT, false),
|
|
(Destination::Script, mime::TEXT_CSS, true),
|
|
(Destination::Style, mime::TEXT_CSS, false),
|
|
];
|
|
|
|
for test in tests {
|
|
let (destination, mime, should_error) = test;
|
|
test_nosniff_request(destination, mime, should_error);
|
|
}
|
|
}
|
|
|
|
fn setup_server_and_fetch(message: &'static [u8], redirect_cap: u32) -> Response {
|
|
let handler =
|
|
move |request: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
let redirects = request
|
|
.uri()
|
|
.path()
|
|
.split("/")
|
|
.collect::<String>()
|
|
.parse::<u32>()
|
|
.unwrap_or(0);
|
|
|
|
if redirects >= redirect_cap {
|
|
*response.body_mut() = make_body(message.to_vec());
|
|
} else {
|
|
*response.status_mut() = StatusCode::FOUND;
|
|
let url = format!("{redirects}", redirects = redirects + 1);
|
|
response
|
|
.headers_mut()
|
|
.insert(header::LOCATION, HeaderValue::from_str(&url).unwrap());
|
|
}
|
|
};
|
|
|
|
let (server, url) = make_server(handler);
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
let fetch_response = fetch(request, None);
|
|
let _ = server.close();
|
|
fetch_response
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_redirect_count_ceiling() {
|
|
static MESSAGE: &'static [u8] = b"no more redirects";
|
|
// how many redirects to cause
|
|
let redirect_cap = 20;
|
|
|
|
let fetch_response = setup_server_and_fetch(MESSAGE, redirect_cap);
|
|
|
|
assert!(!fetch_response.is_network_error());
|
|
assert_eq!(fetch_response.response_type, ResponseType::Basic);
|
|
|
|
match *fetch_response.body.lock().unwrap() {
|
|
ResponseBody::Done(ref body) => {
|
|
assert_eq!(&**body, MESSAGE);
|
|
},
|
|
_ => panic!(),
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_redirect_count_failure() {
|
|
static MESSAGE: &'static [u8] = b"this message shouldn't be reachable";
|
|
// how many redirects to cause
|
|
let redirect_cap = 21;
|
|
|
|
let fetch_response = setup_server_and_fetch(MESSAGE, redirect_cap);
|
|
|
|
assert!(fetch_response.is_network_error());
|
|
|
|
match *fetch_response.body.lock().unwrap() {
|
|
ResponseBody::Done(_) | ResponseBody::Receiving(_) => panic!(),
|
|
_ => {},
|
|
};
|
|
}
|
|
|
|
fn test_fetch_redirect_updates_method_runner(
|
|
tx: Sender<bool>,
|
|
status_code: StatusCode,
|
|
method: Method,
|
|
) {
|
|
let handler_method = method.clone();
|
|
let handler_tx = Arc::new(tx);
|
|
|
|
let handler =
|
|
move |request: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
let redirects = request
|
|
.uri()
|
|
.path()
|
|
.split("/")
|
|
.collect::<String>()
|
|
.parse::<u32>()
|
|
.unwrap_or(0);
|
|
|
|
let mut test_pass = true;
|
|
|
|
if redirects == 0 {
|
|
*response.status_mut() = StatusCode::TEMPORARY_REDIRECT;
|
|
response
|
|
.headers_mut()
|
|
.insert(header::LOCATION, HeaderValue::from_static("1"));
|
|
} else if redirects == 1 {
|
|
// this makes sure that the request method does't change from the wrong status code
|
|
if handler_method != Method::GET && request.method() == Method::GET {
|
|
test_pass = false;
|
|
}
|
|
*response.status_mut() = status_code;
|
|
response
|
|
.headers_mut()
|
|
.insert(header::LOCATION, HeaderValue::from_static("2"));
|
|
} else if request.method() != Method::GET {
|
|
test_pass = false;
|
|
}
|
|
|
|
// the first time this handler is reached, nothing is being tested, so don't send anything
|
|
if redirects > 0 {
|
|
handler_tx.send(test_pass).unwrap();
|
|
}
|
|
};
|
|
|
|
let (server, url) = crate::make_server(handler);
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.method(method)
|
|
.build();
|
|
|
|
let _ = fetch(request, None);
|
|
let _ = server.close();
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_redirect_updates_method() {
|
|
let (tx, rx) = unbounded();
|
|
|
|
test_fetch_redirect_updates_method_runner(
|
|
tx.clone(),
|
|
StatusCode::MOVED_PERMANENTLY,
|
|
Method::POST,
|
|
);
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
// make sure the test doesn't send more data than expected
|
|
assert_eq!(rx.try_recv().is_err(), true);
|
|
|
|
test_fetch_redirect_updates_method_runner(tx.clone(), StatusCode::FOUND, Method::POST);
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
assert_eq!(rx.try_recv().is_err(), true);
|
|
|
|
test_fetch_redirect_updates_method_runner(tx.clone(), StatusCode::SEE_OTHER, Method::GET);
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
assert_eq!(rx.try_recv().is_err(), true);
|
|
|
|
let extension = Method::from_bytes(b"FOO").unwrap();
|
|
|
|
test_fetch_redirect_updates_method_runner(
|
|
tx.clone(),
|
|
StatusCode::MOVED_PERMANENTLY,
|
|
extension.clone(),
|
|
);
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
// for MovedPermanently and Found, Method should only be changed if it was Post
|
|
assert_eq!(rx.recv().unwrap(), false);
|
|
assert_eq!(rx.try_recv().is_err(), true);
|
|
|
|
test_fetch_redirect_updates_method_runner(tx.clone(), StatusCode::FOUND, extension.clone());
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
assert_eq!(rx.recv().unwrap(), false);
|
|
assert_eq!(rx.try_recv().is_err(), true);
|
|
|
|
test_fetch_redirect_updates_method_runner(tx.clone(), StatusCode::SEE_OTHER, extension.clone());
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
// for SeeOther, Method should always be changed, so this should be true
|
|
assert_eq!(rx.recv().unwrap(), true);
|
|
assert_eq!(rx.try_recv().is_err(), true);
|
|
}
|
|
|
|
fn response_is_done(response: &Response) -> bool {
|
|
let response_complete = match response.response_type {
|
|
ResponseType::Default | ResponseType::Basic | ResponseType::Cors => {
|
|
(*response.body.lock().unwrap()).is_done()
|
|
},
|
|
// if the internal response cannot have a body, it shouldn't block the "done" state
|
|
ResponseType::Opaque | ResponseType::OpaqueRedirect | ResponseType::Error(..) => true,
|
|
};
|
|
|
|
let internal_complete = if let Some(ref res) = response.internal_response {
|
|
res.body.lock().unwrap().is_done()
|
|
} else {
|
|
true
|
|
};
|
|
|
|
response_complete && internal_complete
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_async_returns_complete_response() {
|
|
static MESSAGE: &'static [u8] = b"this message should be retrieved in full";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
let fetch_response = fetch(request, None);
|
|
|
|
let _ = server.close();
|
|
assert_eq!(response_is_done(&fetch_response), true);
|
|
}
|
|
|
|
#[test]
|
|
fn test_opaque_filtered_fetch_async_returns_complete_response() {
|
|
static MESSAGE: &'static [u8] = b"";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
let (server, url) = make_server(handler);
|
|
|
|
// an origin mis-match will fall through to an Opaque filtered response
|
|
let request = RequestBuilder::new(None, url, Referrer::NoReferrer).build();
|
|
let fetch_response = fetch(request, None);
|
|
|
|
let _ = server.close();
|
|
|
|
assert_eq!(fetch_response.response_type, ResponseType::Opaque);
|
|
assert_eq!(response_is_done(&fetch_response), true);
|
|
}
|
|
|
|
#[test]
|
|
fn test_opaque_redirect_filtered_fetch_async_returns_complete_response() {
|
|
static MESSAGE: &'static [u8] = b"";
|
|
let handler =
|
|
move |request: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
let redirects = request
|
|
.uri()
|
|
.path()
|
|
.split("/")
|
|
.collect::<String>()
|
|
.parse::<u32>()
|
|
.unwrap_or(0);
|
|
|
|
if redirects == 1 {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
} else {
|
|
*response.status_mut() = StatusCode::FOUND;
|
|
response
|
|
.headers_mut()
|
|
.insert(header::LOCATION, HeaderValue::from_static("1"));
|
|
}
|
|
};
|
|
|
|
let (server, url) = make_server(handler);
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.redirect_mode(RedirectMode::Manual)
|
|
.build();
|
|
|
|
let fetch_response = fetch(request, None);
|
|
|
|
let _ = server.close();
|
|
|
|
assert_eq!(fetch_response.response_type, ResponseType::OpaqueRedirect);
|
|
assert_eq!(response_is_done(&fetch_response), true);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(not(target_os = "windows"))]
|
|
fn test_fetch_with_devtools() {
|
|
static MESSAGE: &'static [u8] = b"Yay!";
|
|
let handler =
|
|
move |_: HyperRequest<Incoming>,
|
|
response: &mut HyperResponse<BoxBody<Bytes, hyper::Error>>| {
|
|
*response.body_mut() = make_body(MESSAGE.to_vec());
|
|
};
|
|
|
|
let (server, url) = make_server(handler);
|
|
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.redirect_mode(RedirectMode::Manual)
|
|
.pipeline_id(Some(TEST_PIPELINE_ID))
|
|
.build();
|
|
|
|
let (devtools_chan, devtools_port) = unbounded();
|
|
|
|
let _ = fetch(request, Some(devtools_chan));
|
|
let _ = server.close();
|
|
|
|
// notification received from devtools
|
|
let devhttprequest = expect_devtools_http_request(&devtools_port);
|
|
let mut devhttpresponse = expect_devtools_http_response(&devtools_port);
|
|
|
|
//Creating default headers for request
|
|
let mut headers = HeaderMap::new();
|
|
|
|
headers.insert(header::ACCEPT, HeaderValue::from_static("*/*"));
|
|
|
|
headers.insert(
|
|
header::ACCEPT_LANGUAGE,
|
|
HeaderValue::from_static("en-US,en;q=0.5"),
|
|
);
|
|
|
|
headers.typed_insert::<UserAgent>(DEFAULT_USER_AGENT.parse().unwrap());
|
|
|
|
headers.insert(
|
|
header::ACCEPT_ENCODING,
|
|
HeaderValue::from_static("gzip, deflate, br"),
|
|
);
|
|
|
|
// Append fetch metadata headers
|
|
headers.insert(
|
|
HeaderName::from_static("sec-fetch-dest"),
|
|
HeaderValue::from_static("empty"),
|
|
);
|
|
headers.insert(
|
|
HeaderName::from_static("sec-fetch-mode"),
|
|
HeaderValue::from_static("no-cors"),
|
|
);
|
|
headers.insert(
|
|
HeaderName::from_static("sec-fetch-site"),
|
|
HeaderValue::from_static("same-origin"),
|
|
);
|
|
|
|
let httprequest = DevtoolsHttpRequest {
|
|
url: url,
|
|
method: Method::GET,
|
|
headers: headers,
|
|
body: Some(vec![]),
|
|
pipeline_id: TEST_PIPELINE_ID,
|
|
started_date_time: devhttprequest.started_date_time,
|
|
time_stamp: devhttprequest.time_stamp,
|
|
connect_time: devhttprequest.connect_time,
|
|
send_time: devhttprequest.send_time,
|
|
is_xhr: true,
|
|
};
|
|
|
|
let content = "Yay!";
|
|
let mut response_headers = HeaderMap::new();
|
|
response_headers.typed_insert(ContentLength(content.len() as u64));
|
|
devhttpresponse
|
|
.headers
|
|
.as_mut()
|
|
.unwrap()
|
|
.remove(header::DATE);
|
|
|
|
let httpresponse = DevtoolsHttpResponse {
|
|
headers: Some(response_headers),
|
|
status: HttpStatus::default(),
|
|
body: None,
|
|
pipeline_id: TEST_PIPELINE_ID,
|
|
};
|
|
|
|
assert_eq!(devhttprequest, httprequest);
|
|
assert_eq!(devhttpresponse, httpresponse);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fetch_request_intercepted() {
|
|
static BODY_PART1: &[u8] = b"Request is";
|
|
static BODY_PART2: &[u8] = b" intercepted";
|
|
static EXPECTED_BODY: &[u8] = b"Request is intercepted";
|
|
static HEADERNAME: &str = "custom-header";
|
|
static HEADERVALUE: &str = "custom-value";
|
|
static STATUS_MESSAGE: &[u8] = b"custom status message";
|
|
|
|
let (embedder_proxy, embedder_receiver) = create_embedder_proxy_and_receiver();
|
|
|
|
std::thread::spawn(move || {
|
|
let embedder_msg = embedder_receiver.recv().unwrap();
|
|
match embedder_msg {
|
|
embedder_traits::EmbedderMsg::WebResourceRequested(
|
|
_,
|
|
web_resource_request,
|
|
response_sender,
|
|
) => {
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(
|
|
HeaderName::from_static(HEADERNAME),
|
|
HeaderValue::from_static(HEADERVALUE),
|
|
);
|
|
let response =
|
|
embedder_traits::WebResourceResponse::new(web_resource_request.url.clone())
|
|
.headers(headers)
|
|
.status_code(StatusCode::FOUND)
|
|
.status_message(STATUS_MESSAGE.to_vec());
|
|
let msg = embedder_traits::WebResourceResponseMsg::Start(response);
|
|
let _ = response_sender.send(msg);
|
|
let msg2 =
|
|
embedder_traits::WebResourceResponseMsg::SendBodyData(BODY_PART1.to_vec());
|
|
let _ = response_sender.send(msg2);
|
|
let msg3 =
|
|
embedder_traits::WebResourceResponseMsg::SendBodyData(BODY_PART2.to_vec());
|
|
let _ = response_sender.send(msg3);
|
|
let _ = response_sender.send(embedder_traits::WebResourceResponseMsg::FinishLoad);
|
|
},
|
|
_ => unreachable!(),
|
|
}
|
|
});
|
|
|
|
let mut context = FetchContext {
|
|
state: Arc::new(create_http_state(None)),
|
|
user_agent: DEFAULT_USER_AGENT.into(),
|
|
devtools_chan: None,
|
|
filemanager: Arc::new(Mutex::new(FileManager::new(
|
|
embedder_proxy.clone(),
|
|
Weak::new(),
|
|
))),
|
|
file_token: FileTokenCheck::NotRequired,
|
|
request_interceptor: Arc::new(Mutex::new(RequestInterceptor::new(embedder_proxy))),
|
|
cancellation_listener: Arc::new(Default::default()),
|
|
timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
|
|
ResourceTimingType::Navigation,
|
|
))),
|
|
protocols: Arc::new(ProtocolRegistry::default()),
|
|
};
|
|
|
|
let url = ServoUrl::parse("http://www.example.org").unwrap();
|
|
let request = RequestBuilder::new(None, url.clone(), Referrer::NoReferrer)
|
|
.origin(url.origin())
|
|
.build();
|
|
let response = fetch_with_context(request, &mut context);
|
|
|
|
assert!(
|
|
response
|
|
.headers
|
|
.get(HEADERNAME)
|
|
.map(|v| v == HEADERVALUE)
|
|
.unwrap_or(false),
|
|
"The custom header does not exist or has an incorrect value!"
|
|
);
|
|
|
|
let body = response.body.lock().unwrap();
|
|
match &*body {
|
|
ResponseBody::Done(data) => {
|
|
assert_eq!(data, &EXPECTED_BODY, "Body content does not match");
|
|
},
|
|
_ => panic!("Expected ResponseBody::Done, but got {:?}", *body),
|
|
}
|
|
|
|
assert_eq!(
|
|
response.status.code(),
|
|
StatusCode::FOUND,
|
|
"Status code does not match!"
|
|
);
|
|
|
|
assert_eq!(
|
|
response.status.message(),
|
|
STATUS_MESSAGE,
|
|
"The status_message was not set correctly!"
|
|
);
|
|
}
|