2020-05-13 19:10:53 +02:00
|
|
|
use gotham::{
|
2020-05-16 14:26:21 +02:00
|
|
|
handler::HandlerFuture,
|
2020-05-14 23:30:59 +02:00
|
|
|
helpers::http::response::create_empty_response,
|
2020-05-16 14:26:21 +02:00
|
|
|
hyper::{
|
2020-05-14 23:30:59 +02:00
|
|
|
header::{
|
2020-09-15 15:10:41 +02:00
|
|
|
HeaderMap, HeaderName, HeaderValue, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
|
|
|
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE,
|
2021-01-01 16:44:55 +01:00
|
|
|
ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, ORIGIN, VARY
|
2020-05-14 23:30:59 +02:00
|
|
|
},
|
|
|
|
Body, Method, Response, StatusCode
|
2020-05-13 19:10:53 +02:00
|
|
|
},
|
2020-05-16 14:26:21 +02:00
|
|
|
middleware::Middleware,
|
2020-05-14 23:30:59 +02:00
|
|
|
pipeline::chain::PipelineHandleChain,
|
2020-09-15 15:20:04 +02:00
|
|
|
router::{builder::*, route::matcher::AccessControlRequestMethodMatcher},
|
2020-09-15 15:10:41 +02:00
|
|
|
state::{FromState, State}
|
2020-05-13 19:10:53 +02:00
|
|
|
};
|
2020-09-15 15:10:41 +02:00
|
|
|
use std::{panic::RefUnwindSafe, pin::Pin};
|
2020-05-13 19:10:53 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
Specify the allowed origins of the request. It is up to the browser to check the validity of the
|
|
|
|
origin. This, when sent to the browser, will indicate whether or not the request's origin was
|
|
|
|
allowed to make the request.
|
|
|
|
*/
|
|
|
|
#[derive(Clone, Debug)]
|
2020-09-15 15:10:41 +02:00
|
|
|
pub enum Origin {
|
2020-05-13 19:10:53 +02:00
|
|
|
/// Do not send any `Access-Control-Allow-Origin` headers.
|
2020-05-16 14:26:21 +02:00
|
|
|
None,
|
2020-05-13 19:10:53 +02:00
|
|
|
/// Send `Access-Control-Allow-Origin: *`. Note that browser will not send credentials.
|
2020-05-16 14:26:21 +02:00
|
|
|
Star,
|
2020-05-13 19:10:53 +02:00
|
|
|
/// Set the `Access-Control-Allow-Origin` header to a single origin.
|
2020-05-16 14:26:21 +02:00
|
|
|
Single(String),
|
2020-05-13 19:10:53 +02:00
|
|
|
/// Copy the `Origin` header into the `Access-Control-Allow-Origin` header.
|
|
|
|
Copy
|
|
|
|
}
|
|
|
|
|
2020-09-15 15:10:41 +02:00
|
|
|
impl Default for Origin {
|
|
|
|
fn default() -> Self {
|
2020-05-13 19:10:53 +02:00
|
|
|
Self::None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-15 15:10:41 +02:00
|
|
|
impl Origin {
|
2020-05-13 19:10:53 +02:00
|
|
|
/// Get the header value for the `Access-Control-Allow-Origin` header.
|
2020-09-15 15:10:41 +02:00
|
|
|
fn header_value(&self, state: &State) -> Option<HeaderValue> {
|
2020-05-13 19:10:53 +02:00
|
|
|
match self {
|
|
|
|
Self::None => None,
|
|
|
|
Self::Star => Some("*".parse().unwrap()),
|
|
|
|
Self::Single(origin) => Some(origin.parse().unwrap()),
|
|
|
|
Self::Copy => {
|
|
|
|
let headers = HeaderMap::borrow_from(state);
|
|
|
|
headers.get(ORIGIN).map(Clone::clone)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-01-01 16:44:55 +01:00
|
|
|
|
|
|
|
/// Returns true if the `Vary` header has to include `Origin`.
|
|
|
|
fn varies(&self) -> bool {
|
|
|
|
matches!(self, Self::Copy)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Specify the allowed headers of the request. It is up to the browser to check that only the allowed
|
|
|
|
headers are sent with the request.
|
|
|
|
*/
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub enum Headers {
|
|
|
|
/// Do not send any `Access-Control-Allow-Headers` headers.
|
|
|
|
None,
|
|
|
|
/// Set the `Access-Control-Allow-Headers` header to the following header list. If empty, this
|
|
|
|
/// is treated as if it was [None].
|
|
|
|
List(Vec<HeaderName>),
|
|
|
|
/// Copy the `Access-Control-Request-Headers` header into the `Access-Control-Allow-Header`
|
|
|
|
/// header.
|
|
|
|
Copy
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Headers {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Headers {
|
|
|
|
/// Get the header value for the `Access-Control-Allow-Headers` header.
|
|
|
|
fn header_value(&self, state: &State) -> Option<HeaderValue> {
|
|
|
|
match self {
|
|
|
|
Self::None => None,
|
|
|
|
Self::List(list) => Some(list.join(",").parse().unwrap()),
|
|
|
|
Self::Copy => {
|
|
|
|
let headers = HeaderMap::borrow_from(state);
|
|
|
|
headers.get(ACCESS_CONTROL_REQUEST_HEADERS).map(Clone::clone)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns true if the `Vary` header has to include `Origin`.
|
|
|
|
fn varies(&self) -> bool {
|
|
|
|
matches!(self, Self::Copy)
|
|
|
|
}
|
2020-05-13 19:10:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
This is the configuration that the CORS handler will follow. Its default configuration is basically
|
|
|
|
not to touch any responses, resulting in the browser's default behaviour.
|
|
|
|
|
2020-11-22 23:55:52 +01:00
|
|
|
To change settings, you need to put this type into gotham's [State]:
|
2020-05-13 19:10:53 +02:00
|
|
|
|
|
|
|
```rust,no_run
|
|
|
|
# use gotham::{router::builder::*, pipeline::{new_pipeline, single::single_pipeline}, state::State};
|
2021-01-01 16:44:55 +01:00
|
|
|
# use gotham_restful::{*, cors::Origin};
|
2020-05-13 19:10:53 +02:00
|
|
|
fn main() {
|
|
|
|
let cors = CorsConfig {
|
2020-05-16 14:26:21 +02:00
|
|
|
origin: Origin::Star,
|
2020-05-14 23:30:59 +02:00
|
|
|
..Default::default()
|
2020-05-13 19:10:53 +02:00
|
|
|
};
|
|
|
|
let (chain, pipelines) = single_pipeline(new_pipeline().add(cors).build());
|
|
|
|
gotham::start("127.0.0.1:8080", build_router(chain, pipelines, |route| {
|
|
|
|
// your routing logic
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
This easy approach allows you to have one global cors configuration. If you prefer to have separate
|
|
|
|
configurations for different scopes, you need to register the middleware inside your routing logic:
|
|
|
|
|
|
|
|
```rust,no_run
|
|
|
|
# use gotham::{router::builder::*, pipeline::*, pipeline::set::*, state::State};
|
2021-01-01 16:44:55 +01:00
|
|
|
# use gotham_restful::{*, cors::Origin};
|
2020-05-16 14:22:23 +02:00
|
|
|
let pipelines = new_pipeline_set();
|
|
|
|
|
|
|
|
// The first cors configuration
|
|
|
|
let cors_a = CorsConfig {
|
|
|
|
origin: Origin::Star,
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
let (pipelines, chain_a) = pipelines.add(
|
|
|
|
new_pipeline().add(cors_a).build()
|
|
|
|
);
|
|
|
|
|
|
|
|
// The second cors configuration
|
|
|
|
let cors_b = CorsConfig {
|
|
|
|
origin: Origin::Copy,
|
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
let (pipelines, chain_b) = pipelines.add(
|
|
|
|
new_pipeline().add(cors_b).build()
|
|
|
|
);
|
|
|
|
|
|
|
|
let pipeline_set = finalize_pipeline_set(pipelines);
|
|
|
|
gotham::start("127.0.0.1:8080", build_router((), pipeline_set, |route| {
|
|
|
|
// routing without any cors config
|
|
|
|
route.with_pipeline_chain((chain_a, ()), |route| {
|
|
|
|
// routing with cors config a
|
|
|
|
});
|
|
|
|
route.with_pipeline_chain((chain_b, ()), |route| {
|
|
|
|
// routing with cors config b
|
|
|
|
});
|
|
|
|
}));
|
2020-05-13 19:10:53 +02:00
|
|
|
```
|
|
|
|
*/
|
|
|
|
#[derive(Clone, Debug, Default, NewMiddleware, StateData)]
|
2020-09-15 15:10:41 +02:00
|
|
|
pub struct CorsConfig {
|
2020-05-14 23:30:59 +02:00
|
|
|
/// The allowed origins.
|
2020-09-15 15:10:41 +02:00
|
|
|
pub origin: Origin,
|
2020-05-14 23:30:59 +02:00
|
|
|
/// The allowed headers.
|
2021-01-01 16:44:55 +01:00
|
|
|
pub headers: Headers,
|
2020-05-14 23:30:59 +02:00
|
|
|
/// The amount of seconds that the preflight request can be cached.
|
2020-09-15 15:10:41 +02:00
|
|
|
pub max_age: u64,
|
2020-05-14 23:30:59 +02:00
|
|
|
/// Whether or not the request may be made with supplying credentials.
|
2020-09-15 15:10:41 +02:00
|
|
|
pub credentials: bool
|
2020-05-13 19:10:53 +02:00
|
|
|
}
|
|
|
|
|
2020-09-15 15:10:41 +02:00
|
|
|
impl Middleware for CorsConfig {
|
|
|
|
fn call<Chain>(self, mut state: State, chain: Chain) -> Pin<Box<HandlerFuture>>
|
2020-05-16 14:26:21 +02:00
|
|
|
where
|
2020-09-15 15:10:41 +02:00
|
|
|
Chain: FnOnce(State) -> Pin<Box<HandlerFuture>>
|
2020-05-16 14:26:21 +02:00
|
|
|
{
|
|
|
|
state.put(self);
|
|
|
|
chain(state)
|
|
|
|
}
|
2020-05-13 19:10:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Handle CORS for a non-preflight request. This means manipulating the `res` HTTP headers so that
|
2020-11-21 16:39:35 +01:00
|
|
|
the response is aligned with the `state`'s [CorsConfig].
|
2020-05-13 19:10:53 +02:00
|
|
|
|
2020-11-22 23:55:52 +01:00
|
|
|
If you are using the [Resource](crate::Resource) type (which is the recommended way), you'll never
|
|
|
|
have to call this method. However, if you are writing your own handler method, you might want to
|
|
|
|
call this after your request to add the required CORS headers.
|
2020-05-13 19:10:53 +02:00
|
|
|
|
2020-11-22 23:55:52 +01:00
|
|
|
For further information on CORS, read
|
|
|
|
[https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
|
2020-05-13 19:10:53 +02:00
|
|
|
*/
|
2020-09-15 15:10:41 +02:00
|
|
|
pub fn handle_cors(state: &State, res: &mut Response<Body>) {
|
2020-05-13 19:10:53 +02:00
|
|
|
let config = CorsConfig::try_borrow_from(state);
|
2021-01-01 16:44:55 +01:00
|
|
|
if let Some(cfg) = config {
|
|
|
|
let headers = res.headers_mut();
|
2020-09-15 15:10:41 +02:00
|
|
|
|
2021-01-01 16:44:55 +01:00
|
|
|
// non-preflight requests require the Access-Control-Allow-Origin header
|
|
|
|
if let Some(header) = cfg.origin.header_value(state) {
|
|
|
|
headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, header);
|
|
|
|
}
|
2020-05-14 23:30:59 +02:00
|
|
|
|
2021-01-01 16:44:55 +01:00
|
|
|
// if the origin is copied over, we should tell the browser by specifying the Vary header
|
|
|
|
if cfg.origin.varies() {
|
|
|
|
let vary = headers.get(VARY).map(|vary| format!("{},origin", vary.to_str().unwrap()));
|
|
|
|
headers.insert(VARY, vary.as_deref().unwrap_or("origin").parse().unwrap());
|
|
|
|
}
|
2020-05-14 23:30:59 +02:00
|
|
|
|
2021-01-01 16:44:55 +01:00
|
|
|
// if we allow credentials, tell the browser
|
|
|
|
if cfg.credentials {
|
|
|
|
headers.insert(ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true"));
|
|
|
|
}
|
2020-05-14 23:30:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-16 14:22:23 +02:00
|
|
|
/// Add CORS routing for your path. This is required for handling preflight requests.
|
2020-09-15 15:10:41 +02:00
|
|
|
///
|
2020-05-16 14:22:23 +02:00
|
|
|
/// Example:
|
2020-09-15 15:10:41 +02:00
|
|
|
///
|
2020-05-16 14:22:23 +02:00
|
|
|
/// ```rust,no_run
|
|
|
|
/// # use gotham::{hyper::{Body, Method, Response}, router::builder::*};
|
|
|
|
/// # use gotham_restful::*;
|
|
|
|
/// build_simple_router(|router| {
|
|
|
|
/// // The handler that needs preflight handling
|
|
|
|
/// router.post("/foo").to(|state| {
|
|
|
|
/// let mut res : Response<Body> = unimplemented!();
|
|
|
|
/// handle_cors(&state, &mut res);
|
|
|
|
/// (state, res)
|
|
|
|
/// });
|
|
|
|
/// // Add preflight handling
|
|
|
|
/// router.cors("/foo", Method::POST);
|
|
|
|
/// });
|
|
|
|
/// ```
|
2020-05-14 23:30:59 +02:00
|
|
|
pub trait CorsRoute<C, P>
|
|
|
|
where
|
2020-09-15 15:10:41 +02:00
|
|
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
|
|
|
P: RefUnwindSafe + Send + Sync + 'static
|
2020-05-14 23:30:59 +02:00
|
|
|
{
|
2020-05-16 14:22:23 +02:00
|
|
|
/// Handle a preflight request on `path` for `method`. To configure the behaviour, use
|
2020-11-22 23:55:52 +01:00
|
|
|
/// [CorsConfig].
|
2020-09-15 15:10:41 +02:00
|
|
|
fn cors(&mut self, path: &str, method: Method);
|
2020-05-14 23:30:59 +02:00
|
|
|
}
|
|
|
|
|
2020-09-15 15:10:41 +02:00
|
|
|
fn cors_preflight_handler(state: State) -> (State, Response<Body>) {
|
2020-05-14 23:30:59 +02:00
|
|
|
let config = CorsConfig::try_borrow_from(&state);
|
|
|
|
|
|
|
|
// prepare the response
|
|
|
|
let mut res = create_empty_response(&state, StatusCode::NO_CONTENT);
|
|
|
|
let headers = res.headers_mut();
|
2021-01-01 16:44:55 +01:00
|
|
|
let mut vary: Vec<HeaderName> = Vec::new();
|
2020-05-14 23:30:59 +02:00
|
|
|
|
|
|
|
// copy the request method over to the response
|
2020-09-15 15:10:41 +02:00
|
|
|
let method = HeaderMap::borrow_from(&state)
|
|
|
|
.get(ACCESS_CONTROL_REQUEST_METHOD)
|
|
|
|
.unwrap()
|
|
|
|
.clone();
|
2020-05-14 23:30:59 +02:00
|
|
|
headers.insert(ACCESS_CONTROL_ALLOW_METHODS, method);
|
2021-01-01 16:44:55 +01:00
|
|
|
vary.push(ACCESS_CONTROL_REQUEST_METHOD);
|
2020-05-14 23:30:59 +02:00
|
|
|
|
2021-01-01 16:44:55 +01:00
|
|
|
if let Some(cfg) = config {
|
|
|
|
// if we allow any headers, copy them over
|
|
|
|
if let Some(header) = cfg.headers.header_value(&state) {
|
|
|
|
headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, header);
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the headers are copied over, we should tell the browser by specifying the Vary header
|
|
|
|
if cfg.headers.varies() {
|
|
|
|
vary.push(ACCESS_CONTROL_REQUEST_HEADERS);
|
2020-05-14 23:30:59 +02:00
|
|
|
}
|
|
|
|
|
2021-01-01 16:44:55 +01:00
|
|
|
// set the max age for the preflight cache
|
|
|
|
if let Some(age) = config.map(|cfg| cfg.max_age) {
|
|
|
|
headers.insert(ACCESS_CONTROL_MAX_AGE, age.into());
|
|
|
|
}
|
2020-05-14 23:30:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// make sure the browser knows that this request was based on the method
|
2021-01-01 16:44:55 +01:00
|
|
|
headers.insert(VARY, vary.join(",").parse().unwrap());
|
2020-09-15 15:10:41 +02:00
|
|
|
|
2020-05-14 23:30:59 +02:00
|
|
|
handle_cors(&state, &mut res);
|
|
|
|
(state, res)
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<D, C, P> CorsRoute<C, P> for D
|
|
|
|
where
|
2020-09-15 15:10:41 +02:00
|
|
|
D: DrawRoutes<C, P>,
|
|
|
|
C: PipelineHandleChain<P> + Copy + Send + Sync + 'static,
|
|
|
|
P: RefUnwindSafe + Send + Sync + 'static
|
2020-05-14 23:30:59 +02:00
|
|
|
{
|
2020-09-15 15:10:41 +02:00
|
|
|
fn cors(&mut self, path: &str, method: Method) {
|
2020-05-14 23:30:59 +02:00
|
|
|
let matcher = AccessControlRequestMethodMatcher::new(method);
|
2020-09-15 15:10:41 +02:00
|
|
|
self.options(path).extend_route_matcher(matcher).to(cors_preflight_handler);
|
2020-05-13 19:10:53 +02:00
|
|
|
}
|
|
|
|
}
|