use std::sync::Arc; pub(crate) use http::header::ACCESS_CONTROL_ALLOW_CREDENTIALS as ALLOW_CREDENTIALS; pub(crate) use http::header::ACCESS_CONTROL_ALLOW_HEADERS as ALLOW_HEADERS; pub(crate) use http::header::ACCESS_CONTROL_ALLOW_METHODS as ALLOW_METHODS; pub(crate) use http::header::ACCESS_CONTROL_ALLOW_ORIGIN as ALLOW_ORIGIN; pub(crate) use http::header::ACCESS_CONTROL_EXPOSE_HEADERS as EXPOSE_HEADERS; pub(crate) use http::header::ACCESS_CONTROL_MAX_AGE as MAX_AGE; pub(crate) use http::header::ACCESS_CONTROL_REQUEST_HEADERS as REQUEST_HEADERS; pub(crate) use http::header::ACCESS_CONTROL_REQUEST_METHOD as REQUEST_METHOD; pub(crate) use http::header::ORIGIN; use http::{header, HeaderMap, HeaderValue, Method}; use tracing::debug; use crate::config::Config; const DEFAULT_ALLOWED_METHODS: &[Method; 2] = &[Method::POST, Method::OPTIONS]; #[derive(Debug, Clone)] pub(crate) struct Cors { cache: Arc, } #[derive(Debug, PartialEq)] pub(crate) enum Error { OriginNotAllowed, MethodNotAllowed, } #[derive(Clone, Debug)] struct Cache { config: Config, expose_headers: HeaderValue, allow_methods: HeaderValue, allow_credentials: HeaderValue, } impl Cors { pub(crate) fn new(config: Config) -> Cors { let expose_headers = join_header_value(&config.exposed_headers).unwrap(); let allow_methods = HeaderValue::from_static("POST,OPTIONS"); let allow_credentials = HeaderValue::from_static("true"); let cache = Arc::new(Cache { config, expose_headers, allow_methods, allow_credentials, }); Cors { cache } } fn is_method_allowed(&self, header: Option<&HeaderValue>) -> bool { match header { Some(value) => match Method::from_bytes(value.as_bytes()) { Ok(method) => DEFAULT_ALLOWED_METHODS.contains(&method), Err(_) => { debug!("access-control-request-method {:?} is not valid", value); false } }, None => { debug!("access-control-request-method is missing"); false } } } pub(crate) fn preflight( &self, req_headers: &HeaderMap, origin: &HeaderValue, request_headers_header: &HeaderValue, ) -> Result { if !self.is_origin_allowed(origin) { return Err(Error::OriginNotAllowed); } if !self.is_method_allowed(req_headers.get(REQUEST_METHOD)) { return Err(Error::MethodNotAllowed); } let mut headers = self.common_headers(origin.clone()); headers.insert(ALLOW_METHODS, self.cache.allow_methods.clone()); headers.insert(ALLOW_HEADERS, request_headers_header.clone()); if let Some(max_age) = self.cache.config.max_age { headers.insert(MAX_AGE, HeaderValue::from(max_age.as_secs())); } Ok(headers) } pub(crate) fn simple(&self, headers: &HeaderMap) -> Result { match headers.get(header::ORIGIN) { Some(origin) if self.is_origin_allowed(origin) => { Ok(self.common_headers(origin.clone())) } Some(_) => Err(Error::OriginNotAllowed), None => Ok(HeaderMap::new()), } } fn common_headers(&self, origin: HeaderValue) -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert(ALLOW_ORIGIN, origin); headers.insert(EXPOSE_HEADERS, self.cache.expose_headers.clone()); if self.cache.config.allow_credentials { headers.insert(ALLOW_CREDENTIALS, self.cache.allow_credentials.clone()); } headers } fn is_origin_allowed(&self, origin: &HeaderValue) -> bool { self.cache.config.allowed_origins.is_allowed(origin) } #[cfg(test)] pub(crate) fn __check_preflight(&self, headers: &HeaderMap) -> Result { self.preflight( headers, headers.get(ORIGIN).unwrap(), headers.get(REQUEST_HEADERS).unwrap(), ) } } #[cfg(test)] impl Default for Cors { fn default() -> Self { Cors::new(Config::default()) } } fn join_header_value(values: I) -> Result where I: IntoIterator, I::Item: AsRef, { let mut values = values.into_iter(); let mut value = Vec::new(); if let Some(v) = values.next() { value.extend(v.as_ref().as_bytes()); } for v in values { value.push(b','); value.extend(v.as_ref().as_bytes()); } HeaderValue::from_bytes(&value) } #[cfg(test)] mod tests { use super::*; macro_rules! assert_value_eq { ($header:expr, $expected:expr) => { fn sorted(value: &str) -> Vec<&str> { let mut vec = value.split(",").collect::>(); vec.sort_unstable(); vec } assert_eq!(sorted($header.to_str().unwrap()), sorted($expected)) }; } fn value(s: &str) -> HeaderValue { s.parse().unwrap() } impl From for Cors { fn from(c: Config) -> Self { Cors::new(c) } } #[test] #[should_panic] #[ignore] fn origin_is_valid_url() { Config::new().allow_origins(vec!["foo"]); } mod preflight { use super::*; fn preflight_headers() -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert(ORIGIN, value("http://example.com")); headers.insert(REQUEST_METHOD, value("POST")); headers.insert(REQUEST_HEADERS, value("x-grpc-web")); headers } #[test] fn default_config() { let cors = Cors::default(); let headers = cors.__check_preflight(&preflight_headers()).unwrap(); assert_eq!(headers[ALLOW_ORIGIN], "http://example.com"); assert_eq!(headers[ALLOW_METHODS], "POST,OPTIONS"); assert_eq!(headers[ALLOW_HEADERS], "x-grpc-web"); assert_eq!(headers[ALLOW_CREDENTIALS], "true"); assert_eq!(headers[MAX_AGE], "86400"); assert_value_eq!(&headers[EXPOSE_HEADERS], "grpc-status,grpc-message"); } #[test] fn any_origin() { let cors: Cors = Config::new().allow_all_origins().into(); assert!(cors.__check_preflight(&preflight_headers()).is_ok()); } #[test] fn origin_list() { let cors: Cors = Config::new() .allow_origins(vec![ HeaderValue::from_static("http://a.com"), HeaderValue::from_static("http://b.com"), ]) .into(); let mut req_headers = preflight_headers(); req_headers.insert(ORIGIN, value("http://b.com")); assert!(cors.__check_preflight(&req_headers).is_ok()); } #[test] fn origin_not_allowed() { let cors: Cors = Config::new().allow_origins(vec!["http://a.com"]).into(); let err = cors.__check_preflight(&preflight_headers()).unwrap_err(); assert_eq!(err, Error::OriginNotAllowed) } #[test] fn disallow_credentials() { let cors = Cors::new(Config::new().allow_credentials(false)); let headers = cors.__check_preflight(&preflight_headers()).unwrap(); assert!(!headers.contains_key(ALLOW_CREDENTIALS)); } #[test] fn expose_headers_are_merged() { let cors = Cors::new(Config::new().expose_headers(vec!["x-request-id"])); let headers = cors.__check_preflight(&preflight_headers()).unwrap(); assert_value_eq!( &headers[EXPOSE_HEADERS], "x-request-id,grpc-message,grpc-status" ); } #[test] fn allow_headers_echo_request_headers() { let cors = Cors::default(); let mut request_headers = preflight_headers(); request_headers.insert(REQUEST_HEADERS, value("x-grpc-web,foo,x-request-id")); let headers = cors.__check_preflight(&request_headers).unwrap(); assert_value_eq!(&headers[ALLOW_HEADERS], "x-grpc-web,foo,x-request-id"); } #[test] fn missing_request_method() { let cors = Cors::default(); let mut request_headers = preflight_headers(); request_headers.remove(REQUEST_METHOD); let err = cors.__check_preflight(&request_headers).unwrap_err(); assert_eq!(err, Error::MethodNotAllowed); } #[test] fn only_options_and_post_allowed() { let cors = Cors::default(); for method in &[ Method::GET, Method::DELETE, Method::TRACE, Method::PATCH, Method::PUT, Method::HEAD, ] { let mut request_headers = preflight_headers(); request_headers.insert(REQUEST_METHOD, value(method.as_str())); assert_eq!( cors.__check_preflight(&request_headers).unwrap_err(), Error::MethodNotAllowed, ) } } #[test] fn custom_max_age() { use std::time::Duration; let cors = Cors::new(Config::new().max_age(Duration::from_secs(99))); let headers = cors.__check_preflight(&preflight_headers()).unwrap(); assert_eq!(headers[MAX_AGE], "99"); } #[test] fn no_max_age() { let cors = Cors::new(Config::new().max_age(None)); let headers = cors.__check_preflight(&preflight_headers()).unwrap(); assert!(!headers.contains_key(MAX_AGE)); } } mod simple { use super::*; fn request_headers() -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert(ORIGIN, value("http://example.com")); headers } #[test] fn default_config() { let cors = Cors::default(); let headers = cors.simple(&request_headers()).unwrap(); assert_eq!(headers[ALLOW_ORIGIN], "http://example.com"); assert_eq!(headers[ALLOW_CREDENTIALS], "true"); assert_value_eq!(&headers[EXPOSE_HEADERS], "grpc-message,grpc-status"); assert!(!headers.contains_key(ALLOW_HEADERS)); assert!(!headers.contains_key(ALLOW_METHODS)); assert!(!headers.contains_key(MAX_AGE)); } #[test] fn any_origin() { let cors: Cors = Config::new().allow_all_origins().into(); assert!(cors.simple(&request_headers()).is_ok()); } #[test] fn origin_list() { let cors: Cors = Config::new() .allow_origins(vec![ HeaderValue::from_static("http://a.com"), HeaderValue::from_static("http://b.com"), ]) .into(); let mut req_headers = request_headers(); req_headers.insert(ORIGIN, value("http://b.com")); assert!(cors.simple(&req_headers).is_ok()); } #[test] fn origin_not_allowed() { let cors: Cors = Config::new().allow_origins(vec!["http://a.com"]).into(); let err = cors.simple(&request_headers()).unwrap_err(); assert_eq!(err, Error::OriginNotAllowed) } #[test] fn disallow_credentials() { let cors = Cors::new(Config::new().allow_credentials(false)); let headers = cors.simple(&request_headers()).unwrap(); assert!(!headers.contains_key(ALLOW_CREDENTIALS)); } #[test] fn expose_headers_are_merged() { let cors: Cors = Config::new() .expose_headers(vec!["x-hello", "custom-1"]) .into(); let headers = cors.simple(&request_headers()).unwrap(); assert_value_eq!( &headers[EXPOSE_HEADERS], "grpc-message,grpc-status,x-hello,custom-1" ); } } }