diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 202 | ||||
| -rw-r--r-- | src/request.rs | 159 | ||||
| -rw-r--r-- | src/response.rs | 60 | ||||
| -rw-r--r-- | src/security.rs | 6 | 
4 files changed, 225 insertions, 202 deletions
| @@ -1,207 +1,11 @@  //! A lightweight layer on top of [Hyper](https://hyper.rs/)  //! to facilitate building web applications. -use std::collections::HashMap; -  pub use error::Error;  pub use mime; -use cookie::Cookie; -use header::HeaderName; -use mime::{APPLICATION_WWW_FORM_URLENCODED, Mime}; -use serde::{Deserialize, de::DeserializeOwned}; -use hyper::{Body, StatusCode, body::Bytes, header::{self, HeaderValue}, http::request::Parts}; -use time::{Duration, OffsetDateTime}; -  pub use httpdate; -type HyperRequest = hyper::Request<Body>; -type HyperResponse = hyper::Response<Body>; -  pub mod security; +pub mod request; +pub mod response;  mod error; -mod signed; - -/// Convenience wrapper around [`hyper::Request`]. -pub struct Request { -    body: Body, -    parts: Parts, -    cookies: Option<HashMap<String,Cookie<'static>>>, -} - -impl From<HyperRequest> for Request { -    fn from(req: HyperRequest) -> Self { -        let (parts, body) = req.into_parts(); -        Request{parts, body, cookies: None} -    } -} - -impl Into<HyperResponse> for Response { -    fn into(self) -> HyperResponse { -        self.res -    } -} - -fn enforce_content_type(req: &Parts, mime: Mime) -> Result<(),Error> { -    let received_type = req.headers.get(header::CONTENT_TYPE).ok_or(Error::bad_request(format!("expected content-type: {}", mime)))?; -    if *received_type != mime.to_string() { -        return Err(Error::bad_request(format!("expected content-type: {}", mime))) -    } -    Ok(()) -} - -#[derive(Deserialize)] -struct CsrfData { -    csrf: String, -} - -impl Request { -    pub fn cookies(&mut self) -> &HashMap<String,Cookie> { -        if let Some(ref cookies) = self.cookies { -            return cookies -        } -        let mut cookies = HashMap::new(); -        for header in self.parts.headers.get_all(header::COOKIE) { -            let raw_str = match std::str::from_utf8(header.as_bytes()) { -                Ok(string) => string, -                Err(_) => continue -            }; - -            for cookie_str in raw_str.split(';').map(|s| s.trim()) { -                if let Ok(cookie) = Cookie::parse_encoded(cookie_str) { -                    cookies.insert(cookie.name().to_string(), cookie.into_owned()); -                } -            } -        } -        self.cookies = Some(cookies); -        &self.cookies.as_ref().unwrap() -    } - -    pub fn method(&self) -> &hyper::Method { -        &self.parts.method -    } - -    pub fn uri(&self) -> &hyper::Uri { -        &self.parts.uri -    } - -    /// Parses a `application/x-www-form-urlencoded` request body into a given struct. -    /// -    /// This does make you vulnerable to CSRF so you normally want to use -    /// [`parse_form_csrf()`] instead. -    /// -    /// # Example -    /// -    /// ``` -    /// use hyper::{Response, Body}; -    /// use sputnik::{Request, Error}; -    /// use serde::Deserialize; -    /// -    /// #[derive(Deserialize)] -    /// struct Message {text: String, year: i64} -    /// -    /// async fn greet(req: &mut Request) -> Result<Response<Body>, Error> { -    ///     let msg: Message = req.into_form().await?; -    ///     Ok(Response::new(format!("hello {}", msg.text).into())) -    /// } -    /// ``` -    pub async fn into_form<T: DeserializeOwned>(&mut self) -> Result<T,Error> { -        enforce_content_type(&self.parts, APPLICATION_WWW_FORM_URLENCODED)?; -        let full_body = self.into_body().await?; -        serde_urlencoded::from_bytes::<T>(&full_body).map_err(|e|Error::bad_request(e.to_string())) -    } - -    /// Parses a `application/x-www-form-urlencoded` request body into a given struct. -    /// Protects from CSRF by checking that the request body contains the same token retrieved from the cookies. -    /// -    /// The CSRF parameter is expected as the `csrf` parameter in the request body. -    /// This means for HTML forms you need to embed the token as a hidden input. -    /// -    /// # Example -    /// -    /// ``` -    /// use hyper::{Method}; -    /// use sputnik::{Request, Response, Error}; -    /// use sputnik::security::CsrfToken; -    /// use serde::Deserialize; -    /// -    /// #[derive(Deserialize)] -    /// struct Message {text: String} -    /// -    /// async fn greet(req: &mut Request) -> Result<Response, Error> { -    ///     let mut response = Response::new(); -    ///     let csrf_token = CsrfToken::from_request(req, &mut response); -    ///     *response.body() = match (req.method()) { -    ///         &Method::GET => format!("<form method=post> -    ///             <input name=text>{}<button>Submit</button></form>", csrf_token.html_input()).into(), -    ///         &Method::POST => { -    ///             let msg: Message = req.into_form_csrf(&csrf_token).await?; -    ///             format!("hello {}", msg.text).into() -    ///         }, -    ///         _ => return Err(Error::method_not_allowed("only GET and POST allowed".to_owned())), -    ///     }; -    ///     Ok(response) -    /// } -    /// ``` -    pub async fn into_form_csrf<T: DeserializeOwned>(&mut self, csrf_token: &security::CsrfToken) -> Result<T,Error> { -        enforce_content_type(&self.parts, APPLICATION_WWW_FORM_URLENCODED)?; -        let full_body = self.into_body().await?; -        let csrf_data = serde_urlencoded::from_bytes::<CsrfData>(&full_body).map_err(|_|Error::bad_request("no csrf token".to_string()))?; -        csrf_token.matches(csrf_data.csrf)?; -        serde_urlencoded::from_bytes::<T>(&full_body).map_err(|e|Error::bad_request(e.to_string())) -    } - -    pub async fn into_body(&mut self) -> Result<Bytes,Error> { -        hyper::body::to_bytes(&mut self.body).await.map_err(|_|Error::internal("failed to read body".to_string())) -    } - -    /// 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())) -    } -} - -/// Convenience wrapper around [`hyper::Response`]. -pub struct Response { -    res: HyperResponse -} - -impl Response { -    pub fn new() -> Self { -        Response{res: HyperResponse::new(Body::empty())} -    } - -    pub fn status(&mut self) -> &mut StatusCode { -        self.res.status_mut() -    } - -    pub fn body(&mut self) -> &mut Body { -        self.res.body_mut() -    } - -    pub fn headers(&mut self) -> &mut hyper::HeaderMap<header::HeaderValue> { -        self.res.headers_mut() -    } - -    pub fn set_header<S: AsRef<str>>(&mut self, header: HeaderName, value: S) { -        self.res.headers_mut().insert(header, HeaderValue::from_str(value.as_ref()).unwrap()); -    } - -    pub fn set_content_type(&mut self, mime: mime::Mime) { -        self.res.headers_mut().insert(header::CONTENT_TYPE, mime.to_string().parse().unwrap()); -    } - -    pub fn redirect<S: AsRef<str>>(&mut self, location: S, code: StatusCode) { -        *self.res.status_mut() = code; -        self.set_header(header::LOCATION, location); -    } - -    pub fn set_cookie(&mut self, cookie: Cookie) { -        self.res.headers_mut().append(header::SET_COOKIE, cookie.encoded().to_string().parse().unwrap()); -    } - -    pub fn delete_cookie(&mut self, name: &str) { -        let mut cookie = Cookie::new(name, ""); -        cookie.set_max_age(Duration::seconds(0)); -        cookie.set_expires(OffsetDateTime::now_utc() - Duration::days(365)); -        self.set_cookie(cookie); -    } -}
\ No newline at end of file +mod signed;
\ No newline at end of file diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..0c25e67 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,159 @@ +//! Provides the [`Parts`] and [`Body`] convenience wrappers. + +use cookie::Cookie; +use header::CONTENT_TYPE; +use mime::{APPLICATION_WWW_FORM_URLENCODED, Mime}; +use serde::{Deserialize, de::DeserializeOwned}; +use hyper::{body::Bytes, header}; +use hyper::http::request::Parts as ReqParts; +use std::collections::HashMap; + +use crate::{Error, security}; + +type HyperRequest = hyper::Request<hyper::Body>; + +/// Convenience wrapper around [`hyper::Body`]. +pub struct Body { +    body: hyper::Body, +    content_type: Option<header::HeaderValue>, +} + +/// Convert [`hyper::Request`] to ([`Parts`], [`Body`]) +pub fn adapt<'a>(req: HyperRequest) -> (Parts, Body) { +    let (parts, body) = req.into_parts(); +    let body = Body{body, content_type: parts.headers.get(CONTENT_TYPE).map(|x| x.to_owned())}; +    let parts = Parts{parts, cookies: None}; +    (parts, body) +} + +/// Convenience wrapper around [`hyper::http::request::Parts`]. +pub struct Parts { +    parts: ReqParts, +    cookies: Option<HashMap<String,Cookie<'static>>>, +} + +#[derive(Deserialize)] +struct CsrfData { +    csrf: String, +} + +impl Parts { +    pub fn cookies(&mut self) -> &HashMap<String,Cookie> { +        if let Some(ref cookies) = self.cookies { +            return cookies +        } +        let mut cookies = HashMap::new(); +        for header in self.parts.headers.get_all(header::COOKIE) { +            let raw_str = match std::str::from_utf8(header.as_bytes()) { +                Ok(string) => string, +                Err(_) => continue +            }; + +            for cookie_str in raw_str.split(';').map(|s| s.trim()) { +                if let Ok(cookie) = Cookie::parse_encoded(cookie_str) { +                    cookies.insert(cookie.name().to_string(), cookie.into_owned()); +                } +            } +        } +        self.cookies = Some(cookies); +        &self.cookies.as_ref().unwrap() +    } + +    pub fn method(&self) -> &hyper::Method { +        &self.parts.method +    } + +    pub fn headers(&self) -> &hyper::HeaderMap<header::HeaderValue> { +        &self.parts.headers +    } + +    pub fn uri(&self) -> &hyper::Uri { +        &self.parts.uri +    } + +    /// 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())) +    } +} + +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())) +    } + +    /// Parses a `application/x-www-form-urlencoded` request body into a given struct. +    /// +    /// This does make you vulnerable to CSRF so you normally want to use +    /// [`parse_form_csrf()`] instead. +    /// +    /// # Example +    /// +    /// ``` +    /// use hyper::{Response}; +    /// use sputnik::{request::Body, Error}; +    /// use serde::Deserialize; +    /// +    /// #[derive(Deserialize)] +    /// struct Message {text: String, year: i64} +    /// +    /// async fn greet(body: Body) -> Result<Response<hyper::Body>, Error> { +    ///     let msg: Message = body.into_form().await?; +    ///     Ok(Response::new(format!("hello {}", msg.text).into())) +    /// } +    /// ``` +    pub async fn into_form<T: DeserializeOwned>(self) -> Result<T,Error> { +        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())) +    } + +    /// Parses a `application/x-www-form-urlencoded` request body into a given struct. +    /// Protects from CSRF by checking that the request body contains the same token retrieved from the cookies. +    /// +    /// The CSRF parameter is expected as the `csrf` parameter in the request body. +    /// This means for HTML forms you need to embed the token as a hidden input. +    /// +    /// # Example +    /// +    /// ``` +    /// use hyper::{Method}; +    /// use sputnik::{request::{Parts, Body}, response::Response, Error}; +    /// use sputnik::security::CsrfToken; +    /// use serde::Deserialize; +    /// +    /// #[derive(Deserialize)] +    /// struct Message {text: String} +    /// +    /// async fn greet(req: &mut Parts, body: Body) -> Result<Response, Error> { +    ///     let mut response = Response::new(); +    ///     let csrf_token = CsrfToken::from_parts(req, &mut response); +    ///     *response.body() = match (req.method()) { +    ///         &Method::GET => format!("<form method=post> +    ///             <input name=text>{}<button>Submit</button></form>", csrf_token.html_input()).into(), +    ///         &Method::POST => { +    ///             let msg: Message = body.into_form_csrf(&csrf_token).await?; +    ///             format!("hello {}", msg.text).into() +    ///         }, +    ///         _ => return Err(Error::method_not_allowed("only GET and POST allowed".to_owned())), +    ///     }; +    ///     Ok(response) +    /// } +    /// ``` +    pub async fn into_form_csrf<T: DeserializeOwned>(self, csrf_token: &security::CsrfToken) -> Result<T,Error> { +        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()))?; +        csrf_token.matches(csrf_data.csrf)?; +        serde_urlencoded::from_bytes::<T>(&full_body).map_err(|e|Error::bad_request(e.to_string())) +    } + +    fn enforce_content_type(&self, mime: Mime) -> Result<(),Error> { +        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))) +    } +}
\ No newline at end of file diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..39e6c98 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,60 @@ +//! Provides the [`Response`] convenience wrapper. + +use cookie::Cookie; +use hyper::{StatusCode, header::{self, HeaderName, HeaderValue}}; +use time::{Duration, OffsetDateTime}; + +type HyperResponse = hyper::Response<hyper::Body>; + +/// Convenience wrapper around [`hyper::Response`]. +pub struct Response { +    res: HyperResponse +} + +impl Into<HyperResponse> for Response { +    fn into(self) -> HyperResponse { +        self.res +    } +} + +impl Response { +    pub fn new() -> Self { +        Response{res: HyperResponse::new(hyper::Body::empty())} +    } + +    pub fn status(&mut self) -> &mut StatusCode { +        self.res.status_mut() +    } + +    pub fn body(&mut self) -> &mut hyper::Body { +        self.res.body_mut() +    } + +    pub fn headers(&mut self) -> &mut hyper::HeaderMap<header::HeaderValue> { +        self.res.headers_mut() +    } + +    pub fn set_header<S: AsRef<str>>(&mut self, header: HeaderName, value: S) { +        self.res.headers_mut().insert(header, HeaderValue::from_str(value.as_ref()).unwrap()); +    } + +    pub fn set_content_type(&mut self, mime: mime::Mime) { +        self.res.headers_mut().insert(header::CONTENT_TYPE, mime.to_string().parse().unwrap()); +    } + +    pub fn redirect<S: AsRef<str>>(&mut self, location: S, code: StatusCode) { +        *self.res.status_mut() = code; +        self.set_header(header::LOCATION, location); +    } + +    pub fn set_cookie(&mut self, cookie: Cookie) { +        self.res.headers_mut().append(header::SET_COOKIE, cookie.encoded().to_string().parse().unwrap()); +    } + +    pub fn delete_cookie(&mut self, name: &str) { +        let mut cookie = Cookie::new(name, ""); +        cookie.set_max_age(Duration::seconds(0)); +        cookie.set_expires(OffsetDateTime::now_utc() - Duration::days(365)); +        self.set_cookie(cookie); +    } +}
\ No newline at end of file diff --git a/src/security.rs b/src/security.rs index fe9c26b..4a17fe3 100644 --- a/src/security.rs +++ b/src/security.rs @@ -6,9 +6,9 @@ use time::{Duration, OffsetDateTime};  pub use crate::signed::Key; -use crate::{Error, Request, Response}; +use crate::{Error, request::Parts, response::Response}; -/// A cookie-based CSRF token to be used with [`crate::Request::into_form_csrf`]. +/// A cookie-based CSRF token to be used with [`crate::request::Body::into_form_csrf`].  pub struct CsrfToken {      token: String,      from_client: bool, @@ -17,7 +17,7 @@ pub struct CsrfToken {  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. -    pub fn from_request(request: &mut Request, response: &mut Response) -> Self { +    pub fn from_parts(request: &mut Parts, response: &mut Response) -> Self {          if let Some(cookie) = request.cookies().get("csrf") {              return CsrfToken{token: cookie.value().to_string(), from_client: true}          } | 
