diff options
-rw-r--r-- | components/net/fetch/cors_cache.rs | 227 | ||||
-rw-r--r-- | components/net/fetch/methods.rs | 43 | ||||
-rw-r--r-- | components/net_traits/request.rs | 2 | ||||
-rw-r--r-- | tests/unit/net/fetch.rs | 61 |
4 files changed, 119 insertions, 214 deletions
diff --git a/components/net/fetch/cors_cache.rs b/components/net/fetch/cors_cache.rs index dc640ce4c37..1f6d39f71b7 100644 --- a/components/net/fetch/cors_cache.rs +++ b/components/net/fetch/cors_cache.rs @@ -12,7 +12,6 @@ use hyper::method::Method; use net_traits::request::Origin; use std::ascii::AsciiExt; -use std::sync::mpsc::{Sender, Receiver, channel}; use time; use time::{now, Timespec}; use url::Url; @@ -20,7 +19,7 @@ use url::Url; /// Union type for CORS cache entries /// /// Each entry might pertain to a header or method -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum HeaderOrMethod { HeaderData(String), MethodData(Method) @@ -43,7 +42,7 @@ impl HeaderOrMethod { } /// An entry in the CORS cache -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct CORSCacheEntry { pub origin: Origin, pub url: Url, @@ -74,51 +73,20 @@ pub struct CacheRequestDetails { pub credentials: bool } -/// Trait for a generic CORS Cache -pub trait CORSCache { - /// [Clear the cache](https://fetch.spec.whatwg.org/#concept-cache-clear) - fn clear (&mut self, request: CacheRequestDetails); - - /// Remove old entries - fn cleanup(&mut self); - - /// Returns true if an entry with a - /// [matching header](https://fetch.spec.whatwg.org/#concept-cache-match-header) is found - fn match_header(&mut self, request: CacheRequestDetails, header_name: &str) -> bool; - - /// Updates max age if an entry for a - /// [matching header](https://fetch.spec.whatwg.org/#concept-cache-match-header) is found. - /// - /// If not, it will insert an equivalent entry - fn match_header_and_update(&mut self, request: CacheRequestDetails, header_name: &str, new_max_age: u32) -> bool; - - /// Returns true if an entry with a - /// [matching method](https://fetch.spec.whatwg.org/#concept-cache-match-method) is found - fn match_method(&mut self, request: CacheRequestDetails, method: Method) -> bool; - - /// Updates max age if an entry for - /// [a matching method](https://fetch.spec.whatwg.org/#concept-cache-match-method) is found. - /// - /// If not, it will insert an equivalent entry - fn match_method_and_update(&mut self, request: CacheRequestDetails, method: Method, new_max_age: u32) -> bool; - /// Insert an entry - fn insert(&mut self, entry: CORSCacheEntry); -} - -/// A simple, vector-based CORS Cache -#[derive(Clone)] -pub struct BasicCORSCache(Vec<CORSCacheEntry>); - fn match_headers(cors_cache: &CORSCacheEntry, cors_req: &CacheRequestDetails) -> bool { cors_cache.origin == cors_req.origin && cors_cache.url == cors_req.destination && - cors_cache.credentials == cors_req.credentials + (cors_cache.credentials || !cors_req.credentials) } -impl BasicCORSCache { +/// A simple, vector-based CORS Cache +#[derive(Clone)] +pub struct CORSCache(Vec<CORSCacheEntry>); + +impl CORSCache { - pub fn new() -> BasicCORSCache { - BasicCORSCache(vec![]) + pub fn new() -> CORSCache { + CORSCache(vec![]) } fn find_entry_by_header<'a>(&'a mut self, request: &CacheRequestDetails, @@ -133,33 +101,37 @@ impl BasicCORSCache { self.cleanup(); self.0.iter_mut().find(|e| match_headers(e, request) && e.header_or_method.match_method(&method)) } -} -impl CORSCache for BasicCORSCache { - /// https://fetch.spec.whatwg.org/#concept-cache-clear - #[allow(dead_code)] - fn clear (&mut self, request: CacheRequestDetails) { - let BasicCORSCache(buf) = self.clone(); + /// [Clear the cache](https://fetch.spec.whatwg.org/#concept-cache-clear) + pub fn clear (&mut self, request: CacheRequestDetails) { + let CORSCache(buf) = self.clone(); let new_buf: Vec<CORSCacheEntry> = buf.into_iter().filter(|e| e.origin == request.origin && request.destination == e.url).collect(); - *self = BasicCORSCache(new_buf); + *self = CORSCache(new_buf); } - // Remove old entries - fn cleanup(&mut self) { - let BasicCORSCache(buf) = self.clone(); + /// Remove old entries + pub fn cleanup(&mut self) { + let CORSCache(buf) = self.clone(); let now = time::now().to_timespec(); let new_buf: Vec<CORSCacheEntry> = buf.into_iter() - .filter(|e| now.sec > e.created.sec + e.max_age as i64) + .filter(|e| now.sec < e.created.sec + e.max_age as i64) .collect(); - *self = BasicCORSCache(new_buf); + *self = CORSCache(new_buf); } - fn match_header(&mut self, request: CacheRequestDetails, header_name: &str) -> bool { + /// Returns true if an entry with a + /// [matching header](https://fetch.spec.whatwg.org/#concept-cache-match-header) is found + pub fn match_header(&mut self, request: CacheRequestDetails, header_name: &str) -> bool { self.find_entry_by_header(&request, header_name).is_some() } - fn match_header_and_update(&mut self, request: CacheRequestDetails, header_name: &str, new_max_age: u32) -> bool { + /// Updates max age if an entry for a + /// [matching header](https://fetch.spec.whatwg.org/#concept-cache-match-header) is found. + /// + /// If not, it will insert an equivalent entry + pub fn match_header_and_update(&mut self, request: CacheRequestDetails, + header_name: &str, new_max_age: u32) -> bool { match self.find_entry_by_header(&request, header_name).map(|e| e.max_age = new_max_age) { Some(_) => true, None => { @@ -171,11 +143,17 @@ impl CORSCache for BasicCORSCache { } } - fn match_method(&mut self, request: CacheRequestDetails, method: Method) -> bool { + /// Returns true if an entry with a + /// [matching method](https://fetch.spec.whatwg.org/#concept-cache-match-method) is found + pub fn match_method(&mut self, request: CacheRequestDetails, method: Method) -> bool { self.find_entry_by_method(&request, method).is_some() } - fn match_method_and_update(&mut self, request: CacheRequestDetails, method: Method, new_max_age: u32) -> bool { + /// Updates max age if an entry for + /// [a matching method](https://fetch.spec.whatwg.org/#concept-cache-match-method) is found. + /// + /// If not, it will insert an equivalent entry + pub fn match_method_and_update(&mut self, request: CacheRequestDetails, method: Method, new_max_age: u32) -> bool { match self.find_entry_by_method(&request, method.clone()).map(|e| e.max_age = new_max_age) { Some(_) => true, None => { @@ -186,138 +164,9 @@ impl CORSCache for BasicCORSCache { } } - fn insert(&mut self, entry: CORSCacheEntry) { + /// Insert an entry + pub fn insert(&mut self, entry: CORSCacheEntry) { self.cleanup(); self.0.push(entry); } } - -/// Various messages that can be sent to a CORSCacheThread -pub enum CORSCacheThreadMsg { - Clear(CacheRequestDetails, Sender<()>), - Cleanup(Sender<()>), - MatchHeader(CacheRequestDetails, String, Sender<bool>), - MatchHeaderUpdate(CacheRequestDetails, String, u32, Sender<bool>), - MatchMethod(CacheRequestDetails, Method, Sender<bool>), - MatchMethodUpdate(CacheRequestDetails, Method, u32, Sender<bool>), - Insert(CORSCacheEntry, Sender<()>), - ExitMsg -} - -/// A Sender to a CORSCacheThread -/// -/// This can be used as a CORS Cache. -/// The methods on this type block until they can run, and it behaves similar to a mutex -pub type CORSCacheSender = Sender<CORSCacheThreadMsg>; - -impl CORSCache for CORSCacheSender { - fn clear (&mut self, request: CacheRequestDetails) { - let (tx, rx) = channel(); - let _ = self.send(CORSCacheThreadMsg::Clear(request, tx)); - let _ = rx.recv(); - } - - fn cleanup(&mut self) { - let (tx, rx) = channel(); - let _ = self.send(CORSCacheThreadMsg::Cleanup(tx)); - let _ = rx.recv(); - } - - fn match_header(&mut self, request: CacheRequestDetails, header_name: &str) -> bool { - let (tx, rx) = channel(); - let _ = self.send(CORSCacheThreadMsg::MatchHeader(request, header_name.to_owned(), tx)); - rx.recv().unwrap_or(false) - } - - fn match_header_and_update(&mut self, request: CacheRequestDetails, header_name: &str, new_max_age: u32) -> bool { - let (tx, rx) = channel(); - let _ = self.send(CORSCacheThreadMsg::MatchHeaderUpdate(request, header_name.to_owned(), new_max_age, tx)); - rx.recv().unwrap_or(false) - } - - fn match_method(&mut self, request: CacheRequestDetails, method: Method) -> bool { - let (tx, rx) = channel(); - let _ = self.send(CORSCacheThreadMsg::MatchMethod(request, method, tx)); - rx.recv().unwrap_or(false) - } - - fn match_method_and_update(&mut self, request: CacheRequestDetails, method: Method, new_max_age: u32) -> bool { - let (tx, rx) = channel(); - let _ = self.send(CORSCacheThreadMsg::MatchMethodUpdate(request, method, new_max_age, tx)); - rx.recv().unwrap_or(false) - } - - fn insert(&mut self, entry: CORSCacheEntry) { - let (tx, rx) = channel(); - let _ = self.send(CORSCacheThreadMsg::Insert(entry, tx)); - let _ = rx.recv(); - } -} - -/// A simple thread-based CORS Cache that can be sent messages -/// -/// #Example -/// ```ignore -/// let thread = CORSCacheThread::new(); -/// let builder = ThreadBuilder::new().named("XHRThread"); -/// let mut sender = thread.sender(); -/// builder.spawn(move || { thread.run() }); -/// sender.insert(CORSCacheEntry::new(/* parameters here */)); -/// ``` -pub struct CORSCacheThread { - receiver: Receiver<CORSCacheThreadMsg>, - cache: BasicCORSCache, - sender: CORSCacheSender -} - -impl CORSCacheThread { - pub fn new() -> CORSCacheThread { - let (tx, rx) = channel(); - CORSCacheThread { - receiver: rx, - cache: BasicCORSCache(vec![]), - sender: tx - } - } - - /// Provides a sender to the cache thread - pub fn sender(&self) -> CORSCacheSender { - self.sender.clone() - } - - /// Runs the cache thread - /// This blocks the current thread, so it is advised - /// to spawn a new thread for this - /// Send ExitMsg to the associated Sender to exit - pub fn run(&mut self) { - loop { - match self.receiver.recv().unwrap() { - CORSCacheThreadMsg::Clear(request, tx) => { - self.cache.clear(request); - let _ = tx.send(()); - }, - CORSCacheThreadMsg::Cleanup(tx) => { - self.cache.cleanup(); - let _ = tx.send(()); - }, - CORSCacheThreadMsg::MatchHeader(request, header, tx) => { - let _ = tx.send(self.cache.match_header(request, &header)); - }, - CORSCacheThreadMsg::MatchHeaderUpdate(request, header, new_max_age, tx) => { - let _ = tx.send(self.cache.match_header_and_update(request, &header, new_max_age)); - }, - CORSCacheThreadMsg::MatchMethod(request, method, tx) => { - let _ = tx.send(self.cache.match_method(request, method)); - }, - CORSCacheThreadMsg::MatchMethodUpdate(request, method, new_max_age, tx) => { - let _ = tx.send(self.cache.match_method_and_update(request, method, new_max_age)); - }, - CORSCacheThreadMsg::Insert(entry, tx) => { - self.cache.insert(entry); - let _ = tx.send(()); - }, - CORSCacheThreadMsg::ExitMsg => break - } - } - } -} diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index 479c9bc9951..868d284707f 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use data_loader::decode; -use fetch::cors_cache::{BasicCORSCache, CORSCache, CacheRequestDetails}; +use fetch::cors_cache::{CORSCache, CacheRequestDetails}; use http_loader::{NetworkHttpRequestFactory, create_http_connector, obtain_response}; use hyper::header::{Accept, AcceptLanguage, Authorization, AccessControlAllowCredentials}; use hyper::header::{AccessControlAllowOrigin, AccessControlAllowHeaders, AccessControlAllowMethods}; @@ -40,6 +40,10 @@ pub fn fetch_async(request: Request, listener: Box<AsyncFetchListener + Send>) { /// [Fetch](https://fetch.spec.whatwg.org#concept-fetch) pub fn fetch(request: Rc<Request>) -> Response { + fetch_with_cors_cache(request, &mut CORSCache::new()) +} + +pub fn fetch_with_cors_cache(request: Rc<Request>, cache: &mut CORSCache) -> Response { // Step 1 if request.window.get() == Window::Client { @@ -102,11 +106,11 @@ pub fn fetch(request: Rc<Request>) -> Response { // TODO: create a fetch record and append it to request's client's fetch group list } // Step 7 - main_fetch(request, false, false) + main_fetch(request, cache, false, false) } /// [Main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch) -fn main_fetch(request: Rc<Request>, cors_flag: bool, recursive_flag: bool) -> Response { +fn main_fetch(request: Rc<Request>, cache: &mut CORSCache, cors_flag: bool, recursive_flag: bool) -> Response { // TODO: Implement main fetch spec // Step 1 @@ -156,14 +160,14 @@ fn main_fetch(request: Rc<Request>, cors_flag: bool, recursive_flag: bool) -> Re current_url.scheme() == "about" || request.mode == RequestMode::Navigate { - basic_fetch(request.clone()) + basic_fetch(request.clone(), cache) } else if request.mode == RequestMode::SameOrigin { Response::network_error() } else if request.mode == RequestMode::NoCORS { request.response_tainting.set(ResponseTainting::Opaque); - basic_fetch(request.clone()) + basic_fetch(request.clone(), cache) } else if !matches!(current_url.scheme(), "http" | "https") { Response::network_error() @@ -175,7 +179,7 @@ fn main_fetch(request: Rc<Request>, cors_flag: bool, recursive_flag: bool) -> Re request.response_tainting.set(ResponseTainting::CORSTainting); request.redirect_mode.set(RedirectMode::Error); - let response = http_fetch(request.clone(), BasicCORSCache::new(), true, true, false); + let response = http_fetch(request.clone(), cache, true, true, false); if response.is_network_error() { // TODO clear cache entries using request } @@ -183,7 +187,7 @@ fn main_fetch(request: Rc<Request>, cors_flag: bool, recursive_flag: bool) -> Re } else { request.response_tainting.set(ResponseTainting::CORSTainting); - http_fetch(request.clone(), BasicCORSCache::new(), true, false, false) + http_fetch(request.clone(), cache, true, false, false) } } }; @@ -280,7 +284,7 @@ fn main_fetch(request: Rc<Request>, cors_flag: bool, recursive_flag: bool) -> Re } /// [Basic fetch](https://fetch.spec.whatwg.org#basic-fetch) -fn basic_fetch(request: Rc<Request>) -> Response { +fn basic_fetch(request: Rc<Request>, cache: &mut CORSCache) -> Response { let url = request.current_url(); @@ -294,7 +298,7 @@ fn basic_fetch(request: Rc<Request>) -> Response { }, "http" | "https" => { - http_fetch(request.clone(), BasicCORSCache::new(), false, false, false) + http_fetch(request.clone(), cache, false, false, false) }, "data" => { @@ -324,7 +328,7 @@ fn basic_fetch(request: Rc<Request>) -> Response { /// [HTTP fetch](https://fetch.spec.whatwg.org#http-fetch) fn http_fetch(request: Rc<Request>, - mut cache: BasicCORSCache, + cache: &mut CORSCache, cors_flag: bool, cors_preflight_flag: bool, authentication_fetch_flag: bool) -> Response { @@ -394,7 +398,7 @@ fn http_fetch(request: Rc<Request>, // Sub-substep 1 if method_mismatch || header_mismatch { - let preflight_result = cors_preflight_fetch(request.clone(), Some(cache)); + let preflight_result = cors_preflight_fetch(request.clone(), cache); // Sub-substep 2 if preflight_result.response_type == ResponseType::Error { return Response::network_error(); @@ -443,7 +447,7 @@ fn http_fetch(request: Rc<Request>, RedirectMode::Follow => { // set back to default response.return_internal.set(true); - http_redirect_fetch(request, Rc::new(response), cors_flag) + http_redirect_fetch(request, cache, Rc::new(response), cors_flag) } } }, @@ -466,7 +470,7 @@ fn http_fetch(request: Rc<Request>, } // Step 4 - return http_fetch(request, BasicCORSCache::new(), cors_flag, cors_preflight_flag, true); + return http_fetch(request, cache, cors_flag, cors_preflight_flag, true); } // Code 407 @@ -482,7 +486,7 @@ fn http_fetch(request: Rc<Request>, // TODO: Prompt the user for proxy authentication credentials // Step 4 - return http_fetch(request, BasicCORSCache::new(), + return http_fetch(request, cache, cors_flag, cors_preflight_flag, authentication_fetch_flag); } @@ -503,6 +507,7 @@ fn http_fetch(request: Rc<Request>, /// [HTTP redirect fetch](https://fetch.spec.whatwg.org#http-redirect-fetch) fn http_redirect_fetch(request: Rc<Request>, + cache: &mut CORSCache, response: Rc<Response>, cors_flag: bool) -> Response { @@ -580,7 +585,7 @@ fn http_redirect_fetch(request: Rc<Request>, request.url_list.borrow_mut().push(location_url); // Step 15 - main_fetch(request, cors_flag, true) + main_fetch(request, cache, cors_flag, true) } /// [HTTP network or cache fetch](https://fetch.spec.whatwg.org#http-network-or-cache-fetch) @@ -917,7 +922,7 @@ fn http_network_fetch(request: Rc<Request>, } /// [CORS preflight fetch](https://fetch.spec.whatwg.org#cors-preflight-fetch) -fn cors_preflight_fetch(request: Rc<Request>, cache: Option<BasicCORSCache>) -> Response { +fn cors_preflight_fetch(request: Rc<Request>, cache: &mut CORSCache) -> Response { // Step 1 let mut preflight = Request::new(request.current_url(), Some(request.origin.borrow().clone()), false); *preflight.method.borrow_mut() = Method::Options; @@ -995,12 +1000,6 @@ fn cors_preflight_fetch(request: Rc<Request>, cache: Option<BasicCORSCache>) -> // TODO: Substep 9 - Need to define what an imposed limit on max-age is - // Substep 10 - let mut cache = match cache { - Some(c) => c, - None => return response - }; - // Substep 11, 12 for method in &methods { cache.match_method_and_update(CacheRequestDetails { diff --git a/components/net_traits/request.rs b/components/net_traits/request.rs index e02529263c5..675a78fd788 100644 --- a/components/net_traits/request.rs +++ b/components/net_traits/request.rs @@ -33,7 +33,7 @@ pub enum Destination { } /// A request [origin](https://fetch.spec.whatwg.org/#concept-request-origin) -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Debug)] pub enum Origin { Client, Origin(UrlOrigin) diff --git a/tests/unit/net/fetch.rs b/tests/unit/net/fetch.rs index 8b95314c726..98b15fd8b8c 100644 --- a/tests/unit/net/fetch.rs +++ b/tests/unit/net/fetch.rs @@ -3,7 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use hyper::header::{AccessControlAllowCredentials, AccessControlAllowHeaders, AccessControlAllowOrigin}; -use hyper::header::{AccessControlAllowMethods, AccessControlRequestHeaders, AccessControlRequestMethod}; +use hyper::header::{AccessControlAllowMethods, AccessControlMaxAge}; +use hyper::header::{AccessControlRequestHeaders, AccessControlRequestMethod}; use hyper::header::{CacheControl, ContentLanguage, ContentType, Expires, LastModified}; use hyper::header::{Headers, HttpDate, Location, SetCookie, Pragma}; use hyper::method::Method; @@ -12,7 +13,8 @@ use hyper::server::{Handler, Listening, Server}; use hyper::server::{Request as HyperRequest, Response as HyperResponse}; use hyper::status::StatusCode; use hyper::uri::RequestUri; -use net::fetch::methods::{fetch, fetch_async}; +use net::fetch::cors_cache::{CacheRequestDetails, CORSCache}; +use net::fetch::methods::{fetch, fetch_async, fetch_with_cors_cache}; use net_traits::AsyncFetchListener; use net_traits::request::{Origin, RedirectMode, Referer, Request, RequestMode}; use net_traits::response::{CacheState, Response, ResponseBody, ResponseType}; @@ -175,6 +177,61 @@ fn test_cors_preflight_fetch() { } #[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::new(); + 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::<AccessControlRequestMethod>()); + assert!(request.headers.has::<AccessControlRequestHeaders>()); + response.headers_mut().set(AccessControlAllowOrigin::Any); + response.headers_mut().set(AccessControlAllowCredentials); + response.headers_mut().set(AccessControlAllowMethods(vec![Method::Get])); + response.headers_mut().set(AccessControlMaxAge(6000)); + } else { + response.headers_mut().set(AccessControlAllowOrigin::Any); + response.send(ACK).unwrap(); + } + }; + let (mut server, url) = make_server(handler); + + let origin = Origin::Origin(UrlOrigin::new_opaque()); + let mut request = Request::new(url.clone(), Some(origin.clone()), false); + request.referer = Referer::NoReferer; + request.use_cors_preflight = true; + request.mode = RequestMode::CORSMode; + let wrapped_request0 = Rc::new(request.clone()); + let wrapped_request1 = Rc::new(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(CacheRequestDetails { + origin: origin, + destination: url, + credentials: false + }, 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)); |