Skip to main content

rocket_cors/
fairing.rs

1//! Fairing implementation
2
3#[allow(unused_imports)]
4use ::log::{error, info};
5use rocket::http::{self, uri::Origin, Status};
6use rocket::{self, error_, info_, outcome::Outcome, Request};
7
8use crate::{
9    actual_request_response, origin, preflight_response, request_headers, validate, Cors, Error,
10};
11
12/// Request Local State to store CORS validation results
13enum CorsValidation {
14    Success,
15    Failure,
16}
17
18/// Create a `Handler` for Fairing error handling
19#[derive(Clone)]
20struct FairingErrorRoute {}
21
22#[rocket::async_trait]
23impl rocket::route::Handler for FairingErrorRoute {
24    async fn handle<'r>(
25        &self,
26        request: &'r Request<'_>,
27        _: rocket::Data<'r>,
28    ) -> rocket::route::Outcome<'r> {
29        let status = request
30            .param::<u16>(0)
31            .unwrap_or(Ok(0))
32            .unwrap_or_else(|e| {
33                error_!("Fairing Error Handling Route error: {:?}", e);
34                500
35            });
36        let status = Status::from_code(status).unwrap_or(Status::InternalServerError);
37        Outcome::Error(status)
38    }
39}
40
41/// Create a new `Route` for Fairing handling
42fn fairing_route(rank: isize) -> rocket::Route {
43    rocket::Route::ranked(rank, http::Method::Get, "/<status>", FairingErrorRoute {})
44}
45
46/// Modifies a `Request` to route to Fairing error handler
47fn route_to_fairing_error_handler(options: &Cors, status: u16, request: &mut Request<'_>) {
48    let origin = Origin::parse_owned(format!("{}/{}", options.fairing_route_base, status)).unwrap();
49
50    request.set_method(http::Method::Get);
51    request.set_uri(origin);
52}
53
54fn on_response_wrapper(
55    options: &Cors,
56    request: &Request<'_>,
57    response: &mut rocket::Response<'_>,
58) -> Result<(), Error> {
59    let origin = match origin(request)? {
60        None => {
61            // Not a CORS request
62            return Ok(());
63        }
64        Some(origin) => origin,
65    };
66
67    let result = request.local_cache(|| unreachable!("This should not be executed so late"));
68
69    if let CorsValidation::Failure = *result {
70        // Nothing else for us to do
71        return Ok(());
72    }
73
74    let origin = origin.to_string();
75    let cors_response = if request.method() == http::Method::Options {
76        let headers = request_headers(request)?;
77        preflight_response(options, &origin, headers.as_ref())
78    } else {
79        actual_request_response(options, &origin)
80    };
81
82    cors_response.merge(response);
83
84    // If this was an OPTIONS request and no route can be found, we should turn this
85    // into a HTTP 204 with no content body.
86    // This allows the user to not have to specify an OPTIONS route for everything.
87    //
88    // TODO: Is there anyway we can make this smarter? Only modify status codes for
89    // requests where an actual route exist?
90    if request.method() == http::Method::Options && request.route().is_none() {
91        info_!(
92            "CORS Fairing: Turned missing route {} into an OPTIONS pre-flight request",
93            request
94        );
95        response.set_status(Status::NoContent);
96        let _ = response.body_mut().take();
97    }
98    Ok(())
99}
100
101#[rocket::async_trait]
102impl rocket::fairing::Fairing for Cors {
103    fn info(&self) -> rocket::fairing::Info {
104        rocket::fairing::Info {
105            name: "CORS",
106            kind: rocket::fairing::Kind::Ignite
107                | rocket::fairing::Kind::Request
108                | rocket::fairing::Kind::Response,
109        }
110    }
111
112    async fn on_ignite(&self, rocket: rocket::Rocket<rocket::Build>) -> rocket::fairing::Result {
113        Ok(rocket.mount(
114            &self.fairing_route_base,
115            vec![fairing_route(self.fairing_route_rank)],
116        ))
117    }
118
119    async fn on_request(&self, request: &mut Request<'_>, _: &mut rocket::Data<'_>) {
120        let result = match validate(self, request) {
121            Ok(_) => CorsValidation::Success,
122            Err(err) => {
123                error_!("CORS Error: {}", err);
124                let status = err.status();
125                route_to_fairing_error_handler(self, status.code, request);
126                CorsValidation::Failure
127            }
128        };
129
130        let _ = request.local_cache(|| result);
131    }
132
133    async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut rocket::Response<'r>) {
134        if let Err(err) = on_response_wrapper(self, request, response) {
135            error_!("Fairings on_response error: {}\nMost likely a bug", err);
136            response.set_status(Status::InternalServerError);
137            let _ = response.body();
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use rocket::http::{Method, Status};
145    use rocket::local::blocking::Client;
146    use rocket::Rocket;
147
148    use crate::{AllowedHeaders, AllowedOrigins, Cors, CorsOptions};
149
150    const CORS_ROOT: &str = "/my_cors";
151
152    fn make_cors_options() -> Cors {
153        let allowed_origins = AllowedOrigins::some_exact(&["https://www.acme.com"]);
154
155        CorsOptions {
156            allowed_origins,
157            allowed_methods: vec![Method::Get].into_iter().map(From::from).collect(),
158            allowed_headers: AllowedHeaders::some(&["Authorization", "Accept"]),
159            allow_credentials: true,
160            fairing_route_base: CORS_ROOT.to_string(),
161
162            ..Default::default()
163        }
164        .to_cors()
165        .expect("Not to fail")
166    }
167
168    fn rocket(fairing: Cors) -> Rocket<rocket::Build> {
169        Rocket::build().attach(fairing)
170    }
171
172    #[test]
173    #[allow(non_snake_case)]
174    fn FairingErrorRoute_returns_passed_in_status() {
175        let client = Client::tracked(rocket(make_cors_options())).expect("to not fail");
176        let request = client.get(format!("{}/403", CORS_ROOT));
177        let response = request.dispatch();
178        assert_eq!(Status::Forbidden, response.status());
179    }
180
181    #[test]
182    #[allow(non_snake_case)]
183    fn FairingErrorRoute_returns_500_for_unknown_status() {
184        let client = Client::tracked(rocket(make_cors_options())).expect("to not fail");
185        let request = client.get(format!("{}/999", CORS_ROOT));
186        let response = request.dispatch();
187        assert_eq!(Status::InternalServerError, response.status());
188    }
189
190    #[rocket::async_test]
191    async fn error_route_is_mounted_on_ignite() {
192        let rocket = rocket(make_cors_options())
193            .ignite()
194            .await
195            .expect("to ignite");
196
197        let expected_uri = format!("{}/<status>", CORS_ROOT);
198        let error_route = rocket
199            .routes()
200            .find(|r| r.method == Method::Get && r.uri.to_string() == expected_uri);
201        assert!(error_route.is_some());
202    }
203
204    // Rest of the things can only be tested in integration tests
205}