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
//! Defines the `AccessControlRequestMethodMatcher`.

use crate::router::non_match::RouteNonMatch;
use crate::router::route::matcher::RouteMatcher;
use crate::state::{FromState, State};
use hyper::header::{HeaderMap, ACCESS_CONTROL_REQUEST_METHOD};
use hyper::{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())
        });
    }
}