Tidy custom forward status changes, update docs.

This commit is contained in:
Sergio Benitez 2023-04-11 12:39:02 -07:00
parent 1f06bb0b73
commit 403604c402
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<str> as Responder<'r, 'static>>
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
|
| 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<str> as Responder<'r, 'static>>
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
|
| 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
/// 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
/// 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::request::{Request, FromRequest, Outcome};
/// use rocket::outcome::try_outcome;
/// use rocket::http::Status;
///
/// // The serial number for the certificate issued to the admin.
/// 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) {
/// Outcome::Success(CertifiedAdmin(cert))
/// } 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]
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 {
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
/// 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
/// forwarded with a 500 status.
/// forwarded with a 500 Internal Server Error status.
///
/// For more information on when an `&Route` is available, see
/// [`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
/// [`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**
///
/// 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,
/// the request is forwarded.
/// the request is forwarded with a 404 Not Found status.
///
/// * **SocketAddr**
///
/// Extracts the remote address of the incoming request as a [`SocketAddr`]
/// 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**
///
@ -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
/// 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
/// forwarded.
/// forwarded with the same status code as the original forward.
///
/// [`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 {
Success(val) => Success(Ok(val)),
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`.
///
/// If the responder returns `Ok`, an outcome of `Success` is returned with
/// the response. If the responder returns `Err`, an outcome of `Forward` is
/// returned.
/// the response. If the responder returns `Err`, an outcome of `Forward`
/// with a status of `404 Not Found` is returned.
///
/// # Example
///

View File

@ -80,7 +80,7 @@ use crate::http::Status;
/// // Or alternatively, using `Rocket::state()`:
/// let outcome = request.rocket().state::<MyConfig>()
/// .map(|my_config| Item(&my_config.user_val))
/// .or_forward(Status::NotFound);
/// .or_forward(Status::InternalServerError);
///
/// 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")
.and_then(|cookie| cookie.value().parse().ok())
.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
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
there are no remaining routes to try. When there are no remaining routes, a
customizable **404 error** is returned.
if there is any. This continues until a route succeeds or fails, or there are no
other matching routes to try. When there are no remaining routes, the [error
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
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
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.
The `FromRequest` implementation for `AdminUser` checks that a cookie
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
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()`:
let outcome = request.rocket().state::<MyConfig>()
.map(|my_config| Item(&my_config.user_val))
.or_forward(Status::NotFound);
.or_forward(Status::InternalServerError);
outcome
}
}
```
[`Request::guard()`]: @api/rocket/struct.Request.html#method.guard
[`Rocket::state()`]: @api/rocket/struct.Rocket.html#method.state