aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml5
-rw-r--r--README.md1
-rw-r--r--src/error.rs12
-rw-r--r--src/lib.rs2
-rw-r--r--src/request.rs82
-rw-r--r--src/security.rs18
6 files changed, 101 insertions, 19 deletions
diff --git a/Cargo.toml b/Cargo.toml
index f974ccc..8cff0fb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
diff --git a/README.md b/README.md
index 04604fa..ff001c9 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/lib.rs b/src/lib.rs
index 9719823..63b372b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(())
}