aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2021-01-24 22:37:36 +0100
committerMartin Fischer <martin@push-f.com>2021-01-24 22:49:57 +0100
commit76e92d7281b45ce506046a8946b7fde3355c485d (patch)
treee3a261e993450047a3a366eae0091efc0948377c
parent4ba2d050bdf1a3c0070f3aa2331c82745611af1f (diff)
define & impl traits instead of wrapping types
bump version to 0.3.0
-rw-r--r--Cargo.toml5
-rw-r--r--README.md89
-rw-r--r--examples/csrf/src/main.rs40
-rw-r--r--src/request.rs249
-rw-r--r--src/response.rs72
-rw-r--r--src/security.rs26
-rw-r--r--src/signed.rs8
7 files changed, 204 insertions, 285 deletions
diff --git a/Cargo.toml b/Cargo.toml
index c58525e..62d1f54 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "sputnik"
-version = "0.2.3"
+version = "0.3.0"
authors = ["Martin Fischer <martin@push-f.com>"]
license = "MIT"
description = "A lightweight layer on top of hyper to facilitate building web applications."
@@ -23,4 +23,5 @@ mime = "0.3"
rand = "0.7.3"
sha2 = "0.9"
time = "0.2"
-thiserror = "1.0" \ No newline at end of file
+thiserror = "1.0"
+async-trait = "0.1.42" \ No newline at end of file
diff --git a/README.md b/README.md
index b2fe919..38bc5d8 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,38 @@
# Sputnik
-A microframework based on [Hyper](https://hyper.rs/) that forces you to:
+A microframework based on [Hyper](https://hyper.rs/) providing traits to:
-* make error handling explicit (no possible failures hidden behind macros)
-* implement your own error type ([because you need to anyway](#error-handling))
+* extend `http::request::Parts` with query parameter deserialization & cookie parsing
+* extend `hyper::Body` with form deserialization (and optional [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) protection)
+* extend `http::response::Builder` with methods to set & delete cookies and set the Content-Type
-Sputnik provides:
+Furthermore Sputnik provides what's necessary to implement [signed & expiring
+cookies](#signed--expiring-cookies) with the expiry date encoded into the
+signed cookie value, providing a more lightweight alternative to JWT if you
+don't need interoperability.
-* convenience wrappers around hyper's `Request` & `Response`
- * parse, set and delete cookies
- (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)
-* 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)
-* `decode_expiring_claim` & `encode_expiring_claim`, which can be combined with
- `Key` to implement [signed & expiring cookies](#signed--expiring-cookies)
- (with the expiry date encoded into the signed cookie value)
+Sputnik does **not** handle routing. For most web applications `match`ing on
+(method, path) suffices. If you need path variables, you can use one of the
+many [router crates](https://crates.io/keywords/router).
-Sputnik does **not**:
-
-* handle routing: for most web apps `match`ing on (method, path) suffices
-* handle configuration: we recommend [toml](https://crates.io/crates/toml)
-* handle persistence: we recommend [diesel](https://diesel.rs/)
-* handle templating: we recommend [maud](https://maud.lambda.xyz/)
-
-## Error handling
-
-Rust provides convenient short-circuiting with the `?` operator, which
-converts errors with `From::from()`. Since you probably want to short-circuit
-errors from other crates (e.g. database errors), a web framework cannot
-provide you an error type since Rust disallows you from defining a `From`
-conversion between two foreign types.
-
-This does imply that you need to define your own error type, allowing you to
-implement a `From` conversion for every error type you want to short-circuit
-with `?`. Fortunately the [thiserror](https://crates.io/crates/thiserror)
-crate makes defining custom errors and `From` implementations trivial.
+Sputnik encourages you to create your own error enum and implement `From`
+conversions for every error type, which you want to short-circuit with the `?`
+operator. This can be easily done with [thiserror](https://crates.io/crates/thiserror)
+because Sputnik restricts its error types to the `'static` lifetime.
## Example
```rust
use std::convert::Infallible;
use hyper::service::{service_fn, make_service_fn};
-use hyper::{Method, Server, StatusCode};
+use hyper::{Method, Server, StatusCode, Body};
+use hyper::http::request::Parts;
+use hyper::http::response::Builder;
use serde::Deserialize;
-use sputnik::security::CsrfToken;
-use sputnik::{request::{Parts, Body}, response::Response};
-use sputnik::request::error::*;
+use sputnik::{mime, request::{SputnikParts, SputnikBody}, response::SputnikBuilder};
+use sputnik::request::CsrfProtectedFormError;
+
+type Response = hyper::Response<Body>;
#[derive(thiserror::Error, Debug)]
enum Error {
@@ -65,8 +49,8 @@ fn render_error(err: Error) -> (StatusCode, String) {
}
}
-async fn route(req: &mut Parts, body: Body) -> Result<Response,Error> {
- match (req.method(), req.uri().path()) {
+async fn route(req: &mut Parts, body: Body) -> Result<Response, Error> {
+ match (&req.method, req.uri.path()) {
(&Method::GET, "/form") => get_form(req).await,
(&Method::POST, "/form") => post_form(req, body).await,
_ => return Err(Error::NotFound("page not found".to_owned()))
@@ -74,29 +58,30 @@ async fn route(req: &mut Parts, body: Body) -> Result<Response,Error> {
}
async fn get_form(req: &mut Parts) -> Result<Response, Error> {
- let mut response = Response::new();
- let csrf_token = CsrfToken::from_parts(req, &mut response);
- *response.body() = format!("<form method=post>
- <input name=text>{}<button>Submit</button></form>", csrf_token.html_input()).into();
- Ok(response)
+ let mut response = Builder::new();
+ let csrf_token = req.csrf_token(&mut response);
+ Ok(response.content_type(mime::TEXT_HTML).body(
+ format!("<form method=post>
+ <input name=text>{}<button>Submit</button></form>", csrf_token.html_input()).into()
+ ).unwrap())
}
#[derive(Deserialize)]
struct FormData {text: String}
async fn post_form(req: &mut Parts, body: Body) -> Result<Response, Error> {
- let mut response = Response::new();
- let csrf_token = CsrfToken::from_parts(req, &mut response);
+ let mut response = Builder::new();
+ let csrf_token = req.csrf_token(&mut response);
let msg: FormData = body.into_form_csrf(&csrf_token).await?;
- *response.body() = format!("hello {}", msg.text).into();
- Ok(response)
+ Ok(response.body(
+ format!("hello {}", msg.text).into()
+ ).unwrap())
}
-/// adapt between Hyper's types and Sputnik's convenience types
async fn service(req: hyper::Request<hyper::Body>) -> Result<hyper::Response<hyper::Body>, Infallible> {
- let (mut parts, body) = sputnik::request::adapt(req);
+ let (mut parts, body) = req.into_parts();
match route(&mut parts, body).await {
- Ok(res) => Ok(res.into()),
+ Ok(res) => Ok(res),
Err(err) => {
let (code, message) = render_error(err);
// you can easily wrap or log errors here
diff --git a/examples/csrf/src/main.rs b/examples/csrf/src/main.rs
index 915d063..e7e1bfa 100644
--- a/examples/csrf/src/main.rs
+++ b/examples/csrf/src/main.rs
@@ -1,10 +1,13 @@
use std::convert::Infallible;
use hyper::service::{service_fn, make_service_fn};
-use hyper::{Method, Server, StatusCode};
+use hyper::{Method, Server, StatusCode, Body};
+use hyper::http::request::Parts;
+use hyper::http::response::Builder;
use serde::Deserialize;
-use sputnik::security::CsrfToken;
-use sputnik::{request::{Parts, Body}, response::Response};
-use sputnik::request::error::*;
+use sputnik::{mime, request::{SputnikParts, SputnikBody}, response::SputnikBuilder};
+use sputnik::request::CsrfProtectedFormError;
+
+type Response = hyper::Response<Body>;
#[derive(thiserror::Error, Debug)]
enum Error {
@@ -21,8 +24,8 @@ fn render_error(err: Error) -> (StatusCode, String) {
}
}
-async fn route(req: &mut Parts, body: Body) -> Result<Response,Error> {
- match (req.method(), req.uri().path()) {
+async fn route(req: &mut Parts, body: Body) -> Result<Response, Error> {
+ match (&req.method, req.uri.path()) {
(&Method::GET, "/form") => get_form(req).await,
(&Method::POST, "/form") => post_form(req, body).await,
_ => return Err(Error::NotFound("page not found".to_owned()))
@@ -30,29 +33,30 @@ async fn route(req: &mut Parts, body: Body) -> Result<Response,Error> {
}
async fn get_form(req: &mut Parts) -> Result<Response, Error> {
- let mut response = Response::new();
- let csrf_token = CsrfToken::from_parts(req, &mut response);
- *response.body() = format!("<form method=post>
- <input name=text>{}<button>Submit</button></form>", csrf_token.html_input()).into();
- Ok(response)
+ let mut response = Builder::new();
+ let csrf_token = req.csrf_token(&mut response);
+ Ok(response.content_type(mime::TEXT_HTML).body(
+ format!("<form method=post>
+ <input name=text>{}<button>Submit</button></form>", csrf_token.html_input()).into()
+ ).unwrap())
}
#[derive(Deserialize)]
struct FormData {text: String}
async fn post_form(req: &mut Parts, body: Body) -> Result<Response, Error> {
- let mut response = Response::new();
- let csrf_token = CsrfToken::from_parts(req, &mut response);
+ let mut response = Builder::new();
+ let csrf_token = req.csrf_token(&mut response);
let msg: FormData = body.into_form_csrf(&csrf_token).await?;
- *response.body() = format!("hello {}", msg.text).into();
- Ok(response)
+ Ok(response.body(
+ format!("hello {}", msg.text).into()
+ ).unwrap())
}
-/// adapt between Hyper's types and Sputnik's convenience types
async fn service(req: hyper::Request<hyper::Body>) -> Result<hyper::Response<hyper::Body>, Infallible> {
- let (mut parts, body) = sputnik::request::adapt(req);
+ let (mut parts, body) = req.into_parts();
match route(&mut parts, body).await {
- Ok(res) => Ok(res.into()),
+ Ok(res) => Ok(res),
Err(err) => {
let (code, message) = render_error(err);
// you can easily wrap or log errors here
diff --git a/src/request.rs b/src/request.rs
index 953e7ec..1166ef2 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -1,51 +1,44 @@
-//! Provides the [`Parts`] and [`Body`] convenience wrappers.
+//! Provides the [`SputnikParts`] and [`SputnikBody`] traits.
use cookie::Cookie;
-use header::CONTENT_TYPE;
-use mime::{APPLICATION_WWW_FORM_URLENCODED, Mime};
+use mime::Mime;
+use rand::{Rng, distributions::Alphanumeric};
+use security::CsrfToken;
use serde::{Deserialize, de::DeserializeOwned};
use hyper::{body::Bytes, header};
-use hyper::http::request::Parts as ReqParts;
-use std::collections::HashMap;
+use time::Duration;
+use std::{collections::HashMap, sync::Arc};
-use crate::security;
+use crate::{response::SputnikBuilder, security};
-use error::*;
-
-type HyperRequest = hyper::Request<hyper::Body>;
+pub trait SputnikParts {
+ /// Parses the query string of the request into a given struct.
+ fn query<X: DeserializeOwned>(&self) -> Result<X,QueryError>;
-/// Convenience wrapper around [`hyper::Body`].
-pub struct Body {
- body: hyper::Body,
- content_type: Option<header::HeaderValue>,
-}
+ /// Parses the cookies of the request.
+ fn cookies(&mut self) -> Arc<HashMap<String, Cookie<'static>>>;
-/// 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)
-}
+ /// Retrieves the CSRF token from a `csrf` cookie or generates
+ /// a new token and stores it as a cookie if it doesn't exist.
+ fn csrf_token(&mut self, builder: &mut dyn SputnikBuilder) -> CsrfToken;
-/// Convenience wrapper around [`hyper::http::request::Parts`].
-pub struct Parts {
- parts: ReqParts,
- cookies: Option<HashMap<String,Cookie<'static>>>,
+ /// Enforces a specific Content-Type.
+ fn enforce_content_type(&self, mime: Mime) -> Result<(), WrongContentTypeError>;
}
-#[derive(Deserialize)]
-struct CsrfData {
- csrf: String,
-}
+impl SputnikParts for hyper::http::request::Parts {
+ fn query<T: DeserializeOwned>(&self) -> Result<T,QueryError> {
+ serde_urlencoded::from_str::<T>(self.uri.query().unwrap_or("")).map_err(QueryError)
+ }
-impl Parts {
- pub fn cookies(&mut self) -> &HashMap<String,Cookie> {
- if let Some(ref cookies) = self.cookies {
- return cookies
+ fn cookies(&mut self) -> Arc<HashMap<String, Cookie<'static>>> {
+ let cookies: Option<&Arc<HashMap<String, Cookie>>> = self.extensions.get();
+ if let Some(cookies) = cookies {
+ return cookies.clone();
}
+
let mut cookies = HashMap::new();
- for header in self.parts.headers.get_all(header::COOKIE) {
+ for header in self.headers.get_all(header::COOKIE) {
let raw_str = match std::str::from_utf8(header.as_bytes()) {
Ok(string) => string,
Err(_) => continue
@@ -57,149 +50,113 @@ impl Parts {
}
}
}
- self.cookies = Some(cookies);
- &self.cookies.as_ref().unwrap()
- }
-
- pub fn method(&self) -> &hyper::Method {
- &self.parts.method
+ let cookies = Arc::new(cookies);
+ self.extensions.insert(cookies.clone());
+ cookies
}
- pub fn headers(&self) -> &hyper::HeaderMap<header::HeaderValue> {
- &self.parts.headers
- }
-
- pub fn uri(&self) -> &hyper::Uri {
- &self.parts.uri
+ fn csrf_token(&mut self, builder: &mut dyn SputnikBuilder) -> CsrfToken {
+ if let Some(cookie) = self.cookies().get("csrf") {
+ return CsrfToken{token: cookie.value().to_string(), from_client: true}
+ }
+ let val: String = rand::thread_rng().sample_iter(Alphanumeric).take(16).collect();
+ let mut c = Cookie::new("csrf", val.clone());
+ c.set_secure(Some(true));
+ c.set_max_age(Some(Duration::hours(1)));
+ builder.set_cookie(c);
+ CsrfToken{token: val, from_client: false}
}
- /// Parses the query string of the request into a given struct.
- pub fn query<T: DeserializeOwned>(&self) -> Result<T,QueryError> {
- serde_urlencoded::from_str::<T>(self.parts.uri.query().unwrap_or("")).map_err(QueryError)
+ fn enforce_content_type(&self, mime: Mime) -> Result<(), WrongContentTypeError> {
+ if let Some(content_type) = self.headers.get(header::CONTENT_TYPE) {
+ if *content_type == mime.to_string() {
+ return Ok(())
+ }
+ }
+ Err(WrongContentTypeError{expected: mime, received: self.headers.get(header::CONTENT_TYPE).as_ref().and_then(|h| h.to_str().ok().map(|s| s.to_owned()))})
}
}
-impl Body {
- pub async fn into_bytes(self) -> Result<Bytes, BodyError> {
- hyper::body::to_bytes(self.body).await.map_err(BodyError)
- }
+use async_trait::async_trait;
+
+#[async_trait]
+pub trait SputnikBody {
+ async fn into_bytes(self) -> Result<Bytes, BodyError>;
/// 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
- /// [`Body::into_form_csrf()`] instead.
- ///
- /// # Example
- ///
- /// ```
- /// use hyper::{Response};
- /// use sputnik::request::{Body, error::FormError};
- /// use serde::Deserialize;
- ///
- /// #[derive(Deserialize)]
- /// struct Message {text: String, year: i64}
- ///
- /// async fn greet(body: Body) -> Result<Response<hyper::Body>, FormError> {
- /// 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, 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(FormError::Deserialize)
- }
+ /// [`SputnikBody::into_form_csrf()`] instead.
+ async fn into_form<T: DeserializeOwned>(self) -> Result<T, FormError>;
/// 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, error::CsrfProtectedFormError}, response::Response};
- /// use sputnik::security::CsrfToken;
- /// use serde::Deserialize;
- ///
- /// #[derive(Deserialize)]
- /// struct Message {text: String}
- ///
- /// async fn greet(req: &mut Parts, body: Body) -> Result<Response, CsrfProtectedFormError> {
- /// let mut response = Response::new();
- /// let csrf_token = CsrfToken::from_parts(req, &mut response);
- /// let msg: Message = body.into_form_csrf(&csrf_token).await?;
- /// *response.body() = format!("hello {}", msg.text).into();
- /// Ok(response)
- /// }
- /// ```
- pub async fn into_form_csrf<T: DeserializeOwned>(self, csrf_token: &security::CsrfToken) -> Result<T, CsrfProtectedFormError> {
- self.enforce_content_type(APPLICATION_WWW_FORM_URLENCODED)?;
+ async fn into_form_csrf<T: DeserializeOwned>(self, csrf_token: &security::CsrfToken) -> Result<T, CsrfProtectedFormError>;
+}
+
+#[async_trait]
+impl SputnikBody for hyper::Body {
+ async fn into_bytes(self) -> Result<Bytes, BodyError> {
+ hyper::body::to_bytes(self).await.map_err(BodyError)
+ }
+
+ async fn into_form<T: DeserializeOwned>(self) -> Result<T, FormError> {
+ let full_body = self.into_bytes().await?;
+ Ok(serde_urlencoded::from_bytes::<T>(&full_body)?)
+ }
+
+ async fn into_form_csrf<T: DeserializeOwned>(self, csrf_token: &CsrfToken) -> Result<T, CsrfProtectedFormError> {
let full_body = self.into_bytes().await?;
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(CsrfProtectedFormError::Deserialize)
}
-
- 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(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 crate::security::CsrfError;
- #[derive(Error, Debug)]
- #[error("query deserialize error: {0}")]
- pub struct QueryError(pub serde_urlencoded::de::Error);
-
- #[derive(Error, Debug)]
- #[error("failed to read body")]
- pub struct BodyError(pub hyper::Error);
+#[derive(Deserialize)]
+struct CsrfData {
+ csrf: String,
+}
- #[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>,
- }
+use crate::security::CsrfError;
+#[derive(thiserror::Error, Debug)]
+#[error("query deserialize error: {0}")]
+pub struct QueryError(pub serde_urlencoded::de::Error);
- #[derive(Error, Debug)]
- pub enum FormError {
- #[error("{0}")]
- ContentType(#[from] WrongContentTypeError),
+#[derive(thiserror::Error, Debug)]
+#[error("failed to read body")]
+pub struct BodyError(pub hyper::Error);
- #[error("{0}")]
- Body(#[from] BodyError),
+#[derive(thiserror::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>,
+}
- #[error("form deserialize error: {0}")]
- Deserialize(#[from] serde_urlencoded::de::Error),
- }
+#[derive(thiserror::Error, Debug)]
+pub enum FormError {
+ #[error("{0}")]
+ Body(#[from] BodyError),
- #[derive(Error, Debug)]
- pub enum CsrfProtectedFormError {
- #[error("{0}")]
- ContentType(#[from] WrongContentTypeError),
+ #[error("form deserialize error: {0}")]
+ Deserialize(#[from] serde_urlencoded::de::Error),
+}
- #[error("{0}")]
- Body(#[from] BodyError),
+#[derive(thiserror::Error, Debug)]
+pub enum CsrfProtectedFormError {
+ #[error("{0}")]
+ Body(#[from] BodyError),
- #[error("form deserialize error: {0}")]
- Deserialize(#[from] serde_urlencoded::de::Error),
+ #[error("form deserialize error: {0}")]
+ Deserialize(#[from] serde_urlencoded::de::Error),
- #[error("no csrf token in form data")]
- NoCsrf,
+ #[error("no csrf token in form data")]
+ NoCsrf,
- #[error("{0}")]
- Csrf(#[from] CsrfError),
- }
+ #[error("{0}")]
+ Csrf(#[from] CsrfError),
} \ No newline at end of file
diff --git a/src/response.rs b/src/response.rs
index 39e6c98..9c92e4c 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -1,60 +1,50 @@
-//! Provides the [`Response`] convenience wrapper.
+//! Provides the [`SputnikBuilder`] trait.
use cookie::Cookie;
-use hyper::{StatusCode, header::{self, HeaderName, HeaderValue}};
+use hyper::{StatusCode, header::{self, HeaderValue}, http};
use time::{Duration, OffsetDateTime};
+use hyper::http::response::Builder;
-type HyperResponse = hyper::Response<hyper::Body>;
+pub trait SputnikBuilder {
+ /// Adds a Set-Cookie header.
+ fn set_cookie(&mut self, cookie: Cookie);
-/// Convenience wrapper around [`hyper::Response`].
-pub struct Response {
- res: HyperResponse
-}
+ /// Adds a Set-Cookie header to delete a cookie.
+ fn delete_cookie(&mut self, name: &str);
-impl Into<HyperResponse> for Response {
- fn into(self) -> HyperResponse {
- self.res
- }
+ /// Sets the Content-Type.
+ fn content_type(self, mime: mime::Mime) -> Builder;
}
-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 redirect(location: &str, code: StatusCode) -> Builder {
+ Builder::new().status(code).header(header::LOCATION, location)
+}
- 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());
+impl SputnikBuilder for Builder {
+ fn set_cookie(&mut self, cookie: Cookie) {
+ self.headers_mut().unwrap().insert(header::SET_COOKIE, HeaderValue::from_str(&cookie.encoded().to_string()).unwrap());
}
- pub fn set_content_type(&mut self, mime: mime::Mime) {
- self.res.headers_mut().insert(header::CONTENT_TYPE, mime.to_string().parse().unwrap());
+ 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);
}
- pub fn redirect<S: AsRef<str>>(&mut self, location: S, code: StatusCode) {
- *self.res.status_mut() = code;
- self.set_header(header::LOCATION, location);
+ fn content_type(self, mime: mime::Mime) -> Self {
+ self.header(header::CONTENT_TYPE, mime.to_string())
}
+}
- pub fn set_cookie(&mut self, cookie: Cookie) {
- self.res.headers_mut().append(header::SET_COOKIE, cookie.encoded().to_string().parse().unwrap());
- }
+pub trait EmptyBuilder<B> {
+ /// Consume the builder with an empty body.
+ fn empty(self) -> http::Result<http::response::Response<B>>;
+}
- 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);
+impl EmptyBuilder<hyper::Body> for Builder {
+ fn empty(self) -> http::Result<http::response::Response<hyper::Body>> {
+ self.body(hyper::Body::empty())
}
} \ No newline at end of file
diff --git a/src/security.rs b/src/security.rs
index c12a97f..5247d9e 100644
--- a/src/security.rs
+++ b/src/security.rs
@@ -1,18 +1,14 @@
//! [`CsrfToken`], [`Key`] and functions to encode & decode expiring claims.
-use rand::{Rng, distributions::Alphanumeric};
-use ::cookie::Cookie;
-use time::{Duration, OffsetDateTime};
+use time::OffsetDateTime;
use thiserror::Error;
pub use crate::signed::Key;
-use crate::{request::Parts, response::Response};
-
-/// A cookie-based CSRF token to be used with [`crate::request::Body::into_form_csrf`].
+/// A cookie-based CSRF token to be used with [`crate::request::SputnikBody::into_form_csrf`].
pub struct CsrfToken {
- token: String,
- from_client: bool,
+ pub(crate) token: String,
+ pub(crate) from_client: bool,
}
#[derive(Error, Debug)]
@@ -25,20 +21,6 @@ pub enum CsrfError {
}
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_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}
- }
- let val: String = rand::thread_rng().sample_iter(Alphanumeric).take(16).collect();
- let mut c = Cookie::new("csrf", val.clone());
- c.set_secure(Some(true));
- c.set_max_age(Some(Duration::hours(1)));
- response.set_cookie(c);
- CsrfToken{token: val, from_client: false}
- }
-
/// Wraps the token in a hidden HTML input.
pub fn html_input(&self) -> String {
format!("<input name=csrf type=hidden value=\"{}\">", self.token)
diff --git a/src/signed.rs b/src/signed.rs
index 9f0d184..4da6760 100644
--- a/src/signed.rs
+++ b/src/signed.rs
@@ -1,7 +1,3 @@
-// This code was adapted from the cookie crate which does not make the sign and verify functions public
-// forcing the use of CookieJars, which are akward to work with without a high-level framework.
-// Thanks to Sergio Benitez for writing the original code and releasing it under MIT!
-
use hmac::{Hmac,NewMac,Mac};
use sha2::Sha256;
@@ -9,6 +5,10 @@ const SIGNED_KEY_LEN: usize = 32;
const BASE64_DIGEST_LEN: usize = 44;
/// A convenience wrapper around HMAC.
+///
+/// This code was adapted from the [`cookie`] crate which does not make the sign and verify functions public
+/// forcing the use of [`CookieJar`](cookie::CookieJar)s, which are akward to work with without a high-level framework.
+// Thanks to Sergio Benitez for writing the original code and releasing it under MIT!
pub struct Key (pub [u8; SIGNED_KEY_LEN]);
impl Key {