Sputnik

This library extends the types from the http crate:

If you use Hyper and want to deserialize request bodies with Serde you can enable the following feature flags:

With the security feature Sputnik furthermore provides what's necessary to implement 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.

Sputnik does not handle routing because even complex routing can be quite easily implemented with nested match blocks. If you want a more high-level router, you can check out the router crates.

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 because Sputnik restricts its error types to the 'static lifetime.

Security Considerations

Protect your application against CSRF by setting SameSite to Lax or Strict for your cookies and checking that the Origin header matches your domain name (especially if you have unauthenticated POST endpoints).

Hyper Example

```rust use std::convert::Infallible; use hyper::service::{servicefn, makeservicefn}; use hyper::{Method, Server, StatusCode, Body}; use hyper::http::request::Parts; use hyper::http::response::Builder; use serde::Deserialize; use sputnik::{htmlescape, mime, request::SputnikParts, response::SputnikBuilder}; use sputnik::hyper_body::{SputnikBody, FormError};

type Response = hyper::Response;

[derive(thiserror::Error, Debug)]

enum Error { #[error("page not found")] NotFound(String), #[error("{0}")] FormError(#[from] FormError) }

fn rendererror(err: Error) -> (StatusCode, String) { match err { Error::NotFound(msg) => (StatusCode::NOTFOUND, msg), Error::FormError(err) => (StatusCode::BADREQUEST, err.tostring()), } }

async fn route(req: &mut Parts, body: Body) -> Result { match (&req.method, req.uri.path()) { (&Method::GET, "/form") => Ok(getform(req)), (&Method::POST, "/form") => postform(req, body).await, _ => return Err(Error::NotFound("page not found".to_owned())) } }

fn getform(req: &mut Parts) -> Response { Builder::new() .contenttype(mime::TEXTHTML) .body( "

".into() ).unwrap() }

[derive(Deserialize)]

struct FormData {text: String}

async fn postform(req: &mut Parts, body: Body) -> Result { let msg: FormData = body.intoform().await?; Ok(Builder::new().contenttype(mime::TEXTHTML).body( format!("hello {}", htmlescape(msg.text)).into() ).unwrap()) }

async fn service(req: hyper::Request) -> Result, Infallible> { let (mut parts, body) = req.intoparts(); match route(&mut parts, body).await { Ok(mut res) => { for (k,v) in parts.responseheaders().iter() { res.headersmut().append(k, v.clone()); } Ok(res) } Err(err) => { let (code, message) = rendererror(err); // you can easily wrap or log errors here Ok(hyper::Response::builder().status(code).body(message.into()).unwrap()) } } }

[tokio::main]

async fn main() { let service = makeservicefn(move || { async move { Ok::<_, hyper::Error>(servicefn(move |req| { service(req) })) } });

let addr = ([127, 0, 0, 1], 8000).into();
let server = Server::bind(&addr).serve(service);
println!("Listening on http://{}", addr);
server.await;

} ```

Signed & expiring cookies

After a successful authentication you can build a session id cookie for example as follows:

rust let expiry_date = SystemTime::now() + Duration::from_secs(24 * 60 * 60); let mut cookie = Cookie::new("userid", key.sign( &encode_expiring_claim(&userid, expiry_date) )); headers.set_cookie(Cookie{ name: "userid".into(), value: key.sign( &encode_expiring_claim(&userid, expiry_date) ), secure: Some(true), expires: Some(expiry_date), same_site: SameSite::Lax, });

This session id cookie can then be retrieved and verified as follows:

rust let userid = req.cookies().find(|(name, _value)| *name == "userid") .ok_or_else(|| "expected userid cookie".to_owned()) .and_then(|(_name, value)| key.verify(value)) .and_then(|value| decode_expiring_claim(value).map_err(|e| format!("failed to decode userid cookie: {}", e)));

Tip: If you want to store multiple claims in the cookie, you can (de)serialize a struct with serde_json. This approach can pose a lightweight alternative to JWT, if you don't care about the standardization aspect.