1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
//! Defines the `AccessControlRequestMethodMatcher`.

use crate::{
    router::{non_match::RouteNonMatch, route::matcher::RouteMatcher},
    state::{FromState, State},
};
use hyper::{
    header::{HeaderMap, ACCESS_CONTROL_REQUEST_METHOD},
    Method, StatusCode,
};

/// A route matcher that checks whether the value of the `Access-Control-Request-Method` header matches the defined value.
///
/// Usage:
///
/// ```rust
/// # use gotham::{helpers::http::response::create_empty_response,
/// #   hyper::{header::ACCESS_CONTROL_ALLOW_METHODS, Method, StatusCode},
/// #   router::{builder::*, route::matcher::AccessControlRequestMethodMatcher}
/// # };
/// let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
///
/// # build_simple_router(|route| {
/// // use the matcher for your request
/// route.options("/foo")
/// 	.extend_route_matcher(matcher)
/// 	.to(|state| {
/// 		// we know that this is a CORS preflight for a PUT request
/// 		let mut res = create_empty_response(&state, StatusCode::NO_CONTENT);
/// 		res.headers_mut().insert(ACCESS_CONTROL_ALLOW_METHODS, "PUT".parse().unwrap());
/// 		(state, res)
/// 	});
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct AccessControlRequestMethodMatcher {
    method: Method,
}

impl AccessControlRequestMethodMatcher {
    /// Construct a new matcher that matches if the `Access-Control-Request-Method` header matches `method`.
    /// Note that during matching the method is normalized according to the fetch specification, that is,
    /// byte-uppercased. This means that when using a custom `method` instead of a predefined one, make sure
    /// it is uppercased or this matcher will never succeed.
    pub fn new(method: Method) -> Self {
        Self { method }
    }
}

impl RouteMatcher for AccessControlRequestMethodMatcher {
    fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
        // according to the fetch specification, methods should be normalized by byte-uppercase
        // https://fetch.spec.whatwg.org/#concept-method
        match HeaderMap::borrow_from(state)
            .get(ACCESS_CONTROL_REQUEST_METHOD)
            .and_then(|value| value.to_str().ok())
            .and_then(|str| str.to_ascii_uppercase().parse::<Method>().ok())
        {
            Some(m) if m == self.method => Ok(()),
            _ => Err(RouteNonMatch::new(StatusCode::NOT_FOUND)),
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    fn with_state<F>(accept: Option<&str>, block: F)
    where
        F: FnOnce(&mut State) -> (),
    {
        State::with_new(|state| {
            let mut headers = HeaderMap::new();
            if let Some(acc) = accept {
                headers.insert(ACCESS_CONTROL_REQUEST_METHOD, acc.parse().unwrap());
            }
            state.put(headers);
            block(state);
        });
    }

    #[test]
    fn no_acrm_header() {
        let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
        with_state(None, |state| assert!(matcher.is_match(&state).is_err()));
    }

    #[test]
    fn correct_acrm_header() {
        let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
        with_state(Some("PUT"), |state| {
            assert!(matcher.is_match(&state).is_ok())
        });
        with_state(Some("put"), |state| {
            assert!(matcher.is_match(&state).is_ok())
        });
    }

    #[test]
    fn incorrect_acrm_header() {
        let matcher = AccessControlRequestMethodMatcher::new(Method::PUT);
        with_state(Some("DELETE"), |state| {
            assert!(matcher.is_match(&state).is_err())
        });
    }
}