/* 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 http://mozilla.org/MPL/2.0/. */ //! A partial implementation of CORS //! For now this library is XHR-specific. //! For stuff involving `<img>`, `<iframe>`, `<form>`, etc please check what //! the request mode should be and compare with the fetch spec //! This library will eventually become the core of the Fetch crate //! with CORSRequest being expanded into FetchRequest (etc) use network_listener::{NetworkListener, PreInvoke}; use script_task::ScriptChan; use net_traits::{AsyncResponseTarget, AsyncResponseListener, ResponseAction, Metadata}; use std::ascii::AsciiExt; use std::borrow::ToOwned; use std::cell::RefCell; use std::sync::{Arc, Mutex}; use time; use time::{now, Timespec}; use hyper::header::{AccessControlRequestMethod, AccessControlAllowMethods}; use hyper::header::{AccessControlMaxAge, AccessControlAllowOrigin}; use hyper::header::{AccessControlRequestHeaders, AccessControlAllowHeaders}; use hyper::header::{Headers, HeaderView}; use hyper::client::Request; use hyper::mime::{Mime, TopLevel, SubLevel}; use hyper::header::{ContentType, Host}; use hyper::method::Method; use hyper::status::StatusClass::Success; use unicase::UniCase; use url::{SchemeData, Url}; use util::task::spawn_named; /// Interface for network listeners concerned with CORS checks. Proper network requests /// should be initiated from this method, based on the response provided. pub trait AsyncCORSResponseListener { fn response_available(&self, response: CORSResponse); } #[derive(Clone)] pub struct CORSRequest { pub origin: Url, pub destination: Url, pub mode: RequestMode, pub method: Method, pub headers: Headers, /// CORS preflight flag (https://fetch.spec.whatwg.org/#concept-http-fetch) /// Indicates that a CORS preflight request and/or cache check is to be performed pub preflight_flag: bool } /// https://fetch.spec.whatwg.org/#concept-request-mode /// This only covers some of the request modes. The /// `same-origin` and `no CORS` modes are unnecessary for XHR. #[derive(PartialEq, Copy, Clone)] pub enum RequestMode { CORS, // CORS ForcedPreflight // CORS-with-forced-preflight } impl CORSRequest { /// Creates a CORS request if necessary. Will return an error when fetching is forbidden pub fn maybe_new(referer: Url, destination: Url, mode: RequestMode, method: Method, headers: Headers) -> Result<Option<CORSRequest>, ()> { if referer.scheme == destination.scheme && referer.host() == destination.host() && referer.port() == destination.port() { return Ok(None); // Not cross-origin, proceed with a normal fetch } match &*destination.scheme { // TODO: If the request's same origin data url flag is set (which isn't the case for XHR) // we can fetch a data URL normally. about:blank can also be fetched by XHR "http" | "https" => { let mut req = CORSRequest::new(referer, destination, mode, method, headers); req.preflight_flag = !is_simple_method(&req.method) || mode == RequestMode::ForcedPreflight; if req.headers.iter().all(|h| is_simple_header(&h)) { req.preflight_flag = true; } Ok(Some(req)) }, _ => Err(()), } } fn new(mut referer: Url, destination: Url, mode: RequestMode, method: Method, headers: Headers) -> CORSRequest { match referer.scheme_data { SchemeData::Relative(ref mut data) => data.path = vec!(), _ => {} }; referer.fragment = None; referer.query = None; CORSRequest { origin: referer, destination: destination, mode: mode, method: method, headers: headers, preflight_flag: false } } pub fn http_fetch_async(&self, listener: Box<AsyncCORSResponseListener+Send>, script_chan: Box<ScriptChan+Send>) { struct CORSContext { listener: Box<AsyncCORSResponseListener+Send>, response: RefCell<Option<CORSResponse>>, } // This is shoe-horning the CORSReponse stuff into the rest of the async network // framework right now. It would be worth redesigning http_fetch to do this properly. impl AsyncResponseListener for CORSContext { fn headers_available(&self, _metadata: Metadata) { } fn data_available(&self, _payload: Vec<u8>) { } fn response_complete(&self, _status: Result<(), String>) { let response = self.response.borrow_mut().take().unwrap(); self.listener.response_available(response); } } impl PreInvoke for CORSContext {} let context = CORSContext { listener: listener, response: RefCell::new(None), }; let listener = NetworkListener { context: Arc::new(Mutex::new(context)), script_chan: script_chan, }; // TODO: this exists only to make preflight check non-blocking // perhaps should be handled by the resource task? let req = self.clone(); spawn_named("cors".to_owned(), move || { let response = req.http_fetch(); let mut context = listener.context.lock(); let context = context.as_mut().unwrap(); *context.response.borrow_mut() = Some(response); listener.invoke_with_listener(ResponseAction::ResponseComplete(Ok(()))); }); } /// http://fetch.spec.whatwg.org/#concept-http-fetch /// This method assumes that the CORS flag is set /// This does not perform the full HTTP fetch, rather it handles part of the CORS filtering /// if self.mode is ForcedPreflight, then the CORS-with-forced-preflight /// fetch flag is set as well pub fn http_fetch(&self) -> CORSResponse { let response = CORSResponse::new(); // Step 2: Handle service workers (unimplemented) // Step 3 // Substep 1: Service workers (unimplemented ) // Substep 2 let cache = &mut CORSCache(vec!()); // XXXManishearth Should come from user agent if self.preflight_flag && !cache.match_method(self, &self.method) && !self.headers.iter().all(|h| is_simple_header(&h) && cache.match_header(self, h.name())) { if !is_simple_method(&self.method) || self.mode == RequestMode::ForcedPreflight { return self.preflight_fetch(); // Everything after this is part of XHR::fetch() // Expect the organization of code to improve once we have a fetch crate } } response } /// https://fetch.spec.whatwg.org/#cors-preflight-fetch fn preflight_fetch(&self) -> CORSResponse { let error = CORSResponse::new_error(); let mut cors_response = CORSResponse::new(); let mut preflight = self.clone(); // Step 1 preflight.method = Method::Options; // Step 2 preflight.headers = Headers::new(); // Step 3 // Step 4 preflight.headers.set(AccessControlRequestMethod(self.method.clone())); // Step 5 - 7 let mut header_names = vec!(); for header in self.headers.iter() { header_names.push(header.name().to_owned()); } header_names.sort(); preflight.headers.set(AccessControlRequestHeaders(header_names.into_iter().map(UniCase).collect())); // Step 8 unnecessary, we don't use the request body // Step 9, 10 unnecessary, we're writing our own fetch code // Step 11 let preflight_request = Request::new(preflight.method, preflight.destination); let mut req = match preflight_request { Ok(req) => req, Err(_) => return error }; let host = req.headers().get::<Host>().unwrap().clone(); *req.headers_mut() = preflight.headers.clone(); req.headers_mut().set(host); let stream = match req.start() { Ok(s) => s, Err(_) => return error }; let response = match stream.send() { Ok(r) => r, Err(_) => return error }; // Step 12 match response.status.class() { Success => {} _ => return error } cors_response.headers = response.headers.clone(); // Substeps 1-3 (parsing rules: https://fetch.spec.whatwg.org/#http-new-header-syntax) let methods_substep4 = [self.method.clone()]; let mut methods = match response.headers.get() { Some(&AccessControlAllowMethods(ref v)) => &**v, _ => return error }; let headers = match response.headers.get() { Some(&AccessControlAllowHeaders(ref h)) => h, _ => return error }; // Substep 4 if methods.len() == 0 || preflight.mode == RequestMode::ForcedPreflight { methods = &methods_substep4; } // Substep 5 if !is_simple_method(&self.method) && !methods.iter().any(|m| m == &self.method) { return error; } // Substep 6 for h in self.headers.iter() { if is_simple_header(&h) { continue; } if !headers.iter().any(|ref h2| h.name().eq_ignore_ascii_case(h2)) { return error; } } // Substep 7, 8 let max_age = match response.headers.get() { Some(&AccessControlMaxAge(num)) => num, None => 0 }; // Substep 9: Impose restrictions on max-age, if any (unimplemented) // Substeps 10-12: Add a cache (partially implemented, XXXManishearth) // This cache should come from the user agent, creating a new one here to check // for compile time errors let cache = &mut CORSCache(vec!()); for m in methods.iter() { let cache_match = cache.match_method_and_update(self, m, max_age); if !cache_match { cache.insert(CORSCacheEntry::new(self.origin.clone(), self.destination.clone(), max_age, false, HeaderOrMethod::MethodData(m.clone()))); } } for h in response.headers.iter() { let cache_match = cache.match_header_and_update(self, h.name(), max_age); if !cache_match { cache.insert(CORSCacheEntry::new(self.origin.clone(), self.destination.clone(), max_age, false, HeaderOrMethod::HeaderData(h.to_string()))); } } cors_response } } pub struct CORSResponse { pub network_error: bool, pub headers: Headers } impl CORSResponse { fn new() -> CORSResponse { CORSResponse { network_error: false, headers: Headers::new() } } fn new_error() -> CORSResponse { CORSResponse { network_error: true, headers: Headers::new() } } } // CORS Cache stuff /// A CORS cache object. Anchor it somewhere to the user agent. #[derive(Clone)] pub struct CORSCache(Vec<CORSCacheEntry>); /// Union type for CORS cache entries /// Each entry might pertain to a header or method #[derive(Clone)] pub enum HeaderOrMethod { HeaderData(String), MethodData(Method) } impl HeaderOrMethod { fn match_header(&self, header_name: &str) -> bool { match *self { HeaderOrMethod::HeaderData(ref s) => s.eq_ignore_ascii_case(header_name), _ => false } } fn match_method(&self, method: &Method) -> bool { match *self { HeaderOrMethod::MethodData(ref m) => m == method, _ => false } } } // An entry in the CORS cache #[derive(Clone)] pub struct CORSCacheEntry { pub origin: Url, pub url: Url, pub max_age: u32, pub credentials: bool, pub header_or_method: HeaderOrMethod, created: Timespec } impl CORSCacheEntry { fn new (origin:Url, url: Url, max_age: u32, credentials: bool, header_or_method: HeaderOrMethod) -> CORSCacheEntry { CORSCacheEntry { origin: origin, url: url, max_age: max_age, credentials: credentials, header_or_method: header_or_method, created: time::now().to_timespec() } } } impl CORSCache { /// https://fetch.spec.whatwg.org/#concept-cache-clear #[allow(dead_code)] fn clear (&mut self, request: &CORSRequest) { 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 = CORSCache(new_buf); } // Remove old entries 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).collect(); *self = CORSCache(new_buf); } /// https://fetch.spec.whatwg.org/#concept-cache-match-header fn find_entry_by_header<'a>(&'a mut self, request: &CORSRequest, header_name: &str) -> Option<&'a mut CORSCacheEntry> { self.cleanup(); let CORSCache(ref mut buf) = *self; // Credentials are not yet implemented here let entry = buf.iter_mut().find(|e| e.origin.scheme == request.origin.scheme && e.origin.host() == request.origin.host() && e.origin.port() == request.origin.port() && e.url == request.destination && e.header_or_method.match_header(header_name)); entry } fn match_header(&mut self, request: &CORSRequest, header_name: &str) -> bool { self.find_entry_by_header(request, header_name).is_some() } fn match_header_and_update(&mut self, request: &CORSRequest, header_name: &str, new_max_age: u32) -> bool { self.find_entry_by_header(request, header_name).map(|e| e.max_age = new_max_age).is_some() } fn find_entry_by_method<'a>(&'a mut self, request: &CORSRequest, method: &Method) -> Option<&'a mut CORSCacheEntry> { // we can take the method from CORSRequest itself self.cleanup(); let CORSCache(ref mut buf) = *self; // Credentials are not yet implemented here let entry = buf.iter_mut().find(|e| e.origin.scheme == request.origin.scheme && e.origin.host() == request.origin.host() && e.origin.port() == request.origin.port() && e.url == request.destination && e.header_or_method.match_method(method)); entry } /// https://fetch.spec.whatwg.org/#concept-cache-match-method fn match_method(&mut self, request: &CORSRequest, method: &Method) -> bool { self.find_entry_by_method(request, method).is_some() } fn match_method_and_update(&mut self, request: &CORSRequest, method: &Method, new_max_age: u32) -> bool { self.find_entry_by_method(request, method).map(|e| e.max_age = new_max_age).is_some() } fn insert(&mut self, entry: CORSCacheEntry) { self.cleanup(); let CORSCache(ref mut buf) = *self; buf.push(entry); } } fn is_simple_header(h: &HeaderView) -> bool { //FIXME: use h.is::<HeaderType>() when AcceptLanguage and //ContentLanguage headers exist match &*h.name().to_ascii_lowercase() { "accept" | "accept-language" | "content-language" => true, "content-type" => match h.value() { Some(&ContentType(Mime(TopLevel::Text, SubLevel::Plain, _))) | Some(&ContentType(Mime(TopLevel::Application, SubLevel::WwwFormUrlEncoded, _))) | Some(&ContentType(Mime(TopLevel::Multipart, SubLevel::FormData, _))) => true, _ => false }, _ => false } } fn is_simple_method(m: &Method) -> bool { match *m { Method::Get | Method::Head | Method::Post => true, _ => false } } /// Perform a CORS check on a header list and CORS request /// https://fetch.spec.whatwg.org/#cors-check pub fn allow_cross_origin_request(req: &CORSRequest, headers: &Headers) -> bool { //FIXME(seanmonstar): use req.headers.get::<AccessControlAllowOrigin>() match headers.get() { Some(&AccessControlAllowOrigin::Any) => true, // Not always true, depends on credentials mode Some(&AccessControlAllowOrigin::Value(ref url)) => url.scheme == req.origin.scheme && url.host() == req.origin.host() && url.port() == req.origin.port(), None => false } }