diff options
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | examples/csrf/src/main.rs | 2 | ||||
-rw-r--r-- | src/request.rs | 36 |
4 files changed, 30 insertions, 12 deletions
@@ -1,6 +1,6 @@ [package] name = "sputnik" -version = "0.3.2" +version = "0.3.3" authors = ["Martin Fischer <martin@push-f.com>"] license = "MIT" description = "A lightweight layer on top of hyper to facilitate building web applications." @@ -59,7 +59,7 @@ async fn route(req: &mut Parts, body: Body) -> Result<Response, Error> { fn get_form(req: &mut Parts) -> Response { let mut response = Builder::new(); - let csrf_input = req.csrf_html_input(&mut response); + let csrf_input = req.csrf_token(&mut response).html_input(); response.content_type(mime::TEXT_HTML).body( format!("<form method=post> <input name=text>{}<button>Submit</button></form>", csrf_input).into() diff --git a/examples/csrf/src/main.rs b/examples/csrf/src/main.rs index 94fd09c..1048689 100644 --- a/examples/csrf/src/main.rs +++ b/examples/csrf/src/main.rs @@ -34,7 +34,7 @@ async fn route(req: &mut Parts, body: Body) -> Result<Response, Error> { fn get_form(req: &mut Parts) -> Response { let mut response = Builder::new(); - let csrf_input = req.csrf_html_input(&mut response); + let csrf_input = req.csrf_token(&mut response).html_input(); response.content_type(mime::TEXT_HTML).body( format!("<form method=post> <input name=text>{}<button>Submit</button></form>", csrf_input).into() diff --git a/src/request.rs b/src/request.rs index 5b5679d..7734d03 100644 --- a/src/request.rs +++ b/src/request.rs @@ -23,11 +23,9 @@ pub trait SputnikParts { /// Enforces a specific Content-Type. fn enforce_content_type(&self, mime: Mime) -> Result<(), WrongContentTypeError>; - /// Retrievs the CSRF token from a cookie or generates - /// a new token and stores it as a cookie if it doesn't exist. - /// Returns a hidden HTML input to be embedded in forms that are received - /// with [`crate::request::SputnikBody::into_form_csrf`]. - fn csrf_html_input(&mut self, builder: &mut Builder) -> String; + /// Returns a CSRF token, either extracted from the `csrf` cookie or newly + /// generated if the cookie wasn't sent (in which case the cookie is set). + fn csrf_token(&mut self, builder: &mut Builder) -> CsrfToken; } impl SputnikParts for hyper::http::request::Parts { @@ -68,16 +66,36 @@ impl SputnikParts for hyper::http::request::Parts { 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()))}) } - fn csrf_html_input(&mut self, builder: &mut Builder) -> String { + fn csrf_token(&mut self, builder: &mut Builder) -> CsrfToken { let token = csrf_token_from_cookies(self).unwrap_or_else(|| { - let token: String = rand::thread_rng().sample_iter(Alphanumeric).take(16).collect(); + let token: String = rand::thread_rng().sample_iter( + Alphanumeric + // must be HTML-safe because we embed it in CsrfToken::html_input + ).take(16).collect(); let mut c = Cookie::new(CSRF_COOKIE_NAME, token.clone()); c.set_secure(Some(true)); c.set_max_age(Some(Duration::hours(1))); builder.set_cookie(c); token }); - format!("<input name=csrf type=hidden value=\"{}\">", token) + CsrfToken(token) + } +} + +/// A CSRF token retrievable with [`SputnikParts::csrf_token`]. +pub struct CsrfToken(String); + +impl std::fmt::Display for CsrfToken { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl CsrfToken { + /// Returns a hidden HTML input to be embedded in forms that are received + /// with [`crate::request::SputnikBody::into_form_csrf`]. + pub fn html_input(&self) -> String { + format!("<input name=csrf type=hidden value=\"{}\">", self) } } @@ -94,7 +112,7 @@ pub trait SputnikBody { /// 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 HTML form must embed a hidden input generated with [`crate::request::SputnikParts::csrf_html_input`]. + /// The HTML form must embed a hidden input generated with [`CsrfToken::html_input`]. async fn into_form_csrf<T: DeserializeOwned>(self, req: &mut Parts) -> Result<T, CsrfProtectedFormError>; /// Attempts to deserialize the request body as JSON. |