sputnik-0.3.1 has been yanked.
Sputnik
A microframework based on Hyper providing traits to:
- extend
http::request::Parts with query parameter deserialization & cookie parsing
- extend
hyper::Body with form deserialization (and optional CSRF protection)
- extend
http::response::Builder with methods to set & delete cookies and set the Content-Type
Furthermore Sputnik 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. For most web applications matching on
(method, path) suffices. If you need path variables, you can use one of the
many 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.
Example
use std::convert::Infallible;
use hyper::service::{service_fn, make_service_fn};
use hyper::{Method, Server, StatusCode, Body};
use hyper::http::request::Parts;
use hyper::http::response::Builder;
use serde::Deserialize;
use sputnik::{mime, request::{SputnikParts, SputnikBody}, response::SputnikBuilder};
use sputnik::request::CsrfProtectedFormError;
type Response = hyper::Response<Body>;
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("page not found")]
NotFound(String),
#[error("{0}")]
CsrfError(#[from] CsrfProtectedFormError)
}
fn render_error(err: Error) -> (StatusCode, String) {
match err {
Error::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
Error::CsrfError(err) => (StatusCode::BAD_REQUEST, err.to_string()),
}
}
async fn route(req: &mut Parts, body: Body) -> Result<Response, Error> {
match (&req.method, req.uri.path()) {
(&Method::GET, "/form") => Ok(get_form(req)),
(&Method::POST, "/form") => post_form(req, body).await,
_ => return Err(Error::NotFound("page not found".to_owned()))
}
}
fn get_form(req: &mut Parts) -> Response {
let mut response = Builder::new();
let csrf_input = req.csrf_html_input(&mut response);
response.content_type(mime::TEXT_HTML).body(
format!("<form method=post>
<input name=text>{}<button>Submit</button></form>", csrf_input).into()
).unwrap()
}
#[derive(Deserialize)]
struct FormData {text: String}
async fn post_form(req: &mut Parts, body: Body) -> Result<Response, Error> {
let msg: FormData = body.into_form_csrf(req).await?;
Ok(Builder::new().body(
format!("hello {}", msg.text).into()
).unwrap())
}
async fn service(req: hyper::Request<hyper::Body>) -> Result<hyper::Response<hyper::Body>, Infallible> {
let (mut parts, body) = req.into_parts();
match route(&mut parts, body).await {
Ok(res) => Ok(res),
Err(err) => {
let (code, message) = render_error(err);
Ok(hyper::Response::builder().status(code).body(message.into()).unwrap())
}
}
}
#[tokio::main]
async fn main() {
let service = make_service_fn(move |_| {
async move {
Ok::<_, hyper::Error>(service_fn(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:
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:
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.