diff options
| author | Martin Fischer <martin@push-f.com> | 2021-01-19 22:49:09 +0100 | 
|---|---|---|
| committer | Martin Fischer <martin@push-f.com> | 2021-01-19 23:17:45 +0100 | 
| commit | c55c4a49414f9dbcb637ba5ea765f4af9aebf807 (patch) | |
| tree | 691a4a6838a19d5793002f4350c35302561d9152 /src | |
| parent | e63998de1eb6c348438ecbf6334eb11ec9236bcb (diff) | |
request: improve error handling with thiserrorv0.2.1
bump version to 0.2.1
Diffstat (limited to 'src')
| -rw-r--r-- | src/error.rs | 12 | ||||
| -rw-r--r-- | src/lib.rs | 2 | ||||
| -rw-r--r-- | src/request.rs | 82 | ||||
| -rw-r--r-- | src/security.rs | 18 | 
4 files changed, 97 insertions, 17 deletions
| diff --git a/src/error.rs b/src/error.rs index d40ddf3..5f35da2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,8 @@ use std::fmt::Display;  use hyper::StatusCode;  /// Encapsulates a status code and an error message. +/// +/// All client errors in [`crate::request`] implement [`Into<Error>`].  #[derive(Debug)]  pub struct Error {      pub code: StatusCode, @@ -41,4 +43,14 @@ impl Error {      pub fn method_not_allowed(message: String) -> Self {          Error{code: StatusCode::METHOD_NOT_ALLOWED, message}      } +} + +macro_rules! impl_into_http_error { +    ($type:ident, $status:expr) => { +       impl From<$type> for crate::error::Error { +            fn from(err: $type) -> Self { +                Self{code: $status, message: format!("{}", err)} +            } +       } +    };  }
\ No newline at end of file @@ -4,8 +4,8 @@ pub use error::Error;  pub use mime;  pub use httpdate; +#[macro_use] mod error;  pub mod security;  pub mod request;  pub mod response; -mod error;  mod signed;
\ No newline at end of file diff --git a/src/request.rs b/src/request.rs index 7846367..79ca2ae 100644 --- a/src/request.rs +++ b/src/request.rs @@ -8,7 +8,9 @@ use hyper::{body::Bytes, header};  use hyper::http::request::Parts as ReqParts;  use std::collections::HashMap; -use crate::{Error, security}; +use crate::security; + +use error::*;  type HyperRequest = hyper::Request<hyper::Body>; @@ -72,14 +74,14 @@ impl Parts {      }      /// Parses the query string of the request into a given struct. -    pub fn query<T: DeserializeOwned>(&self) -> Result<T,Error> { -        serde_urlencoded::from_str::<T>(self.parts.uri.query().unwrap_or("")).map_err(|e|Error::bad_request(e.to_string())) +    pub fn query<T: DeserializeOwned>(&self) -> Result<T,QueryError> { +        serde_urlencoded::from_str::<T>(self.parts.uri.query().unwrap_or("")).map_err(QueryError)      }  }  impl Body { -    pub async fn into_bytes(self) -> Result<Bytes,Error> { -        hyper::body::to_bytes(self.body).await.map_err(|_|Error::internal("failed to read body".to_string())) +    pub async fn into_bytes(self) -> Result<Bytes, BodyError> { +        hyper::body::to_bytes(self.body).await.map_err(BodyError)      }      /// Parses a `application/x-www-form-urlencoded` request body into a given struct. @@ -102,10 +104,10 @@ impl Body {      ///     Ok(Response::new(format!("hello {}", msg.text).into()))      /// }      /// ``` -    pub async fn into_form<T: DeserializeOwned>(self) -> Result<T,Error> { +    pub async fn into_form<T: DeserializeOwned>(self) -> Result<T, FormError> {          self.enforce_content_type(APPLICATION_WWW_FORM_URLENCODED)?;          let full_body = self.into_bytes().await?; -        serde_urlencoded::from_bytes::<T>(&full_body).map_err(|e|Error::bad_request(e.to_string())) +        serde_urlencoded::from_bytes::<T>(&full_body).map_err(FormError::Deserialize)      }      /// Parses a `application/x-www-form-urlencoded` request body into a given struct. @@ -140,20 +142,76 @@ impl Body {      ///     Ok(response)      /// }      /// ``` -    pub async fn into_form_csrf<T: DeserializeOwned>(self, csrf_token: &security::CsrfToken) -> Result<T,Error> { +    pub async fn into_form_csrf<T: DeserializeOwned>(self, csrf_token: &security::CsrfToken) -> Result<T, CsrfProtectedFormError> {          self.enforce_content_type(APPLICATION_WWW_FORM_URLENCODED)?;          let full_body = self.into_bytes().await?; -        let csrf_data = serde_urlencoded::from_bytes::<CsrfData>(&full_body).map_err(|_|Error::bad_request("no csrf token".to_string()))?; +        let csrf_data = serde_urlencoded::from_bytes::<CsrfData>(&full_body).map_err(|_| CsrfProtectedFormError::NoCsrf)?;          csrf_token.matches(csrf_data.csrf)?; -        serde_urlencoded::from_bytes::<T>(&full_body).map_err(|e|Error::bad_request(e.to_string())) +        serde_urlencoded::from_bytes::<T>(&full_body).map_err(CsrfProtectedFormError::Deserialize)      } -    fn enforce_content_type(&self, mime: Mime) -> Result<(),Error> { +    fn enforce_content_type(&self, mime: Mime) -> Result<(), WrongContentTypeError> {          if let Some(content_type) = &self.content_type {              if *content_type == mime.to_string() {                  return Ok(())              }          } -        Err(Error::bad_request(format!("expected content-type: {}", mime))) +        Err(WrongContentTypeError{expected: mime, received: self.content_type.as_ref().and_then(|h| h.to_str().ok().map(|s| s.to_owned()))}) +    } +} + +pub mod error { +    use mime::Mime; +    use thiserror::Error; +    use hyper::StatusCode; + +    use crate::security::CsrfError; +    #[derive(Error, Debug)] +    #[error("query deserialize error: {0}")] +    pub struct QueryError(pub serde_urlencoded::de::Error); +    impl_into_http_error!(QueryError, StatusCode::BAD_REQUEST); + +    #[derive(Error, Debug)] +    #[error("failed to read body")] +    pub struct BodyError(pub hyper::Error); +    impl_into_http_error!(BodyError, StatusCode::BAD_REQUEST); + +    #[derive(Error, Debug)] +    #[error("expected Content-Type {expected} but received {}", received.as_ref().unwrap_or(&"nothing".to_owned()))] +    pub struct WrongContentTypeError { +        pub expected: Mime, +        pub received: Option<String>, +    } + +    #[derive(Error, Debug)] +    pub enum FormError { +        #[error("{0}")] +        ContentType(#[from] WrongContentTypeError), + +        #[error("{0}")] +        Body(#[from] BodyError), + +        #[error("form deserialize error: {0}")] +        Deserialize(#[from] serde_urlencoded::de::Error), +    } +    impl_into_http_error!(FormError, StatusCode::BAD_REQUEST); + +    #[derive(Error, Debug)] +    pub enum CsrfProtectedFormError { +        #[error("{0}")] +        ContentType(#[from] WrongContentTypeError), + +        #[error("{0}")] +        Body(#[from] BodyError), + +        #[error("form deserialize error: {0}")] +        Deserialize(#[from] serde_urlencoded::de::Error), + +        #[error("no csrf token in form data")] +        NoCsrf, + +        #[error("{0}")] +        Csrf(#[from] CsrfError),      } +    impl_into_http_error!(CsrfProtectedFormError, StatusCode::BAD_REQUEST);  }
\ No newline at end of file diff --git a/src/security.rs b/src/security.rs index 4a17fe3..c12a97f 100644 --- a/src/security.rs +++ b/src/security.rs @@ -3,10 +3,11 @@  use rand::{Rng, distributions::Alphanumeric};  use ::cookie::Cookie;  use time::{Duration, OffsetDateTime}; +use thiserror::Error;  pub use crate::signed::Key; -use crate::{Error, request::Parts, response::Response}; +use crate::{request::Parts, response::Response};  /// A cookie-based CSRF token to be used with [`crate::request::Body::into_form_csrf`].  pub struct CsrfToken { @@ -14,6 +15,15 @@ pub struct CsrfToken {      from_client: bool,  } +#[derive(Error, Debug)] +pub enum CsrfError { +    #[error("expected csrf cookie")] +    NoCookie, + +    #[error("csrf parameter doesn't match csrf cookie")] +    Mismatch, +} +  impl CsrfToken {      /// Retrieves the CSRF token from a `csrf` cookie or generates      /// a new token and stores it as a cookie if it doesn't exist. @@ -34,12 +44,12 @@ impl CsrfToken {          format!("<input name=csrf type=hidden value=\"{}\">", self.token)      } -    pub(crate) fn matches(&self, str: String) -> Result<(), Error> { +    pub(crate) fn matches(&self, str: String) -> Result<(), CsrfError> {          if !self.from_client { -            return Err(Error::bad_request("expected csrf cookie".to_string())) +            return Err(CsrfError::NoCookie)          }          if self.token != str { -            return Err(Error::bad_request("csrf parameter doesn't match csrf cookie".to_string())) +            return Err(CsrfError::Mismatch)          }          Ok(())      } | 
