use std::collections::HashSet;
use std::iter::FromIterator;
use http::{self, Method, HttpTryFrom, Uri};
use http::header::{self, HeaderName, HeaderValue};
use error::{Result, ResponseError};
use resource::Resource;
use httpmessage::HttpMessage;
use httprequest::HttpRequest;
use httpresponse::HttpResponse;
use httpcodes::{HttpOk, HttpBadRequest};
use middleware::{Middleware, Response, Started};
#[derive(Debug, Fail)]
pub enum CorsError {
#[fail(display="The HTTP request header `Origin` is required but was not provided")]
MissingOrigin,
#[fail(display="The HTTP request header `Origin` could not be parsed correctly.")]
BadOrigin,
#[fail(display="The request header `Access-Control-Request-Method` is required but is missing")]
MissingRequestMethod,
#[fail(display="The request header `Access-Control-Request-Method` has an invalid value")]
BadRequestMethod,
#[fail(display="The request header `Access-Control-Request-Headers` has an invalid value")]
BadRequestHeaders,
#[fail(display="The request header `Access-Control-Request-Headers` is required but is
missing")]
MissingRequestHeaders,
#[fail(display="Origin is not allowed to make this request")]
OriginNotAllowed,
#[fail(display="Requested method is not allowed")]
MethodNotAllowed,
#[fail(display="One or more headers requested are not allowed")]
HeadersNotAllowed,
}
#[derive(Debug, Fail)]
pub enum CorsBuilderError {
#[fail(display="Parse error: {}", _0)]
ParseError(http::Error),
#[fail(display="Credentials are allowed, but the Origin is set to \"*\"")]
CredentialsWithWildcardOrigin,
}
impl ResponseError for CorsError {
fn error_response(&self) -> HttpResponse {
HttpBadRequest.build().body(format!("{}", self)).unwrap()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AllOrSome<T> {
All,
Some(T),
}
impl<T> Default for AllOrSome<T> {
fn default() -> Self {
AllOrSome::All
}
}
impl<T> AllOrSome<T> {
pub fn is_all(&self) -> bool {
match *self {
AllOrSome::All => true,
AllOrSome::Some(_) => false,
}
}
pub fn is_some(&self) -> bool {
!self.is_all()
}
pub fn as_ref(&self) -> Option<&T> {
match *self {
AllOrSome::All => None,
AllOrSome::Some(ref t) => Some(t),
}
}
}
pub struct Cors {
methods: HashSet<Method>,
origins: AllOrSome<HashSet<String>>,
origins_str: Option<HeaderValue>,
headers: AllOrSome<HashSet<HeaderName>>,
expose_hdrs: Option<String>,
max_age: Option<usize>,
preflight: bool,
send_wildcard: bool,
supports_credentials: bool,
vary_header: bool,
}
impl Default for Cors {
fn default() -> Cors {
Cors {
origins: AllOrSome::default(),
origins_str: None,
methods: HashSet::from_iter(
vec![Method::GET, Method::HEAD,
Method::POST, Method::OPTIONS, Method::PUT,
Method::PATCH, Method::DELETE].into_iter()),
headers: AllOrSome::All,
expose_hdrs: None,
max_age: None,
preflight: true,
send_wildcard: false,
supports_credentials: false,
vary_header: true,
}
}
}
impl Cors {
pub fn build() -> CorsBuilder {
CorsBuilder {
cors: Some(Cors {
origins: AllOrSome::All,
origins_str: None,
methods: HashSet::new(),
headers: AllOrSome::All,
expose_hdrs: None,
max_age: None,
preflight: true,
send_wildcard: false,
supports_credentials: false,
vary_header: true,
}),
methods: false,
error: None,
expose_hdrs: HashSet::new(),
}
}
pub fn register<S: 'static>(self, resource: &mut Resource<S>) {
resource.method(Method::OPTIONS).h(HttpOk);
resource.middleware(self);
}
fn validate_origin<S>(&self, req: &mut HttpRequest<S>) -> Result<(), CorsError> {
if let Some(hdr) = req.headers().get(header::ORIGIN) {
if let Ok(origin) = hdr.to_str() {
return match self.origins {
AllOrSome::All => Ok(()),
AllOrSome::Some(ref allowed_origins) => {
allowed_origins
.get(origin)
.and_then(|_| Some(()))
.ok_or_else(|| CorsError::OriginNotAllowed)
}
};
}
Err(CorsError::BadOrigin)
} else {
return match self.origins {
AllOrSome::All => Ok(()),
_ => Err(CorsError::MissingOrigin)
}
}
}
fn validate_allowed_method<S>(&self, req: &mut HttpRequest<S>) -> Result<(), CorsError> {
if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_METHOD) {
if let Ok(meth) = hdr.to_str() {
if let Ok(method) = Method::try_from(meth) {
return self.methods.get(&method)
.and_then(|_| Some(()))
.ok_or_else(|| CorsError::MethodNotAllowed);
}
}
Err(CorsError::BadRequestMethod)
} else {
Err(CorsError::MissingRequestMethod)
}
}
fn validate_allowed_headers<S>(&self, req: &mut HttpRequest<S>) -> Result<(), CorsError> {
match self.headers {
AllOrSome::All => Ok(()),
AllOrSome::Some(ref allowed_headers) => {
if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) {
if let Ok(headers) = hdr.to_str() {
let mut hdrs = HashSet::new();
for hdr in headers.split(',') {
match HeaderName::try_from(hdr.trim()) {
Ok(hdr) => hdrs.insert(hdr),
Err(_) => return Err(CorsError::BadRequestHeaders)
};
}
if !hdrs.is_empty() && !hdrs.is_subset(allowed_headers) {
return Err(CorsError::HeadersNotAllowed)
}
return Ok(())
}
Err(CorsError::BadRequestHeaders)
} else {
Err(CorsError::MissingRequestHeaders)
}
}
}
}
}
impl<S> Middleware<S> for Cors {
fn start(&self, req: &mut HttpRequest<S>) -> Result<Started> {
if self.preflight && Method::OPTIONS == *req.method() {
self.validate_origin(req)?;
self.validate_allowed_method(req)?;
self.validate_allowed_headers(req)?;
let headers = if let Some(headers) = self.headers.as_ref() {
Some(HeaderValue::try_from(&headers.iter().fold(
String::new(), |s, v| s + "," + v.as_str()).as_str()[1..]).unwrap())
} else if let Some(hdr) = req.headers().get(header::ACCESS_CONTROL_REQUEST_HEADERS) {
Some(hdr.clone())
} else {
None
};
Ok(Started::Response(
HttpOk.build()
.if_some(self.max_age.as_ref(), |max_age, resp| {
let _ = resp.header(
header::ACCESS_CONTROL_MAX_AGE, format!("{}", max_age).as_str());})
.if_some(headers, |headers, resp| {
let _ = resp.header(header::ACCESS_CONTROL_ALLOW_HEADERS, headers); })
.if_true(self.origins.is_all(), |resp| {
if self.send_wildcard {
resp.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*");
} else {
let origin = req.headers().get(header::ORIGIN).unwrap();
resp.header(
header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone());
}
})
.if_true(self.origins.is_some(), |resp| {
resp.header(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
self.origins_str.as_ref().unwrap().clone());
})
.if_true(self.supports_credentials, |resp| {
resp.header(header::ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
})
.header(
header::ACCESS_CONTROL_ALLOW_METHODS,
&self.methods.iter().fold(
String::new(), |s, v| s + "," + v.as_str()).as_str()[1..])
.finish()
.unwrap()))
} else {
self.validate_origin(req)?;
Ok(Started::Done)
}
}
fn response(&self, req: &mut HttpRequest<S>, mut resp: HttpResponse) -> Result<Response> {
match self.origins {
AllOrSome::All => {
if self.send_wildcard {
resp.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
} else {
let origin = req.headers().get(header::ORIGIN).unwrap();
resp.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN, origin.clone());
}
}
AllOrSome::Some(_) => {
resp.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
self.origins_str.as_ref().unwrap().clone());
}
}
if let Some(ref expose) = self.expose_hdrs {
resp.headers_mut().insert(
header::ACCESS_CONTROL_EXPOSE_HEADERS,
HeaderValue::try_from(expose.as_str()).unwrap());
}
if self.supports_credentials {
resp.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true"));
}
if self.vary_header {
let value = if let Some(hdr) = resp.headers_mut().get(header::VARY) {
let mut val: Vec<u8> = Vec::with_capacity(hdr.as_bytes().len() + 8);
val.extend(hdr.as_bytes());
val.extend(b", Origin");
HeaderValue::try_from(&val[..]).unwrap()
} else {
HeaderValue::from_static("Origin")
};
resp.headers_mut().insert(header::VARY, value);
}
Ok(Response::Done(resp))
}
}
pub struct CorsBuilder {
cors: Option<Cors>,
methods: bool,
error: Option<http::Error>,
expose_hdrs: HashSet<HeaderName>,
}
fn cors<'a>(parts: &'a mut Option<Cors>, err: &Option<http::Error>) -> Option<&'a mut Cors> {
if err.is_some() {
return None
}
parts.as_mut()
}
impl CorsBuilder {
pub fn allowed_origin(&mut self, origin: &str) -> &mut CorsBuilder {
if let Some(cors) = cors(&mut self.cors, &self.error) {
match Uri::try_from(origin) {
Ok(_) => {
if cors.origins.is_all() {
cors.origins = AllOrSome::Some(HashSet::new());
}
if let AllOrSome::Some(ref mut origins) = cors.origins {
origins.insert(origin.to_owned());
}
}
Err(e) => {
self.error = Some(e.into());
}
}
}
self
}
pub fn allowed_methods<U, M>(&mut self, methods: U) -> &mut CorsBuilder
where U: IntoIterator<Item=M>, Method: HttpTryFrom<M>
{
self.methods = true;
if let Some(cors) = cors(&mut self.cors, &self.error) {
for m in methods {
match Method::try_from(m) {
Ok(method) => {
cors.methods.insert(method);
},
Err(e) => {
self.error = Some(e.into());
break
}
}
};
}
self
}
pub fn allowed_header<H>(&mut self, header: H) -> &mut CorsBuilder
where HeaderName: HttpTryFrom<H>
{
if let Some(cors) = cors(&mut self.cors, &self.error) {
match HeaderName::try_from(header) {
Ok(method) => {
if cors.headers.is_all() {
cors.headers = AllOrSome::Some(HashSet::new());
}
if let AllOrSome::Some(ref mut headers) = cors.headers {
headers.insert(method);
}
}
Err(e) => self.error = Some(e.into()),
}
}
self
}
pub fn allowed_headers<U, H>(&mut self, headers: U) -> &mut CorsBuilder
where U: IntoIterator<Item=H>, HeaderName: HttpTryFrom<H>
{
if let Some(cors) = cors(&mut self.cors, &self.error) {
for h in headers {
match HeaderName::try_from(h) {
Ok(method) => {
if cors.headers.is_all() {
cors.headers = AllOrSome::Some(HashSet::new());
}
if let AllOrSome::Some(ref mut headers) = cors.headers {
headers.insert(method);
}
}
Err(e) => {
self.error = Some(e.into());
break
}
}
};
}
self
}
pub fn expose_headers<U, H>(&mut self, headers: U) -> &mut CorsBuilder
where U: IntoIterator<Item=H>, HeaderName: HttpTryFrom<H>
{
for h in headers {
match HeaderName::try_from(h) {
Ok(method) => {
self.expose_hdrs.insert(method);
},
Err(e) => {
self.error = Some(e.into());
break
}
}
}
self
}
pub fn max_age(&mut self, max_age: usize) -> &mut CorsBuilder {
if let Some(cors) = cors(&mut self.cors, &self.error) {
cors.max_age = Some(max_age)
}
self
}
pub fn send_wildcard(&mut self) -> &mut CorsBuilder {
if let Some(cors) = cors(&mut self.cors, &self.error) {
cors.send_wildcard = true
}
self
}
pub fn supports_credentials(&mut self) -> &mut CorsBuilder {
if let Some(cors) = cors(&mut self.cors, &self.error) {
cors.supports_credentials = true
}
self
}
pub fn disable_vary_header(&mut self) -> &mut CorsBuilder {
if let Some(cors) = cors(&mut self.cors, &self.error) {
cors.vary_header = false
}
self
}
pub fn disable_preflight(&mut self) -> &mut CorsBuilder {
if let Some(cors) = cors(&mut self.cors, &self.error) {
cors.preflight = false
}
self
}
pub fn finish(&mut self) -> Result<Cors, CorsBuilderError> {
if !self.methods {
self.allowed_methods(vec![Method::GET, Method::HEAD,
Method::POST, Method::OPTIONS, Method::PUT,
Method::PATCH, Method::DELETE]);
}
if let Some(e) = self.error.take() {
return Err(CorsBuilderError::ParseError(e))
}
let mut cors = self.cors.take().expect("cannot reuse CorsBuilder");
if cors.supports_credentials && cors.send_wildcard && cors.origins.is_all() {
return Err(CorsBuilderError::CredentialsWithWildcardOrigin)
}
if let AllOrSome::Some(ref origins) = cors.origins {
let s = origins.iter().fold(String::new(), |s, v| s + &format!("{}", v));
cors.origins_str = Some(HeaderValue::try_from(s.as_str()).unwrap());
}
if !self.expose_hdrs.is_empty() {
cors.expose_hdrs = Some(
self.expose_hdrs.iter().fold(
String::new(), |s, v| s + v.as_str())[1..].to_owned());
}
Ok(cors)
}
}
#[cfg(test)]
mod tests {
use super::*;
use test::TestRequest;
impl Started {
fn is_done(&self) -> bool {
match *self {
Started::Done => true,
_ => false,
}
}
fn response(self) -> HttpResponse {
match self {
Started::Response(resp) => resp,
_ => panic!(),
}
}
}
impl Response {
fn response(self) -> HttpResponse {
match self {
Response::Done(resp) => resp,
_ => panic!(),
}
}
}
#[test]
#[should_panic(expected = "CredentialsWithWildcardOrigin")]
fn cors_validates_illegal_allow_credentials() {
Cors::build()
.supports_credentials()
.send_wildcard()
.finish()
.unwrap();
}
#[test]
fn validate_origin_allows_all_origins() {
let cors = Cors::default();
let mut req = TestRequest::with_header(
"Origin", "https://www.example.com").finish();
assert!(cors.start(&mut req).ok().unwrap().is_done())
}
#[test]
fn test_preflight() {
let mut cors = Cors::build()
.send_wildcard()
.max_age(3600)
.allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST])
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
.allowed_header(header::CONTENT_TYPE)
.finish().unwrap();
let mut req = TestRequest::with_header(
"Origin", "https://www.example.com")
.method(Method::OPTIONS)
.finish();
assert!(cors.start(&mut req).is_err());
let mut req = TestRequest::with_header("Origin", "https://www.example.com")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "put")
.method(Method::OPTIONS)
.finish();
assert!(cors.start(&mut req).is_err());
let mut req = TestRequest::with_header("Origin", "https://www.example.com")
.header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
.header(header::ACCESS_CONTROL_REQUEST_HEADERS, "AUTHORIZATION,ACCEPT")
.method(Method::OPTIONS)
.finish();
let resp = cors.start(&mut req).unwrap().response();
assert_eq!(
&b"*"[..],
resp.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap().as_bytes());
assert_eq!(
&b"3600"[..],
resp.headers().get(header::ACCESS_CONTROL_MAX_AGE).unwrap().as_bytes());
cors.preflight = false;
assert!(cors.start(&mut req).unwrap().is_done());
}
#[test]
#[should_panic(expected = "MissingOrigin")]
fn test_validate_missing_origin() {
let cors = Cors::build()
.allowed_origin("https://www.example.com").finish().unwrap();
let mut req = HttpRequest::default();
cors.start(&mut req).unwrap();
}
#[test]
#[should_panic(expected = "OriginNotAllowed")]
fn test_validate_not_allowed_origin() {
let cors = Cors::build()
.allowed_origin("https://www.example.com").finish().unwrap();
let mut req = TestRequest::with_header("Origin", "https://www.unknown.com")
.method(Method::GET)
.finish();
cors.start(&mut req).unwrap();
}
#[test]
fn test_validate_origin() {
let cors = Cors::build()
.allowed_origin("https://www.example.com").finish().unwrap();
let mut req = TestRequest::with_header("Origin", "https://www.example.com")
.method(Method::GET)
.finish();
assert!(cors.start(&mut req).unwrap().is_done());
}
#[test]
fn test_response() {
let cors = Cors::build()
.send_wildcard()
.disable_preflight()
.max_age(3600)
.allowed_methods(vec![Method::GET, Method::OPTIONS, Method::POST])
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
.allowed_header(header::CONTENT_TYPE)
.finish().unwrap();
let mut req = TestRequest::with_header(
"Origin", "https://www.example.com")
.method(Method::OPTIONS)
.finish();
let resp: HttpResponse = HttpOk.into();
let resp = cors.response(&mut req, resp).unwrap().response();
assert_eq!(
&b"*"[..],
resp.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap().as_bytes());
assert_eq!(
&b"Origin"[..],
resp.headers().get(header::VARY).unwrap().as_bytes());
let resp: HttpResponse = HttpOk.build()
.header(header::VARY, "Accept")
.finish().unwrap();
let resp = cors.response(&mut req, resp).unwrap().response();
assert_eq!(
&b"Accept, Origin"[..],
resp.headers().get(header::VARY).unwrap().as_bytes());
let cors = Cors::build()
.disable_vary_header()
.allowed_origin("https://www.example.com")
.finish().unwrap();
let resp: HttpResponse = HttpOk.into();
let resp = cors.response(&mut req, resp).unwrap().response();
assert_eq!(
&b"https://www.example.com"[..],
resp.headers().get(header::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap().as_bytes());
}
}