403 lines
12 KiB
Rust
403 lines
12 KiB
Rust
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<Cache>,
|
|
}
|
|
|
|
#[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<HeaderMap, Error> {
|
|
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<HeaderMap, Error> {
|
|
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<HeaderMap, Error> {
|
|
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<I>(values: I) -> Result<HeaderValue, header::InvalidHeaderValue>
|
|
where
|
|
I: IntoIterator,
|
|
I::Item: AsRef<str>,
|
|
{
|
|
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<_>>();
|
|
vec.sort_unstable();
|
|
vec
|
|
}
|
|
|
|
assert_eq!(sorted($header.to_str().unwrap()), sorted($expected))
|
|
};
|
|
}
|
|
|
|
fn value(s: &str) -> HeaderValue {
|
|
s.parse().unwrap()
|
|
}
|
|
|
|
impl From<Config> 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"
|
|
);
|
|
}
|
|
}
|
|
}
|