Files
tonic/tonic-web/src/cors.rs
T

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"
);
}
}
}