diff --git a/components/constellation/network_listener.rs b/components/constellation/network_listener.rs index 239a6089f40..038d304fae4 100644 --- a/components/constellation/network_listener.rs +++ b/components/constellation/network_listener.rs @@ -103,6 +103,7 @@ impl NetworkListener { self.res_init = Some(ResponseInit { url: metadata.final_url.clone(), + location_url: metadata.location_url.clone(), headers: headers.clone().into_inner(), referrer: metadata.referrer.clone(), }); diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index 59da722fb03..75a829c46d9 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -10,14 +10,14 @@ use filemanager_thread::FileManager; use http_loader::{HttpState, determine_request_referrer, http_fetch}; use http_loader::{set_default_accept, set_default_accept_language}; use hyper::{Error, Result as HyperResult}; -use hyper::header::{Accept, AcceptLanguage, ContentLanguage, ContentType}; +use hyper::header::{Accept, AcceptLanguage, AccessControlExposeHeaders, ContentLanguage, ContentType}; use hyper::header::{Header, HeaderFormat, HeaderView, Headers, Referer as RefererHeader}; use hyper::method::Method; use hyper::mime::{Mime, SubLevel, TopLevel}; use hyper::status::StatusCode; use mime_guess::guess_mime_type; use net_traits::{FetchTaskTarget, NetworkError, ReferrerPolicy}; -use net_traits::request::{Referrer, Request, RequestMode, ResponseTainting}; +use net_traits::request::{CredentialsMode, Referrer, Request, RequestMode, ResponseTainting}; use net_traits::request::{Type, Origin, Window}; use net_traits::response::{Response, ResponseBody, ResponseType}; use servo_url::ServoUrl; @@ -107,9 +107,8 @@ pub fn main_fetch(request: &mut Request, // Step 2. if request.local_urls_only { - match request.current_url().scheme() { - "about" | "blob" | "data" | "filesystem" => (), // Ok, the URL is local. - _ => response = Some(Response::network_error(NetworkError::Internal("Non-local scheme".into()))) + if !matches!(request.current_url().scheme(), "about" | "blob" | "data" | "filesystem") { + response = Some(Response::network_error(NetworkError::Internal("Non-local scheme".into()))); } } @@ -130,8 +129,7 @@ pub fn main_fetch(request: &mut Request, // TODO: handle request's client's referrer policy. // Step 7. - let referrer_policy = request.referrer_policy.unwrap_or(ReferrerPolicy::NoReferrerWhenDowngrade); - request.referrer_policy = Some(referrer_policy); + request.referrer_policy = request.referrer_policy.or(Some(ReferrerPolicy::NoReferrerWhenDowngrade)); // Step 8. { @@ -147,7 +145,7 @@ pub fn main_fetch(request: &mut Request, request.headers.remove::(); let current_url = request.current_url().clone(); determine_request_referrer(&mut request.headers, - referrer_policy, + request.referrer_policy.unwrap(), url, current_url) } @@ -168,7 +166,7 @@ pub fn main_fetch(request: &mut Request, // Not applicable: see fetch_async. // Step 12. - let response = response.unwrap_or_else(|| { + let mut response = response.unwrap_or_else(|| { let current_url = request.current_url(); let same_origin = if let Origin::Origin(ref origin) = request.origin { *origin == current_url.origin() @@ -178,35 +176,50 @@ pub fn main_fetch(request: &mut Request, if (same_origin && !cors_flag ) || current_url.scheme() == "data" || - current_url.scheme() == "file" || + current_url.scheme() == "file" || // FIXME: Fetch spec has already dropped filtering against file: + // and about: schemes, but CSS tests will break on loading Ahem + // since we load them through a file: URL. current_url.scheme() == "about" || request.mode == RequestMode::Navigate { - basic_fetch(request, cache, target, done_chan, context) + // Substep 1. + request.response_tainting = ResponseTainting::Basic; + + // Substep 2. + scheme_fetch(request, cache, target, done_chan, context) } else if request.mode == RequestMode::SameOrigin { Response::network_error(NetworkError::Internal("Cross-origin response".into())) } else if request.mode == RequestMode::NoCors { + // Substep 1. request.response_tainting = ResponseTainting::Opaque; - basic_fetch(request, cache, target, done_chan, context) + + // Substep 2. + scheme_fetch(request, cache, target, done_chan, context) } else if !matches!(current_url.scheme(), "http" | "https") { Response::network_error(NetworkError::Internal("Non-http scheme".into())) } else if request.use_cors_preflight || (request.unsafe_request && - (!is_simple_method(&request.method) || - request.headers.iter().any(|h| !is_simple_header(&h)))) { + (!is_cors_safelisted_method(&request.method) || + request.headers.iter().any(|h| !is_cors_safelisted_request_header(&h)))) { + // Substep 1. request.response_tainting = ResponseTainting::CorsTainting; + // Substep 2. let response = http_fetch(request, cache, true, true, false, target, done_chan, context); + // Substep 3. if response.is_network_error() { // TODO clear cache entries using request } + // Substep 4. response } else { + // Substep 1. request.response_tainting = ResponseTainting::CorsTainting; + // Substep 2. http_fetch(request, cache, true, false, false, target, done_chan, context) } }); @@ -217,9 +230,28 @@ pub fn main_fetch(request: &mut Request, } // Step 14. - // We don't need to check whether response is a network error, - // given its type would not be `Default` in this case. - let mut response = if response.response_type == ResponseType::Default { + let mut response = if !response.is_network_error() && response.internal_response.is_none() { + // Substep 1. + if request.response_tainting == ResponseTainting::CorsTainting { + // Subsubstep 1. + let header_names = response.headers.get::(); + match header_names { + // Subsubstep 2. + Some(list) if request.credentials_mode != CredentialsMode::Include => { + if list.len() == 1 && list[0] == "*" { + response.cors_exposed_header_name_list = + response.headers.iter().map(|h| h.name().to_owned()).collect(); + } + }, + // Subsubstep 3. + Some(list) => { + response.cors_exposed_header_name_list = list.iter().map(|h| (**h).clone()).collect(); + }, + _ => (), + } + } + + // Substep 2. let response_type = match request.response_tainting { ResponseTainting::Basic => ResponseType::Basic, ResponseTainting::CorsTainting => ResponseType::Cors, @@ -365,7 +397,7 @@ fn wait_for_response(response: &Response, target: Target, done_chan: &mut DoneCh let body = response.body.lock().unwrap(); if let ResponseBody::Done(ref vec) = *body { // in case there was no channel to wait for, the body was - // obtained synchronously via basic_fetch for data/file/about/etc + // obtained synchronously via scheme_fetch for data/file/about/etc // We should still send the body across as a chunk target.process_response_chunk(vec.clone()); } else { @@ -374,8 +406,8 @@ fn wait_for_response(response: &Response, target: Target, done_chan: &mut DoneCh } } -/// [Basic fetch](https://fetch.spec.whatwg.org#basic-fetch) -fn basic_fetch(request: &mut Request, +/// [Scheme fetch](https://fetch.spec.whatwg.org#scheme-fetch) +fn scheme_fetch(request: &mut Request, cache: &mut CorsCache, target: Target, done_chan: &mut DoneChannel, @@ -463,7 +495,7 @@ fn basic_fetch(request: &mut Request, } /// https://fetch.spec.whatwg.org/#cors-safelisted-request-header -pub fn is_simple_header(h: &HeaderView) -> bool { +pub fn is_cors_safelisted_request_header(h: &HeaderView) -> bool { if h.is::() { match h.value() { Some(&ContentType(Mime(TopLevel::Text, SubLevel::Plain, _))) | @@ -477,7 +509,8 @@ pub fn is_simple_header(h: &HeaderView) -> bool { } } -pub fn is_simple_method(m: &Method) -> bool { +/// https://fetch.spec.whatwg.org/#cors-safelisted-method +pub fn is_cors_safelisted_method(m: &Method) -> bool { match *m { Method::Get | Method::Head | Method::Post => true, _ => false diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index 6c59669a750..c96858b9a9e 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -9,7 +9,8 @@ use cookie_storage::CookieStorage; use devtools_traits::{ChromeToDevtoolsControlMsg, DevtoolsControlMsg, HttpRequest as DevtoolsHttpRequest}; use devtools_traits::{HttpResponse as DevtoolsHttpResponse, NetworkEvent}; use fetch::cors_cache::CorsCache; -use fetch::methods::{Data, DoneChannel, FetchContext, Target, is_simple_header, is_simple_method, main_fetch}; +use fetch::methods::{Data, DoneChannel, FetchContext, Target}; +use fetch::methods::{is_cors_safelisted_request_header, is_cors_safelisted_method, main_fetch}; use flate2::read::{DeflateDecoder, GzDecoder}; use hsts::HstsList; use hyper::Error as HttpError; @@ -38,6 +39,7 @@ use net_traits::request::{ResponseTainting, ServiceWorkersMode, Type}; use net_traits::response::{HttpsState, Response, ResponseBody, ResponseType}; use resource_thread::AuthCache; use servo_url::{ImmutableOrigin, ServoUrl}; +use std::ascii::AsciiExt; use std::collections::HashSet; use std::error::Error; use std::io::{self, Read, Write}; @@ -529,7 +531,7 @@ pub fn http_fetch(request: &mut Request, // Substep 2 if response.is_none() && request.is_subresource_request() && match request.origin { - Origin::Origin(ref origin) if *origin == request.url().origin() => true, + Origin::Origin(ref origin) => *origin == request.url().origin(), _ => false, } { // TODO (handle foreign fetch unimplemented) @@ -559,24 +561,16 @@ pub fn http_fetch(request: &mut Request, } // Step 4 - let credentials = match request.credentials_mode { - CredentialsMode::Include => true, - CredentialsMode::CredentialsSameOrigin if request.response_tainting == ResponseTainting::Basic - => true, - _ => false - }; - - // Step 5 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_simple_method(&request.method) || + let method_mismatch = !method_cache_match && (!is_cors_safelisted_method(&request.method) || request.use_cors_preflight); let header_mismatch = request.headers.iter().any(|view| - !cache.match_header(&*request, view.name()) && !is_simple_header(&view) + !cache.match_header(&*request, view.name()) && !is_cors_safelisted_request_header(&view) ); // Sub-substep 1 @@ -611,81 +605,37 @@ pub fn http_fetch(request: &mut Request, let mut response = response.unwrap(); // Step 5 - match response.actual_response().status { - // Code 301, 302, 303, 307, 308 - status if status.map_or(false, is_redirect_status) => { - 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) - } - } - }, - - // Code 401 - Some(StatusCode::Unauthorized) => { - // Step 1 - // FIXME: Figure out what to do with request window objects - if cors_flag || !credentials { - return response; - } - - // Step 2 - // TODO: Spec says requires testing on multiple WWW-Authenticate headers - - // Step 3 - if !request.use_url_credentials || authentication_fetch_flag { - // TODO: 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; - } - - // Step 4 - return http_fetch(request, cache, cors_flag, cors_preflight_flag, - true, target, done_chan, context); + if response.actual_response().status.map_or(false, is_redirect_status) { + // Substep 1. + if response.actual_response().status.map_or(true, |s| s != StatusCode::SeeOther) { + // TODO: send RST_STREAM frame } - // Code 407 - Some(StatusCode::ProxyAuthenticationRequired) => { - // Step 1 - // TODO: Figure out what to do with request window objects + // Substep 2-3. + let location = response.actual_response().headers.get::().map( + |l| ServoUrl::parse_with_base(response.actual_response().url(), l) + .map_err(|err| err.description().into())); - // Step 2 - // TODO: Spec says requires testing on Proxy-Authenticate headers + // Substep 4. + response.actual_response_mut().location_url = location; - // Step 3 - // TODO: 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_fetch(request, cache, - // cors_flag, cors_preflight_flag, - // authentication_fetch_flag, target, - // done_chan, context); - } - - _ => { } + // 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) + } + }; } - - // Step 6 - if authentication_fetch_flag { - // TODO: Create authentication entry for this request - } - // set back to default response.return_internal = true; - // Step 7 + // Step 6 response } @@ -701,29 +651,20 @@ pub fn http_redirect_fetch(request: &mut Request, // Step 1 assert!(response.return_internal); - // Step 2 - if !response.actual_response().headers.has::() { - return response; - } - - // Step 3 - let location = match response.actual_response().headers.get::() { - Some(&Location(ref location)) => location.clone(), - _ => return Response::network_error(NetworkError::Internal("Location header parsing failure".into())) - }; - let response_url = response.actual_response().url().unwrap(); - let location_url = response_url.join(&*location); + let location_url = response.actual_response().location_url.clone(); let location_url = match location_url { - Ok(url) => url, - _ => return Response::network_error(NetworkError::Internal("Location URL parsing failure".into())) + // 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 4 - match location_url.scheme() { - "http" | "https" => { }, - _ => return Response::network_error(NetworkError::Internal("Not an HTTP(S) Scheme".into())) - } - // Step 5 if request.redirect_count >= 20 { return Response::network_error(NetworkError::Internal("Too many redirects".into())); @@ -733,7 +674,8 @@ pub fn http_redirect_fetch(request: &mut Request, request.redirect_count += 1; // Step 7 - let same_origin = location_url.origin()== request.current_url().origin(); + // FIXME: Correctly use request's origin + let same_origin = location_url.origin() == request.current_url().origin(); let has_credentials = has_credentials(&location_url); if request.mode == RequestMode::CorsMode && !same_origin && has_credentials { @@ -746,28 +688,38 @@ pub fn http_redirect_fetch(request: &mut Request, } // Step 9 + if response.actual_response().status.map_or(true, |s| s != StatusCode::SeeOther) && + 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 && !same_origin { request.origin = Origin::Origin(ImmutableOrigin::new_opaque()); } - // Step 10 - let status_code = response.actual_response().status.unwrap(); - if ((status_code == StatusCode::MovedPermanently || status_code == StatusCode::Found) && - request.method == Method::Post) || - status_code == StatusCode::SeeOther { + // Step 11 + if response.actual_response().status.map_or(false, |code| + ((code == StatusCode::MovedPermanently || code == StatusCode::Found) && request.method == Method::Post) || + code == StatusCode::SeeOther) { request.method = Method::Get; request.body = None; } - // Step 11 - request.url_list.push(location_url); - // Step 12 - // TODO implement referrer policy - - let recursive_flag = request.redirect_mode != RedirectMode::Manual; + 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; + main_fetch(request, cache, cors_flag, recursive_flag, target, done_chan, context) } @@ -826,9 +778,11 @@ fn http_network_or_cache_fetch(request: &mut Request, // Step 8 if let Some(content_length_value) = content_length_value { http_request.headers.set(ContentLength(content_length_value)); + if http_request.keep_alive { + // Step 9 TODO: needs request's client object + } } - // Step 9 TODO: needs request's client object // Step 10 match http_request.referrer { @@ -940,7 +894,7 @@ fn http_network_or_cache_fetch(request: &mut Request, let mut response: Option = None; // Step 20 - let mut revalidation_needed = false; + let mut revalidating_flag = false; // Step 21 // TODO have a HTTP cache to check for a completed response @@ -952,7 +906,7 @@ fn http_network_or_cache_fetch(request: &mut Request, // Substep 3 if let Some(ref response) = response { - revalidation_needed = response_needs_revalidation(&response); + revalidating_flag = response_needs_revalidation(&response); }; // Substep 4 @@ -962,7 +916,7 @@ fn http_network_or_cache_fetch(request: &mut Request, // response = http_request } - if revalidation_needed { + if revalidating_flag { // Substep 5 // TODO set If-None-Match and If-Modified-Since according to cached // response headers. @@ -984,82 +938,76 @@ fn http_network_or_cache_fetch(request: &mut Request, // Substep 2 let forward_response = http_network_fetch(http_request, credentials_flag, done_chan, context); - match forward_response.raw_status { - // Substep 3 - Some((200...303, _)) | - Some((305...399, _)) => { - if !http_request.method.safe() { - // TODO Invalidate HTTP cache response - } - }, - // Substep 4 - Some((304, _)) => { - if revalidation_needed { - // TODO update forward_response headers with cached response - // headers - } - }, - _ => {} + // Substep 3 + if let Some((200...399, _)) = forward_response.raw_status { + if !http_request.method.safe() { + // TODO Invalidate HTTP cache response + } + } + // Substep 4 + if revalidating_flag && forward_response.status.map_or(false, |s| s == StatusCode::NotModified) { + // TODO update forward_response headers with cached response headers } // Substep 5 if response.is_none() { + // Subsubstep 1 response = Some(forward_response); + // Subsubstep 2 + // TODO: store http_request and forward_response in cache } } - let response = response.unwrap(); + let mut response = response.unwrap(); - match response.status { - Some(StatusCode::Unauthorized) => { - // Step 23 - // FIXME: Figure out what to do with request window objects - if cors_flag && !credentials_flag { - return response; - } + // Step 23 + // FIXME: Figure out what to do with request window objects + if let (Some(StatusCode::Unauthorized), false, true) = (response.status, cors_flag, credentials_flag) { + // Substep 1 + // TODO: Spec says requires testing on multiple WWW-Authenticate headers - // 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 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 - // Substep 3 - if !http_request.use_url_credentials || authentication_fetch_flag { - // TODO: 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 - return http_network_or_cache_fetch(http_request, - true /* authentication flag */, - cors_flag, done_chan, context); - }, - Some(StatusCode::ProxyAuthenticationRequired) => { - // Step 24 - // Step 1 - // TODO: Figure out what to do with request window objects - - // Step 2 - // TODO: Spec says requires testing on Proxy-Authenticate headers - - // Step 3 - // TODO: 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); - }, - _ => {} + // Substep 4 + response = http_network_or_cache_fetch(http_request, + true /* authentication flag */, + cors_flag, done_chan, context); + } + + // Step 24 + if let Some(StatusCode::ProxyAuthenticationRequired) = response.status { + // 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 25 @@ -1077,8 +1025,6 @@ fn http_network_fetch(request: &Request, done_chan: &mut DoneChannel, context: &FetchContext) -> Response { - // TODO: Implement HTTP network fetch spec - // Step 1 // nothing to do here, since credentials_flag is already a boolean @@ -1089,6 +1035,9 @@ fn http_network_fetch(request: &Request, // 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(|_| { @@ -1203,10 +1152,10 @@ fn http_network_fetch(request: &Request, // TODO Read request - // Step 5-9 + // Step 6-11 // (needs stream bodies) - // Step 10 + // Step 12 // TODO when https://bugzilla.mozilla.org/show_bug.cgi?id=1030660 // is resolved, this step will become uneccesary // TODO this step @@ -1218,24 +1167,22 @@ fn http_network_fetch(request: &Request, } }; - // Step 11 + // Step 13 // TODO this step isn't possible yet (CSP) - // Step 12 - if response.is_network_error() && request.cache_mode == CacheMode::NoStore { + // Step 14 + if !response.is_network_error() && request.cache_mode != CacheMode::NoStore { // TODO update response in the HTTP cache for request } // TODO this step isn't possible yet - // Step 13 - - // Step 14. + // Step 15 if credentials_flag { set_cookies_from_headers(&url, &response.headers, &context.state.cookie_jar); } // TODO these steps - // Step 15 + // Step 16 // Substep 1 // Substep 2 // Sub-substep 1 @@ -1259,6 +1206,7 @@ fn cors_preflight_fetch(request: &Request, preflight.initiator = request.initiator.clone(); preflight.type_ = request.type_.clone(); preflight.destination = request.destination.clone(); + preflight.origin = request.origin.clone(); preflight.referrer = request.referrer.clone(); preflight.referrer_policy = request.referrer_policy; @@ -1266,86 +1214,107 @@ fn cors_preflight_fetch(request: &Request, preflight.headers.set::( AccessControlRequestMethod(request.method.clone())); - // Step 3, 4 - let mut value = request.headers + // Step 3 + let mut headers = request.headers .iter() - .filter(|view| !is_simple_header(view)) - .map(|view| UniCase(view.name().to_owned())) + .filter(|view| !is_cors_safelisted_request_header(view)) + .map(|view| UniCase(view.name().to_ascii_lowercase().to_owned())) .collect::>>(); - value.sort(); + headers.sort(); + + // Step 4 + if !headers.is_empty() { + preflight.headers.set::(AccessControlRequestHeaders(headers)); + } // Step 5 - preflight.headers.set::( - AccessControlRequestHeaders(value)); - - // Step 6 let response = http_network_or_cache_fetch(&mut preflight, false, false, &mut None, context); - // Step 7 + // Step 6 if cors_check(&request, &response).is_ok() && response.status.map_or(false, |status| status.is_success()) { - // Substep 1 + // Substep 1, 2 let mut methods = if response.headers.has::() { match response.headers.get::() { Some(&AccessControlAllowMethods(ref m)) => m.clone(), - // Substep 3 + // Substep 4 None => return Response::network_error(NetworkError::Internal("CORS ACAM check failed".into())) } } else { vec![] }; - // Substep 2 + // Substep 3 let header_names = if response.headers.has::() { match response.headers.get::() { Some(&AccessControlAllowHeaders(ref hn)) => hn.clone(), - // Substep 3 + // Substep 4 None => return Response::network_error(NetworkError::Internal("CORS ACAH check failed".into())) } } else { vec![] }; - // Substep 4 + // Substep 5 + if (methods.iter().any(|m| m.as_ref() == "*") || + header_names.iter().any(|hn| &**hn == "*")) && + 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 5 + // Substep 7 debug!("CORS check: Allowed methods: {:?}, current method: {:?}", methods, request.method); if methods.iter().all(|method| *method != request.method) && - !is_simple_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 6 + // Substep 8 + if request.headers.iter().any( + |header| header.name() == "authorization" && + header_names.iter().all(|hn| *hn != UniCase(header.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<&UniCase> = HashSet::from_iter(header_names.iter()); - if request.headers.iter().any(|ref hv| !set.contains(&UniCase(hv.name().to_owned())) && !is_simple_header(hv)) { + if request.headers.iter().any( + |ref hv| !set.contains(&UniCase(hv.name().to_owned())) && !is_cors_safelisted_request_header(hv)) { return Response::network_error(NetworkError::Internal("CORS headers check failed".into())); } - // Substep 7, 8 + // Substep 10, 11 let max_age = response.headers.get::().map(|acma| acma.0).unwrap_or(0); - // TODO: Substep 9 - Need to define what an imposed limit on max-age is + // Substep 12 + // TODO: Need to define what an imposed limit on max-age is - // Substep 11, 12 + // 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 13, 14 + // Substep 16, 17 for header_name in &header_names { cache.match_header_and_update(&*request, &*header_name, max_age); } - // Substep 15 + // Substep 18 return response; } - // Step 8 + // Step 7 Response::network_error(NetworkError::Internal("CORS check failed".into())) } diff --git a/components/net/websocket_loader.rs b/components/net/websocket_loader.rs index 71178bbc7d9..dc4253fb594 100644 --- a/components/net/websocket_loader.rs +++ b/components/net/websocket_loader.rs @@ -352,7 +352,7 @@ fn main_fetch(url: ServoUrl, // doesn't need to be filtered at all. // Step 12.2. - basic_fetch(&url, origin, &mut headers, http_state) + scheme_fetch(&url, origin, &mut headers, http_state) }); // Step 13. @@ -386,8 +386,8 @@ fn main_fetch(url: ServoUrl, response } -// https://fetch.spec.whatwg.org/#concept-basic-fetch -fn basic_fetch(url: &ServoUrl, +// https://fetch.spec.whatwg.org/#concept-scheme-fetch +fn scheme_fetch(url: &ServoUrl, origin: String, headers: &mut Headers, http_state: &HttpState) diff --git a/components/net_traits/lib.rs b/components/net_traits/lib.rs index 7e841fd645b..562f2e5a15d 100644 --- a/components/net_traits/lib.rs +++ b/components/net_traits/lib.rs @@ -411,6 +411,9 @@ pub struct Metadata { /// Final URL after redirects. pub final_url: ServoUrl, + /// Location URL from the response headers. + pub location_url: Option>, + #[ignore_heap_size_of = "Defined in hyper"] /// MIME type / subtype. pub content_type: Option>, @@ -440,6 +443,7 @@ impl Metadata { pub fn default(url: ServoUrl) -> Self { Metadata { final_url: url, + location_url: None, content_type: None, charset: None, headers: None, diff --git a/components/net_traits/request.rs b/components/net_traits/request.rs index 6946cdbef0f..9a5922b91cc 100644 --- a/components/net_traits/request.rs +++ b/components/net_traits/request.rs @@ -220,7 +220,7 @@ pub struct Request { // TODO: target browsing context /// https://fetch.spec.whatwg.org/#request-keepalive-flag pub keep_alive: bool, - // https://fetch.spec.whatwg.org/#request-service-workers-mode + /// https://fetch.spec.whatwg.org/#request-service-workers-mode pub service_workers_mode: ServiceWorkersMode, /// https://fetch.spec.whatwg.org/#concept-request-initiator pub initiator: Initiator, diff --git a/components/net_traits/response.rs b/components/net_traits/response.rs index 74525a85f07..823e26cad6f 100644 --- a/components/net_traits/response.rs +++ b/components/net_traits/response.rs @@ -82,6 +82,7 @@ pub struct ResponseInit { #[ignore_heap_size_of = "Defined in hyper"] pub headers: Headers, pub referrer: Option, + pub location_url: Option>, } /// A [Response](https://fetch.spec.whatwg.org/#concept-response) as defined by the Fetch spec @@ -102,12 +103,16 @@ pub struct Response { pub cache_state: CacheState, pub https_state: HttpsState, pub referrer: Option, + pub referrer_policy: Option, + /// [CORS-exposed header-name list](https://fetch.spec.whatwg.org/#concept-response-cors-exposed-header-name-list) + pub cors_exposed_header_name_list: Vec, + /// [Location URL](https://fetch.spec.whatwg.org/#concept-response-location-url) + pub location_url: Option>, /// [Internal response](https://fetch.spec.whatwg.org/#concept-internal-response), only used if the Response /// is a filtered response pub internal_response: Option>, /// whether or not to try to return the internal_response when asked for actual_response pub return_internal: bool, - pub referrer_policy: Option, } impl Response { @@ -125,6 +130,8 @@ impl Response { https_state: HttpsState::None, referrer: None, referrer_policy: None, + cors_exposed_header_name_list: vec![], + location_url: None, internal_response: None, return_internal: true, } @@ -132,6 +139,7 @@ impl Response { pub fn from_init(init: ResponseInit) -> Response { let mut res = Response::new(init.url); + res.location_url = init.location_url; res.headers = init.headers; res.referrer = init.referrer; res @@ -151,6 +159,8 @@ impl Response { https_state: HttpsState::None, referrer: None, referrer_policy: None, + cors_exposed_header_name_list: vec![], + location_url: None, internal_response: None, return_internal: true, } @@ -279,6 +289,7 @@ impl Response { Some(&ContentType(ref mime)) => Some(mime), None => None, }); + metadata.location_url = response.location_url.clone(); metadata.headers = Some(Serde(response.headers.clone())); metadata.status = response.raw_status.clone(); metadata.https_state = response.https_state; diff --git a/tests/unit/net/fetch.rs b/tests/unit/net/fetch.rs index 675ada47b5c..796ddf0ff18 100644 --- a/tests/unit/net/fetch.rs +++ b/tests/unit/net/fetch.rs @@ -208,7 +208,7 @@ fn test_cors_preflight_fetch() { let handler = move |request: HyperRequest, mut response: HyperResponse| { if request.method == Method::Options && state.clone().fetch_add(1, Ordering::SeqCst) == 0 { assert!(request.headers.has::()); - assert!(request.headers.has::()); + assert!(!request.headers.has::()); assert!(!request.headers.get::().unwrap().contains("a.html")); response.headers_mut().set(AccessControlAllowOrigin::Any); response.headers_mut().set(AccessControlAllowCredentials); @@ -247,7 +247,7 @@ fn test_cors_preflight_cache_fetch() { let handler = move |request: HyperRequest, mut response: HyperResponse| { if request.method == Method::Options && state.clone().fetch_add(1, Ordering::SeqCst) == 0 { assert!(request.headers.has::()); - assert!(request.headers.has::()); + assert!(!request.headers.has::()); response.headers_mut().set(AccessControlAllowOrigin::Any); response.headers_mut().set(AccessControlAllowCredentials); response.headers_mut().set(AccessControlAllowMethods(vec![Method::Get])); @@ -297,7 +297,7 @@ fn test_cors_preflight_fetch_network_error() { let handler = move |request: HyperRequest, mut response: HyperResponse| { if request.method == Method::Options && state.clone().fetch_add(1, Ordering::SeqCst) == 0 { assert!(request.headers.has::()); - assert!(request.headers.has::()); + assert!(!request.headers.has::()); response.headers_mut().set(AccessControlAllowOrigin::Any); response.headers_mut().set(AccessControlAllowCredentials); response.headers_mut().set(AccessControlAllowMethods(vec![Method::Get])); diff --git a/tests/wpt/metadata/fetch/api/cors/cors-preflight.any.js.ini b/tests/wpt/metadata/fetch/api/cors/cors-preflight.any.js.ini index 18a0447734d..f8538b0d5d1 100644 --- a/tests/wpt/metadata/fetch/api/cors/cors-preflight.any.js.ini +++ b/tests/wpt/metadata/fetch/api/cors/cors-preflight.any.js.ini @@ -1,11 +1,5 @@ [cors-preflight.any.html] type: testharness - [CORS [DELETE\], server allows] - expected: FAIL - - [CORS [PUT\], server allows] - expected: FAIL - [CORS [PATCH\], server allows] expected: FAIL @@ -27,18 +21,8 @@ [CORS [PUT\] [several headers\], server allows] expected: FAIL - [CORS [PUT\] [only safe headers\], server allows] - expected: FAIL - - [cors-preflight.any.worker.html] type: testharness - [CORS [DELETE\], server allows] - expected: FAIL - - [CORS [PUT\], server allows] - expected: FAIL - [CORS [PATCH\], server allows] expected: FAIL @@ -59,7 +43,3 @@ [CORS [PUT\] [several headers\], server allows] expected: FAIL - - [CORS [PUT\] [only safe headers\], server allows] - expected: FAIL -