Tidy custom forward status changes, update docs.

This commit is contained in:
Sergio Benitez 2023-04-11 12:39:02 -07:00
parent 055ad107df
commit 9b0564ed27
12 changed files with 145 additions and 104 deletions

View File

@ -117,8 +117,8 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied
<Arc<[u8]> as Responder<'r, 'static>> <Arc<[u8]> as Responder<'r, 'static>>
<Arc<str> as Responder<'r, 'static>> <Arc<str> as Responder<'r, 'static>>
and $N others and $N others
note: required by a bound in `route::handler::<impl Outcome<rocket::Response<'o>, Status, rocket::Data<'o>>>::from` note: required by a bound in `route::handler::<impl Outcome<rocket::Response<'o>, Status, (rocket::Data<'o>, Status)>>::from`
--> $WORKSPACE/core/lib/src/route/handler.rs --> $WORKSPACE/core/lib/src/route/handler.rs
| |
| pub fn from<R: Responder<'r, 'o>>(req: &'r Request<'_>, responder: R) -> Outcome<'r> { | pub fn from<R: Responder<'r, 'o>>(req: &'r Request<'_>, responder: R) -> Outcome<'r> {
| ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::<impl Outcome<Response<'o>, Status, Data<'o>>>::from` | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::<impl Outcome<Response<'o>, Status, (Data<'o>, Status)>>::from`

View File

@ -117,8 +117,8 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied
<Arc<[u8]> as Responder<'r, 'static>> <Arc<[u8]> as Responder<'r, 'static>>
<Arc<str> as Responder<'r, 'static>> <Arc<str> as Responder<'r, 'static>>
and $N others and $N others
note: required by a bound in `route::handler::<impl Outcome<rocket::Response<'o>, Status, rocket::Data<'o>>>::from` note: required by a bound in `route::handler::<impl Outcome<rocket::Response<'o>, Status, (rocket::Data<'o>, Status)>>::from`
--> $WORKSPACE/core/lib/src/route/handler.rs --> $WORKSPACE/core/lib/src/route/handler.rs
| |
| pub fn from<R: Responder<'r, 'o>>(req: &'r Request<'_>, responder: R) -> Outcome<'r> { | pub fn from<R: Responder<'r, 'o>>(req: &'r Request<'_>, responder: R) -> Outcome<'r> {
| ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::<impl Outcome<Response<'o>, Status, Data<'o>>>::from` | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::<impl Outcome<Response<'o>, Status, (Data<'o>, Status)>>::from`

View File

@ -64,7 +64,8 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
/// configured `ca_certs` and with respect to SNI, if any. See [module level /// configured `ca_certs` and with respect to SNI, if any. See [module level
/// docs](crate::mtls) for configuration details. /// docs](crate::mtls) for configuration details.
/// ///
/// If the client does not present certificates, the guard _forwards_. /// If the client does not present certificates, the guard _forwards_ with a
/// status of 401 Unauthorized.
/// ///
/// If the certificate chain fails to validate or verify, the guard _fails_ with /// If the certificate chain fails to validate or verify, the guard _fails_ with
/// the respective [`Error`]. /// the respective [`Error`].
@ -81,6 +82,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
/// use rocket::mtls::{self, bigint::BigUint, Certificate}; /// use rocket::mtls::{self, bigint::BigUint, Certificate};
/// use rocket::request::{Request, FromRequest, Outcome}; /// use rocket::request::{Request, FromRequest, Outcome};
/// use rocket::outcome::try_outcome; /// use rocket::outcome::try_outcome;
/// use rocket::http::Status;
/// ///
/// // The serial number for the certificate issued to the admin. /// // The serial number for the certificate issued to the admin.
/// const ADMIN_SERIAL: &str = "65828378108300243895479600452308786010218223563"; /// const ADMIN_SERIAL: &str = "65828378108300243895479600452308786010218223563";
@ -97,7 +99,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
/// if let Some(true) = cert.has_serial(ADMIN_SERIAL) { /// if let Some(true) = cert.has_serial(ADMIN_SERIAL) {
/// Outcome::Success(CertifiedAdmin(cert)) /// Outcome::Success(CertifiedAdmin(cert))
/// } else { /// } else {
/// Outcome::Forward(()) /// Outcome::Forward(Status::Unauthorized)
/// } /// }
/// } /// }
/// } /// }

View File

@ -22,10 +22,10 @@ impl<'r, S, E> IntoOutcome<S, (Status, E), (Data<'r>, Status)> for Result<S, E>
} }
#[inline] #[inline]
fn or_forward(self, (data, error): (Data<'r>, Status)) -> Outcome<'r, S, E> { fn or_forward(self, (data, status): (Data<'r>, Status)) -> Outcome<'r, S, E> {
match self { match self {
Ok(val) => Success(val), Ok(val) => Success(val),
Err(_) => Forward((data, error)) Err(_) => Forward((data, status))
} }
} }
} }

View File

@ -142,7 +142,7 @@ impl<S, E> IntoOutcome<S, (Status, E), Status> for Result<S, E> {
/// Extracts the [`Route`] from the request if one is available. When used /// Extracts the [`Route`] from the request if one is available. When used
/// as a request guard in a route handler, this will always succeed. Outside /// as a request guard in a route handler, this will always succeed. Outside
/// of a route handler, a route may not be available, and the request is /// of a route handler, a route may not be available, and the request is
/// forwarded with a 500 status. /// forwarded with a 500 Internal Server Error status.
/// ///
/// For more information on when an `&Route` is available, see /// For more information on when an `&Route` is available, see
/// [`Request::route()`]. /// [`Request::route()`].
@ -165,19 +165,19 @@ impl<S, E> IntoOutcome<S, (Status, E), Status> for Result<S, E> {
/// ///
/// Extracts the [`ContentType`] from the incoming request via /// Extracts the [`ContentType`] from the incoming request via
/// [`Request::content_type()`]. If the request didn't specify a /// [`Request::content_type()`]. If the request didn't specify a
/// Content-Type, the request is forwarded. /// Content-Type, the request is forwarded with a 404 Not Found status.
/// ///
/// * **IpAddr** /// * **IpAddr**
/// ///
/// Extracts the client ip address of the incoming request as an [`IpAddr`] /// Extracts the client ip address of the incoming request as an [`IpAddr`]
/// via [`Request::client_ip()`]. If the client's IP address is not known, /// via [`Request::client_ip()`]. If the client's IP address is not known,
/// the request is forwarded. /// the request is forwarded with a 404 Not Found status.
/// ///
/// * **SocketAddr** /// * **SocketAddr**
/// ///
/// Extracts the remote address of the incoming request as a [`SocketAddr`] /// Extracts the remote address of the incoming request as a [`SocketAddr`]
/// via [`Request::remote()`]. If the remote address is not known, the /// via [`Request::remote()`]. If the remote address is not known, the
/// request is forwarded. /// request is forwarded with a 404 Not Found status.
/// ///
/// * **Option&lt;T>** _where_ **T: FromRequest** /// * **Option&lt;T>** _where_ **T: FromRequest**
/// ///
@ -193,7 +193,7 @@ impl<S, E> IntoOutcome<S, (Status, E), Status> for Result<S, E> {
/// `FromRequest` implementation. If derivation is a `Success`, the value is /// `FromRequest` implementation. If derivation is a `Success`, the value is
/// returned in `Ok`. If the derivation is a `Failure`, the error value is /// returned in `Ok`. If the derivation is a `Failure`, the error value is
/// returned in `Err`. If the derivation is a `Forward`, the request is /// returned in `Err`. If the derivation is a `Forward`, the request is
/// forwarded. /// forwarded with the same status code as the original forward.
/// ///
/// [`Config`]: crate::config::Config /// [`Config`]: crate::config::Config
/// ///
@ -503,7 +503,7 @@ impl<'r, T: FromRequest<'r>> FromRequest<'r> for Result<T, T::Error> {
match T::from_request(request).await { match T::from_request(request).await {
Success(val) => Success(Ok(val)), Success(val) => Success(Ok(val)),
Failure((_, e)) => Success(Err(e)), Failure((_, e)) => Success(Err(e)),
Forward(_) => Forward(Status::NotFound), Forward(status) => Forward(status),
} }
} }
} }
@ -519,3 +519,12 @@ impl<'r, T: FromRequest<'r>> FromRequest<'r> for Option<T> {
} }
} }
} }
#[crate::async_trait]
impl<'r, T: FromRequest<'r>> FromRequest<'r> for Outcome<T, T::Error> {
type Error = std::convert::Infallible;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
Success(T::from_request(request).await)
}
}

View File

@ -221,8 +221,8 @@ impl<'r, 'o: 'r> Outcome<'o> {
/// Return the `Outcome` of response to `req` from `responder`. /// Return the `Outcome` of response to `req` from `responder`.
/// ///
/// If the responder returns `Ok`, an outcome of `Success` is returned with /// If the responder returns `Ok`, an outcome of `Success` is returned with
/// the response. If the responder returns `Err`, an outcome of `Forward` is /// the response. If the responder returns `Err`, an outcome of `Forward`
/// returned. /// with a status of `404 Not Found` is returned.
/// ///
/// # Example /// # Example
/// ///

View File

@ -80,7 +80,7 @@ use crate::http::Status;
/// // Or alternatively, using `Rocket::state()`: /// // Or alternatively, using `Rocket::state()`:
/// let outcome = request.rocket().state::<MyConfig>() /// let outcome = request.rocket().state::<MyConfig>()
/// .map(|my_config| Item(&my_config.user_val)) /// .map(|my_config| Item(&my_config.user_val))
/// .or_forward(Status::NotFound); /// .or_forward(Status::InternalServerError);
/// ///
/// outcome /// outcome
/// } /// }

View File

@ -1,79 +0,0 @@
#[macro_use] extern crate rocket;
use rocket::http::Status;
use rocket::request::{self, Request, FromRequest};
pub struct Authenticated;
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Authenticated {
type Error = std::convert::Infallible;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
if request.headers().contains("Authenticated") {
request::Outcome::Success(Authenticated)
} else {
request::Outcome::Forward(Status::Unauthorized)
}
}
}
#[get("/one")]
pub async fn get_protected_one(_user: Authenticated) -> &'static str {
"Protected"
}
#[get("/one", rank = 2)]
pub async fn get_public_one() -> &'static str {
"Public"
}
#[get("/two")]
pub async fn get_protected_two(_user: Authenticated) -> &'static str {
"Protected"
}
mod tests {
use super::*;
use rocket::routes;
use rocket::local::blocking::Client;
use rocket::http::{Header, Status};
#[test]
fn one_protected_returned_for_authenticated() {
let rocket = rocket::build().mount("/",
routes![get_protected_one, get_public_one, get_protected_two]);
let client = Client::debug(rocket).unwrap();
let req = client.get("/one").header(Header::new("Authenticated", "true"));
let response = req.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string(), Some("Protected".into()));
}
#[test]
fn one_public_returned_for_unauthenticated() {
let rocket = rocket::build().mount("/",
routes![get_protected_one, get_public_one, get_protected_two]);
let client = Client::debug(rocket).unwrap();
let req = client.get("/one");
let response = req.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string(), Some("Public".into()));
}
#[test]
fn two_unauthorized_returned_for_unauthenticated() {
let rocket = rocket::build().mount("/",
routes![get_protected_one, get_public_one, get_protected_two]);
let client = Client::debug(rocket).unwrap();
let req = client.get("/two");
let response = req.dispatch();
assert_eq!(response.status(), Status::Unauthorized);
}
}

View File

@ -0,0 +1,108 @@
#[macro_use] extern crate rocket;
use rocket::http::Status;
use rocket::request::{self, Request, FromRequest};
struct Authenticated;
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Authenticated {
type Error = std::convert::Infallible;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
if request.headers().contains("Authenticated") {
request::Outcome::Success(Authenticated)
} else {
request::Outcome::Forward(Status::Unauthorized)
}
}
}
struct TeapotForward;
#[rocket::async_trait]
impl<'r> FromRequest<'r> for TeapotForward {
type Error = std::convert::Infallible;
async fn from_request(_: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
request::Outcome::Forward(Status::ImATeapot)
}
}
#[get("/auth")]
fn auth(_name: Authenticated) -> &'static str {
"Protected"
}
#[get("/auth", rank = 2)]
fn public() -> &'static str {
"Public"
}
#[get("/auth", rank = 3)]
fn teapot(_teapot: TeapotForward) -> &'static str {
"Protected"
}
#[get("/need-auth")]
fn auth_needed(_auth: Authenticated) -> &'static str {
"Have Auth"
}
#[catch(401)]
fn catcher() -> &'static str {
"Custom Catcher"
}
mod tests {
use super::*;
use rocket::routes;
use rocket::local::blocking::Client;
use rocket::http::{Header, Status};
#[test]
fn authorized_forwards() {
let client = Client::debug_with(routes![auth, public, auth_needed]).unwrap();
let response = client.get("/auth")
.header(Header::new("Authenticated", "true"))
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string().unwrap(), "Protected");
let response = client.get("/auth").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string().unwrap(), "Public");
let response = client.get("/need-auth")
.header(Header::new("Authenticated", "true"))
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string().unwrap(), "Have Auth");
let response = client.get("/need-auth").dispatch();
assert_eq!(response.status(), Status::Unauthorized);
assert!(response.into_string().unwrap().contains("Rocket"));
}
#[test]
fn unauthorized_custom_catcher() {
let rocket = rocket::build()
.mount("/", routes![auth_needed])
.register("/", catchers![catcher]);
let client = Client::debug(rocket).unwrap();
let response = client.get("/need-auth").dispatch();
assert_eq!(response.status(), Status::Unauthorized);
assert_eq!(response.into_string().unwrap(), "Custom Catcher");
}
#[test]
fn use_last_forward() {
let client = Client::debug_with(routes![auth, teapot]).unwrap();
let response = client.get("/auth").dispatch();
assert_eq!(response.status(), Status::ImATeapot);
}
}

View File

@ -24,7 +24,7 @@ impl<'r> FromRequest<'r> for User {
.get_private("user_id") .get_private("user_id")
.and_then(|cookie| cookie.value().parse().ok()) .and_then(|cookie| cookie.value().parse().ok())
.map(User) .map(User)
.or_forward(Status::NotFound) .or_forward(Status::Unauthorized)
} }
} }

View File

@ -206,9 +206,10 @@ fn hello(name: &str, age: u8, cool: bool) { /* ... */ }
What if `cool` isn't a `bool`? Or, what if `age` isn't a `u8`? When a parameter What if `cool` isn't a `bool`? Or, what if `age` isn't a `u8`? When a parameter
type mismatch occurs, Rocket _forwards_ the request to the next matching route, type mismatch occurs, Rocket _forwards_ the request to the next matching route,
if there is any. This continues until a route doesn't forward the request or if there is any. This continues until a route succeeds or fails, or there are no
there are no remaining routes to try. When there are no remaining routes, a other matching routes to try. When there are no remaining routes, the [error
customizable **404 error** is returned. catcher](#error-catchers) associated with the status set by the last forwarding
guard is called.
Routes are attempted in increasing _rank_ order. Rocket chooses a default Routes are attempted in increasing _rank_ order. Rocket chooses a default
ranking from -12 to -1, detailed in the next section, but a route's rank can also ranking from -12 to -1, detailed in the next section, but a route's rank can also
@ -436,13 +437,14 @@ We start with two request guards:
The `FromRequest` implementation for `User` checks that a cookie identifies The `FromRequest` implementation for `User` checks that a cookie identifies
a user and returns a `User` value if so. If no user can be authenticated, a user and returns a `User` value if so. If no user can be authenticated,
the guard forwards. the guard forwards with a 401 Unauthorized status.
* `AdminUser`: A user authenticated as an administrator. * `AdminUser`: A user authenticated as an administrator.
The `FromRequest` implementation for `AdminUser` checks that a cookie The `FromRequest` implementation for `AdminUser` checks that a cookie
identifies an _administrative_ user and returns an `AdminUser` value if so. identifies an _administrative_ user and returns an `AdminUser` value if so.
If no user can be authenticated, the guard forwards. If no user can be authenticated, the guard forwards with a 401 Unauthorized
status.
We now use these two guards in combination with forwarding to implement the We now use these two guards in combination with forwarding to implement the
following three routes, each leading to an administrative control panel at following three routes, each leading to an administrative control panel at

View File

@ -141,14 +141,13 @@ impl<'r> FromRequest<'r> for Item<'r> {
// Or alternatively, using `Rocket::state()`: // Or alternatively, using `Rocket::state()`:
let outcome = request.rocket().state::<MyConfig>() let outcome = request.rocket().state::<MyConfig>()
.map(|my_config| Item(&my_config.user_val)) .map(|my_config| Item(&my_config.user_val))
.or_forward(Status::NotFound); .or_forward(Status::InternalServerError);
outcome outcome
} }
} }
``` ```
[`Request::guard()`]: @api/rocket/struct.Request.html#method.guard [`Request::guard()`]: @api/rocket/struct.Request.html#method.guard
[`Rocket::state()`]: @api/rocket/struct.Rocket.html#method.state [`Rocket::state()`]: @api/rocket/struct.Rocket.html#method.state