mirror of https://github.com/rwf2/Rocket.git
Tidy custom forward status changes, update docs.
This commit is contained in:
parent
055ad107df
commit
9b0564ed27
|
@ -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`
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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)
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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
|
||||
/// }
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue