diff options
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | README.md | 1 | ||||
-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 |
6 files changed, 101 insertions, 19 deletions
@@ -1,6 +1,6 @@ [package] name = "sputnik" -version = "0.2.0" +version = "0.2.1" authors = ["Martin Fischer <martin@push-f.com>"] license = "MIT" description = "A lightweight layer on top of hyper to facilitate building web applications." @@ -22,4 +22,5 @@ httpdate = "0.3.2" mime = "0.3" rand = "0.7.3" sha2 = "0.9" -time = "0.2"
\ No newline at end of file +time = "0.2" +thiserror = "1.0"
\ No newline at end of file @@ -10,6 +10,7 @@ Sputnik provides: (powered by the [cookie](https://crates.io/crates/cookie) crate) * parse query strings and HTML form data (powered by the [serde_urlencoded](https://crates.io/crates/serde_urlencoded) crate) +* an `Error` struct that makes it easy to centrally control the presentation of all error messages * cookie-based [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) tokens * `Key`: a convenience wrapper around HMAC (stolen from the cookie crate, so that you don't have to use `CookieJar`s if you don't need them) 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(()) } |