A microframework based on Hyper that forces you to:
Sputnik provides:
Request
& Response
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
(with the expiry date encoded into the signed cookie value)Sputnik does not:
match
ing on (method, path) sufficesRust 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
crate makes defining custom errors and From
implementations trivial.
```rust use std::convert::Infallible; use hyper::service::{servicefn, makeservice_fn}; use hyper::{Method, Server, StatusCode}; use serde::Deserialize; use sputnik::security::CsrfToken; use sputnik::{request::{Parts, Body}, response::Response}; use sputnik::request::error::*;
enum Error { #[error("page not found")] NotFound(String), #[error("{0}")] CsrfError(#[from] CsrfProtectedFormError) }
fn rendererror(err: Error) -> (StatusCode, String) { match err { Error::NotFound(msg) => (StatusCode::NOTFOUND, msg), Error::CsrfError(err) => (StatusCode::BADREQUEST, err.tostring()), } }
async fn route(req: &mut Parts, body: Body) -> Result
async fn getform(req: &mut Parts) -> Result
struct FormData {text: String}
async fn postform(req: &mut Parts, body: Body) -> Result
/// adapt between Hyper's types and Sputnik's convenience types
async fn service(req: hyper::Request
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;
} ```
After a successful authentication you can build a session id cookie for example as follows:
rust
let expiry_date = OffsetDateTime::now_utc() + Duration::hours(24);
let mut cookie = Cookie::new("userid",
key.sign(
&encode_expiring_claim(&userid, expiry_date)
));
cookie.set_secure(Some(true));
cookie.set_expires(expiry_date);
cookie.set_same_site(SameSite::Lax);
resp.set_cookie(cookie);
This session id cookie can then be retrieved and verified as follows:
rust
let userid = req.cookies().get("userid")
.ok_or_else(|| "expected userid cookie".to_owned())
.and_then(|cookie| key.verify(cookie.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.